一文搞懂 SO 脱壳全流程:识别加壳、Frida Dump、原理深入解析
使用 IDA 打开 so 提示无法正确识别 ELF 文件结构。section 定义无效或不符合预期格式。有很多红色的汇编代码块,表示错误或者未能正常解析的地址/数据这通常就是 so 可能 2025-8-31 23:50:0 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

使用 IDA 打开 so 提示无法正确识别 ELF 文件结构。

图片

section 定义无效或不符合预期格式。

图片

有很多红色的汇编代码块,表示错误或者未能正常解析的地址/数据

图片

这通常就是 so 可能被“混淆”、“裁剪”或“加壳”了。

frida_dump 是基于 frida 的 so 和 dex 的脱壳工具。

先把 frida_dump 源码 clone 到本地。 如果使用的是远程链接,把 dump_so.py 中的

device: frida.core.Device = frida.get_usb_device()

改成

device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")

比如目标 so 是 libGameVMP.so,通过下面命令执行 dump_so.py

python dump_so.py libGameVMP.so

输出如下:

(anti-app) PS D:\Python\anti-app\frida_dump> python dump_so.py libGameVMP.so{'name': 'libGameVMP.so', 'base': '0x7bd7b81000', 'size': 462848, 'path': '/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so'}libGameVMP.so.dump.soandroid/SoFixer64: 1 file pushed, 0 skipped. 66.8 MB/s (186656 bytes in 0.003s)libGameVMP.so.dump.so: 1 file pushed, 0 skipped. 217.6 MB/s (462848 bytes in 0.002s)adb shell /data/local/tmp/SoFixer -m 0x7bd7b81000 -s /data/local/tmp/libGameVMP.so.dump.so -o /data/local/tmp/libGameVMP.so.dump.so.fix.so[main_loop:87]start to rebuild elf file[Load:69]dynamic segment have been found in loadable segment, argument baseso will be ignored.[RebuildPhdr:25]=============LoadDynamicSectionFromBaseSource==========RebuildPhdr=========================[RebuildPhdr:37]=====================RebuildPhdr End======================[ReadSoInfo:549]=======================ReadSoInfo=========================[ReadSoInfo:696]soname[ReadSoInfo:621] constructors (DT_INIT) found at 1bd68[ReadSoInfo:629] constructors (DT_INIT_ARRAY) found at 6e9e8[ReadSoInfo:633] constructors (DT_INIT_ARRAYSZ) 27[ReadSoInfo:637] destructors (DT_FINI_ARRAY) found at 6eac0[ReadSoInfo:641] destructors (DT_FINI_ARRAYSZ) 2[ReadSoInfo:580]string table found at ec0[ReadSoInfo:584]symbol table found at 518[ReadSoInfo:595] plt_rel_count (DT_PLTRELSZ) 93[ReadSoInfo:591] plt_rel (DT_JMPREL) found at 1c78[ReadSoInfo:699]Unused DT entry: type 0x00000009 arg 0x00000018[ReadSoInfo:699]Unused DT entry: type 0x00000018 arg 0x00000000[ReadSoInfo:699]Unused DT entry: type 0x6ffffffb arg 0x00000001[ReadSoInfo:699]Unused DT entry: type 0x6ffffffe arg 0x000012d0[ReadSoInfo:699]Unused DT entry: type 0x6fffffff arg 0x00000003[ReadSoInfo:699]Unused DT entry: type 0x6ffffff0 arg 0x00001202[ReadSoInfo:699]Unused DT entry: type 0x6ffffff9 arg 0x0000004c[ReadSoInfo:703]=======================ReadSoInfo End=========================[RebuildShdr:42]=======================RebuildShdr=========================[RebuildShdr:536]=====================RebuildShdr End======================[RebuildRelocs:783]=======================RebuildRelocs=========================[RebuildRelocs:809]=======================RebuildRelocs End=======================[RebuildFin:709]=======================try to finish file rebuild =========================[RebuildFin:733]=======================End=========================[main:123]Done!!!/data/local/tmp/libGameVMP.so.dump.so.fix.so: 1 file pulled, 0 skipped. 18.6 MB/s (463793 bytes in 0.024s)libGameVMP.so_0x7bd7b81000_462848_fix.so

