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

相关推荐
用户20187928316726 分钟前
Android 虚拟机的奇妙工厂之旅:从 Dalvik 到 ART 的技术童话
android
用户20187928316731 分钟前
Android 安全机制:应用沙箱与攻防技术的 "快递战"
android
hashiqimiya33 分钟前
android studio底部导航栏
android·ide·android studio
用户2018792831671 小时前
Java 线程池:工厂流水线的高效工人管理
android
炎码工坊1 小时前
微服务通信安全:OAuth2 从入门到实践
安全·网络安全·微服务·云原生·系统安全
安科瑞刘鸿鹏1 小时前
双碳时代,能源调度的难题正从“发电侧”转向“企业侧”
大数据·运维·物联网·安全·能源
用户2018792831671 小时前
Android 耗电统计:城市能源管理的 "电力侦探" 故事
android
ajassi20001 小时前
开源 java android app 开发(十二)封库.aar
android·java·linux·开源
恋猫de小郭1 小时前
Flutter 小技巧之:实现 iOS 26 的 “液态玻璃”
android·前端·flutter
帅次2 小时前
Flutter Container 组件详解
android·flutter·ios·小程序·kotlin·iphone·xcode