一文搞懂 SO 脱壳全流程:识别加壳、Frida Dump、原理深入解析
字数 1327 2025-09-04 23:22:12
SO 脱壳全流程详解:识别加壳、Frida Dump 与原理解析
一、SO 加壳识别方法
1.1 IDA 识别特征
- ELF 结构异常:IDA 打开 so 文件时提示无法正确识别 ELF 文件结构
- Section 定义无效:section 定义不符合预期格式
- 红色代码块:存在大量红色汇编代码块,表示错误或未能正常解析的地址/数据
- 其他特征:代码被"混淆"、"裁剪"或"加壳"的迹象
二、Frida Dump 脱壳实战
2.1 准备工作
- 克隆 frida_dump 工具到本地
- 修改远程连接配置(如需):
# 原配置 device: frida.core.Device = frida.get_usb_device() # 远程连接配置 device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
2.2 脱壳执行流程
- 执行脱壳命令:
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.so
2.3 SoFixer 修复流程
- 推送 SoFixer 到设备:
adb push android/SoFixer64 /data/local/tmp/SoFixer - 执行修复:
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 [RebuildPhdr:37]RebuildPhdr End [ReadSoInfo:696]soname [ReadSoInfo:621] constructors (DT_INIT) found at 1bd68 [ReadSoInfo:629] constructors (DT_INIT_ARRAY) found at 6e9e8 [ReadSoInfo:703]ReadSoInfo End [RebuildShdr:536]RebuildShdr End [RebuildRelocs:809]RebuildRelocs End [RebuildFin:733]End [main:123]Done!!!
三、SO 查找与信息获取
3.1 查找单个 SO
rpc.exports.findmodule("libGameVMP.so")
// 返回示例
{
"base": "0x7b6ae0e000",
"name": "libGameVMP.so",
"path": "/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so",
"size": 462848
}
3.2 获取所有 SO 信息
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
}
]
四、Frida Dump 脱壳原理详解
4.1 整体流程
- 使用 Frida 连接目标 Android 进程
- 加载 dump_so.js 脚本
- 获取目标 .so 文件的基地址和大小
- 从内存中转储目标 .so 文件
- 使用 SoFixer 修复转储的内存数据
- 下载修复后的 .so 文件到本地
4.2 关键代码解析
4.2.1 Python 端代码
def read_frida_js_source():
with open("dump_so.js", "r") as f:
return f.read()
def on_message(message, data):
pass
if __name__ == "__main__":
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()
4.2.2 JavaScript 端代码
rpc.exports = {
findmodule: function(so_name) {
var libso = Process.findModuleByName(so_name);
return libso;
},
dumpmodule: function(so_name) {
var libso = Process.findModuleByName(so_name);
if (libso == null) {
return -1;
}
Memory.protect(ptr(libso.base), libso.size, 'rwx');
var libso_buffer = ptr(libso.base).readByteArray(libso.size);
libso.buffer = libso_buffer;
return libso_buffer;
},
allmodule: function() {
return Process.enumerateModules()
},
arch: function() {
return Process.arch;
}
}
4.2.3 修复函数
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)
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"
五、SoFixer 修复原理
5.1 为什么需要修复
- dump 下来的 .so 是执行视图(段为主)
- IDA 需要的是链接视图(节为主)
- SoFixer 作为桥梁,用于还原链接视图结构
5.2 修复过程
- 重建 ELF 文件头
- 修复动态段(dynamic segment)
- 重建程序头表(Phdr)
- 重建节头表(Shdr)
- 修复重定位表(Relocs)
六、SO 定位原理
6.1 Frida 枚举模块机制
- 遍历 linker 内部维护的 soinfo 链表
- dlopen 成功后,linker 会将 .so 加入 solist
- frida-gum 是 Frida 实现这些功能的核心组件
6.2 调用链
gum_android_enumerate_modules
→ gum_enumerate_soinfo
→ gum_linker_api_get
→ gum_linker_api_try_init
→ gum_android_get_linker_module
→ gum_try_init_linker_module
→ 遍历 /proc/self/maps 查找 linker
→ for (si = api->solist_get_head (); carry_on && si != NULL; si = next)
→ gum_emit_module_from_soinfo
6.3 soinfo 结构
struct soinfo {
const char* name; // 共享库的文件名
Elf_Addr base; // 共享库加载到内存的基地址
size_t size; // 共享库在内存中的大小
soinfo* next; // 指向链表中下一个已加载共享库的指针
};
6.4 solist 定位
- solist 是 linker 中的静态变量
- 真实符号:
__dl__ZL6solist - 位于 linker64 的 .bss 段
- 可通过
adb pull /apex/com.android.runtime/bin/linker64获取
七、脱壳关键点
- 定位解密后的 .so:通过 solist 和 soinfo 结构找到内存中的 so 信息
- 内存转储:获取正确的基地址和大小后 dump 内存数据
- 结构修复:使用 SoFixer 将执行视图转换为链接视图
- 验证:使用 IDA 打开修复后的 so,确认能正常解析代码
八、总结
SO 脱壳的核心流程可以概括为:
- 识别加壳特征
- 使用 Frida 定位内存中的 so 信息
- 转储内存中的 so 数据
- 修复 ELF 结构
- 验证脱壳结果
理解 linker 的 solist 和 soinfo 机制是成功脱壳的关键,而 SoFixer 则是将内存转储数据转换为可分析格式的重要工具。