可以看到本地多个一个 _fix 后缀的 so 文件,这个就是 脱壳并修复好的 so。

图片

使用 ida 打开 so 可以看到能正常打开,而且多了很多函数,代码块都能正常识别。

图片

除了用来脱壳 so ,也可以用 dump_so.js 中的函数查找 so 或打印所有 so 信息。 执行 dump_so.js 脚本

frida -H 127.0.0.1:1234 -F -l dump_so.js

输出如下:

[Remote::cyrus]-> rpc.exports.findmodule("libGameVMP.so"){    "base": "0x7b6ae0e000",    "name": "libGameVMP.so",    "path": "/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so",    "size": 462848}[Remote::cyrus]-> rpc.exports.allmodule()[    {        "base": "0x6545887000",        "name": "app_process64",        "path": "/system/bin/app_process64",        "size": 40960    },    {        "base": "0x7c69419000",        "name": "linker64",        "path": "/system/bin/linker64",        "size": 225280    },    ...]

1、使用 Frida 连接目标 Android 进程,加载 dump_so.js 脚本。

def read_frida_js_source():    with open("dump_so.js", "r") as f:        return f.read()def on_message(message, data):    passif __name__ == "__main__":    # device: frida.core.Device = frida.get_usb_device()    device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")    pid = device.get_frontmost_application().pid    session: frida.core.Session = device.attach(pid)    script = session.create_script(read_frida_js_source())    script.on('message', on_message)    script.load()

2、在 dump_so.js 的 dumpmodule 中获取目标 .so 文件的基地址和大小,返回内存中的 so 数据

