一文搞懂 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 到本地。

相关推荐
BoomHe25 分钟前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农8 小时前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少8 小时前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker8 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋9 小时前
Android 协程时代,Handler 应该退休了吗?
android
用户962377954481 天前
DVWA 靶场实验报告 (High Level)
安全
火柴就是我1 天前
让我们实现一个更好看的内部阴影按钮
android·flutter
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954481 天前
DVWA 靶场实验报告 (Medium Level)
安全