一文搞懂 SO 脱壳全流程:识别加壳、Frida Dump、原理深入解析

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

加壳 so 识别

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

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

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

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

使用 frida_dump 实现 so 脱壳

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

先把 frida_dump 源码 clone 到本地。

如果使用的是远程链接,把 dump_so.py 中的

ini 复制代码
device: frida.core.Device = frida.get_usb_device()

改成

ini 复制代码
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

输出如下:

csharp 复制代码
(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.so
android/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 或打印所有 so

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

执行 dump_so.js 脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l dump_so.js

输出如下:

rust 复制代码
[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
    },
    ...
]

so 脱壳流程

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

scss 复制代码
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.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 数据

javascript 复制代码
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 文件,保存为 <name>.dump.so。

scss 复制代码
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 文件到本地,清理设备上的临时文件。

scss 复制代码
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"

为什么要用 SoFixer 进行修复

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

SoFixer 开源地址:github.com/F8LEFT/SoFi...

定位内存中的 .so

Frida 在 Android 上枚举模块(如 Process.enumerateModules())时,核心机制是:👉 遍历 linker(动态链接器)内部维护的 soinfo 链表,dlopen 成功后,linker 会将 .so 加入 solist。

frida-gum 是 Frida 内部用来实现这些功能的核心组件,Frida-Gum 是 Frida 的底层动态插桩引擎,提供跨平台的 C/C++ 接口。

开源地址:github.com/frida/frida...

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

ruby 复制代码
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*),最终将模块信息传给调用方

github.com/frida/frida...

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

脱壳点:solist 与 soinfo

脱壳的关键:定位解密后的 .so 在内存中的地址和大小,dump 出来再修复结构即可。

solist 是 linker 中的静态变量,把 linker64 拉取到本地:

bash 复制代码
 adb pull  /apex/com.android.runtime/bin/linker64

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

solist 在 android 源码中定义如下:

arduino 复制代码
static soinfo* solist;

cs.android.com/android/pla...

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

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

cs.android.com/android/pla...

在 Android 的动态链接器(linker)中,soinfo 结构体的 next 字段用于构建一个单向链表,指向下一个已加载的共享库(.so 文件)。通过全局的 solist(共享库列表的头节点),可以遍历所有已加载的共享库。

因此,通过 solist 可以轻松找到所有已加载的库,再通过 soinfo 的 base 和 size 把 so 从内存 dump 到本地。

相关推荐
工程师老罗2 小时前
如何在Android工程中配置NDK版本
android
Hello.Reader3 小时前
Flink ZooKeeper HA 实战原理、必配项、Kerberos、安全与稳定性调优
安全·zookeeper·flink
智驱力人工智能4 小时前
小区高空抛物AI实时预警方案 筑牢社区头顶安全的实践 高空抛物检测 高空抛物监控安装教程 高空抛物误报率优化方案 高空抛物监控案例分享
人工智能·深度学习·opencv·算法·安全·yolo·边缘计算
数据与后端架构提升之路4 小时前
论系统安全架构设计及其应用(基于AI大模型项目)
人工智能·安全·系统安全
Libraeking5 小时前
破壁行动:在旧项目中丝滑嵌入 Compose(混合开发实战)
android·经验分享·android jetpack
市场部需要一个软件开发岗位6 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
lingggggaaaa6 小时前
安全工具篇&动态绕过&DumpLsass凭据&Certutil下载&变异替换&打乱源头特征
学习·安全·web安全·免杀对抗
凯子坚持 c6 小时前
CANN-LLM:基于昇腾 CANN 的高性能、全功能 LLM 推理引擎
人工智能·安全
QT.qtqtqtqtqt7 小时前
未授权访问漏洞
网络·安全·web安全
JMchen1237 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio