使用 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