rpc.exports = {    findmodule: function(so_name) {        var libso = Process.findModuleByName(so_name);        return libso;    },    dumpmodule: function(so_name) {        // 根据 so_name 查找已加载的模块(共享库)        var libso = Process.findModuleByName(so_name);        // 如果没找到对应模块,返回 -1 表示失败        if (libso == null) {            return -1;        }        // 修改模块内存权限为 可读(r)、可写(w)、可执行(x)        // 这样后面才能安全地读取和修改该内存区域        Memory.protect(ptr(libso.base), libso.size, 'rwx');        // 从模块基址开始,读取整个模块大小的字节数组        var libso_buffer = ptr(libso.base).readByteArray(libso.size);        // 把读取到的字节数组缓存到 libso 对象的 buffer 属性,方便后续使用        libso.buffer = libso_buffer;        // 返回读取到的字节数组        return libso_buffer;            },    allmodule: function() {        return Process.enumerateModules()    },    arch: function() {        return Process.arch;    }}

3、从内存中转储目标 .so 文件,保存为 .dump.so。

module_buffer = script.exports.dumpmodule(origin_so_name)if module_buffer != -1:    dump_so_name = origin_so_name + ".dump.so"    print(dump_so_name)    with open(dump_so_name, "wb") as f:        f.write(module_buffer)        f.close()

4、使用 SoFixer 工具修复转储的内存数据,重建 ELF 文件结构,使 IDA 可以正常识别。 5、下载修复后的 .so 文件到本地,清理设备上的临时文件。

def fix_so(arch, origin_so_name, so_name, base, size):    if arch == "arm":        os.system("adb push android/SoFixer32 /data/local/tmp/SoFixer")    elif arch == "arm64":        os.system("adb push android/SoFixer64 /data/local/tmp/SoFixer")    os.system("adb shell chmod +x /data/local/tmp/SoFixer")    os.system("adb push " + so_name + " /data/local/tmp/" + so_name)    print("adb shell /data/local/tmp/SoFixer -m " + base + " -s /data/local/tmp/" + so_name + " -o /data/local/tmp/" + so_name + ".fix.so")    os.system("adb shell /data/local/tmp/SoFixer -m " + base + " -s /data/local/tmp/" + so_name + " -o /data/local/tmp/" + so_name + ".fix.so")    os.system("adb pull /data/local/tmp/" + so_name + ".fix.so " + origin_so_name + "_" + base + "_" + str(size) + "_fix.so")    os.system("adb shell rm /data/local/tmp/" + so_name)    os.system("adb shell rm /data/local/tmp/" + so_name + ".fix.so")    os.system("adb shell rm /data/local/tmp/SoFixer")    return origin_so_name + "_" + base + "_" + str(size) + "_fix.so"

dump 下来的 .so 是执行视图(段为主),而 IDA 需要的是链接视图(节为主),SoFixer 就是桥梁,用来还原链接视图结构。

图片

Frida 在 Android 上枚举模块(如 Process.enumerateModules())时,核心机制是: 遍历 linker(动态链接器)内部维护的 soinfo 链表,dlopen 成功后,linker 会将 .so 加入 solist。 frida-gum 是 Frida 内部用来实现这些功能的核心组件,Frida-Gum 是 Frida 的底层动态插桩引擎,提供跨平台的 C/C++ 接口。

frida 在 android 下 Process.enumerateModules() 的调用链大概如下:

gum_android_enumerate_modules  └── 枚举 Android 中已加载模块的统一入口,对外暴露 API。  └── gum_enumerate_soinfo        ├── gum_linker_api_get        │     └── 获取 linker API(dlopen、solist 等)的单例结构。        │        │     └── gum_linker_api_try_init        │           └── 初始化 linker API,识别 linker 结构,并提取关键符号地址。        │        │           └── gum_android_get_linker_module        │                 └── 获取 linker 自身的 GumModule 实例(包含 ELF 基址等信息)。        │        │                 └── gum_try_init_linker_module   ← maps查找linker        │                       └── 遍历 /proc/self/maps,查找 `/linker` 或 `/linker64` 映射段,        │                           构造用于后续符号查找的 `GumModule`。        │        └── for (si = api->solist_get_head (); carry_on && si != NULL; si = next)              └── 遍历 linker 内部维护的 `soinfo` 链表,代表所有已加载模块(包括 `dlopen` 的模块)。              └── gum_emit_module_from_soinfo                    └── 将每个 `soinfo` 对象转换为 `GumModule` 结构,提取模块名、基址、路径、大小等信息。                    └── 回调用户传入的 func(GumModule*),最终将模块信息传给调用方

所以 frida 是通过 solist 找到内存中的 so 信息的。

脱壳的关键:定位解密后的 .so 在内存中的地址和大小,dump 出来再修复结构即可。 solist 是 linker 中的静态变量,把 linker64 拉取到本地:

 adb pull  /apex/com.android.runtime/bin/linker64

可以看到 solist 位于 .bss 段,其真实符号是 __dl__ZL6solist

图片

solist 在 android 源码中定义如下:

static soinfo* solist;

在 Android linker 源码中,soinfo 是一个结构体,用来记录每个已加载 .so 模块的各种信息:

struct soinfo {  const char* name;  // 共享库的文件名(通常是 .so 文件的路径或名称)  Elf_Addr    base;  // 共享库加载到内存的基地址  size_t      size;  // 共享库在内存中的大小(以字节为单位)  ...               soinfo*     next;  // 指向链表中下一个已加载共享库的 soinfo 结构体的指针};

在 Android 的动态链接器(linker)中,soinfo 结构体的 next 字段用于构建一个单向链表,指向下一个已加载的共享库(.so 文件)。通过全局的 solist(共享库列表的头节点),可以遍历所有已加载的共享库。 因此,通过 solist 可以轻松找到所有已加载的库,再通过 soinfo 的 base 和 size 把 so 从内存 dump


文章来源: https://www.freebuf.com/articles/web/446965.html
如有侵权请联系:admin#unsafe.sh