libjiagu.so 内存 Dump(libjiagu_vip_64.so)
背景
360加固商业版(libjiagu_vip_64.so)会在运行时自解密代码段,磁盘上的 SO 文件是加密的,IDA 无法直接分析。文章记录如何通过 Frida 从内存中 dump 出解密后的 SO,并在 IDA 中正确加载分析。
环境
- Frida 15.2.2(魔改去特征版)
- 目标 App:com.lucky.luckyclient(jiagu 加固)
- 版本: 5.5.40
- 设备:pixel 3
第一步:确认加固类型
用最简单的 dlopen 监控脚本,观察加载了哪些 SO:
1 2 3 4 5 6 7 8 9 10 11 12
| function hook_dlopen() { Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { onEnter: function(args) { this.fileName = args[0].readCString(); console.log("dlopen: " + this.fileName); }, onLeave: function(retval) { console.log("loaded: " + this.fileName + "\n"); } }); } setImmediate(hook_dlopen);
|
libjgdtc.so 加载后程序就闪退了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| (frida15) λ frida -H 127.0.0.1:26666 -f com.lucky.luckyclient -l dump_jiagu_vip_so.js --no-pause ____ / _ | Frida 15.2.2 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to 127.0.0.1:26666 (id=socket@127.0.0.1:26666) Spawned `com.lucky.luckyclient`. Resuming main thread! [Remote::com.lucky.luckyclient ]-> dlopen: /system/framework/oat/arm64/org.apache.http.legacy.odex loaded: /system/framework/oat/arm64/org.apache.http.legacy.odex
dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/oat/arm64/base.odex loaded: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/oat/arm64/base.odex
dlopen: /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so loaded: /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so
dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/lib/arm64/libjgdtc.so loaded: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/lib/arm64/libjgdtc.so
Process crashed: Bad access due to invalid address
|
虽然崩溃发生在 libjgdtc.so 加载之后,但通过下一步的异常捕获 setExceptionHandler 定位可以发现,崩溃的 lr(返回地址)指向的是 libjiagu_vip_64.so,说明实际执行崩溃代码的是 libjiagu_vip_64.so,libjgdtc.so 只是触发了加固核心的检测流程。所以我们需要 dump 的目标是 libjiagu_vip_64.so。
jiagu 加固的特征 SO:
/data/data/<pkg>/.jiagu/libjiagu_vip_64.so — 加固核心(运行时释放,磁盘上加密)
libjgdtc.so — 安全检测库
第二步:定位崩溃点
如果 Frida 注入后 app 崩溃,用 Process.setExceptionHandler 捕获异常,获取精确的崩溃地址和调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Process.setExceptionHandler(function(details) { console.log("=== Exception: " + details.type + " ==="); console.log("address: " + details.address); console.log("pc: " + details.context.pc); console.log("lr: " + details.context.lr);
var lrModule = Process.findModuleByAddress(details.context.lr); if (lrModule) { console.log("lr module: " + lrModule.name); console.log("lr offset: " + details.context.lr.sub(lrModule.base)); }
console.log("backtrace:\n" + Thread.backtrace(details.context, Backtracer.FUZZY) .map(function(addr) { var m = Process.findModuleByAddress(addr); if (m) return addr + " " + m.name + "!0x" + addr.sub(m.base).toString(16); return addr + " (unknown)"; }).join("\n"));
return false; });
|
再次运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| (frida15) λ frida -H 127.0.0.1:26666 -f com.lucky.luckyclient -l dump_jiagu_vip_so.js --no-pause ____ / _ | Frida 15.2.2 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https: . . . . . . . . Connected to 127.0.0.1:26666 (id=socket@127.0.0.1:26666) Spawned `com.lucky.luckyclient`. Resuming main thread! [Remote::com.lucky.luckyclient ]-> dlopen: /system/framework/oat/arm64/org.apache.http.legacy.odex loaded: /system/framework/oat/arm64/org.apache.http.legacy.odex
dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/oat/arm64/base.odex loaded: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/oat/arm64/base.odex
dlopen: /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so loaded: /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so
dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/lib/arm64/libjgdtc.so loaded: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/lib/arm64/libjgdtc.so
=== Exception: access-violation === address: 0x0 pc: 0x0 lr: 0x709828e96c lr module: libjiagu_vip_64.so lr offset: 0x25696c backtrace: 0x709828e96c libjiagu_vip_64.so!0x25696c 0x709828f37c libjiagu_vip_64.so!0x25737c 0x70981bd9fc libjiagu_vip_64.so!0x1859fc 0x712aa22248 libart.so!0x222248 0x712aa1160c libart.so!0x21160c 0x725a7d70 boot-framework.oat!0x1e4d70 0x728f041c boot-framework.oat!0x52d41c 0x725b0940 boot-framework.oat!0x1ed940 0x7268a030 boot-framework.oat!0x2c7030 0x7267f5fc boot-framework.oat!0x2bc5fc 0x70e0cbc4 boot.oat!0xc9bc4 0x728b3180 boot-framework.oat!0x4f0180 0x728b5fb0 boot-framework.oat!0x4f2fb0 0x728b5b08 boot-framework.oat!0x4f2b08 0x728d51e0 boot-framework.oat!0x5121e0 0x726920d0 boot-framework.oat!0x2cf0d0 Process crashed: Bad access due to invalid address
|
本例中崩溃信息:
1 2
| address: 0x0 ← 跳转到空指针 lr offset: 0x25696c ← 在 libjiagu_vip_64.so 中的偏移
|
第三步:从磁盘 pull SO
1
| adb pull /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so .
|
用 IDA 打开跳到偏移地址后,会发现代码段全是 % 1(加密数据),无法分析。这是正常的,加固的 SO 在磁盘上是加密的。
![image]()
第四步:内存 Dump(关键步骤)
Dump 脚本
jiagu 加固的 SO 有部分内存页是 execute-only(不可读),直接 readByteArray 会报 access violation。解决方案:逐页读取,不可读的页先用 Memory.protect 改权限再读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function dump_so_from_memory(module) { console.log("dumping " + module.name + " base=" + module.base + " size=" + module.size); var PAGE_SIZE = 0x1000; var totalSize = module.size; var base = module.base; var buffer = new ArrayBuffer(totalSize); var view = new Uint8Array(buffer); var fixed = 0;
for (var offset = 0; offset < totalSize; offset += PAGE_SIZE) { var readSize = Math.min(PAGE_SIZE, totalSize - offset); var pageAddr = base.add(offset); try { var page = pageAddr.readByteArray(readSize); var pageView = new Uint8Array(page); for (var j = 0; j < pageView.length; j++) { view[offset + j] = pageView[j]; } } catch(e) { try { Memory.protect(pageAddr, readSize, 'rwx'); var page = pageAddr.readByteArray(readSize); var pageView = new Uint8Array(page); for (var j = 0; j < pageView.length; j++) { view[offset + j] = pageView[j]; } fixed++; } catch(e2) { } } } console.log("fixed " + fixed + " pages with Memory.protect"); send("dumping", buffer); console.log("dump sent to python"); }
|
Dump 时机
关键:必须在 SO 自解密完成后再 dump。
jiagu 加固的解密时序:
libjiagu_vip_64.so 加载 → 此时代码段还未完全解密
libjgdtc.so 加载 → 此时 jiagu 已完全解密
所以要在 libjgdtc.so 的 onLeave 中触发 dump,如果在 libjiagu_vip_64.so 的 onLeave 就 dump,代码段可能还是全 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { onEnter: function(args) { this.fileName = args[0].readCString(); }, onLeave: function(retval) { if (this.fileName && this.fileName.indexOf("libjgdtc.so") >= 0) { var modules = Process.enumerateModules(); for (var i = 0; i < modules.length; i++) { if (modules[i].name.indexOf("libjiagu_vip_64") >= 0) { dump_so_from_memory(modules[i]); break; } } } } });
|
Python 接收脚本
由于手机文件系统可能没有写权限,通过 Frida 的 send() + Python 脚本将数据传回电脑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import frida import sys
def on_message(message, data): if message['type'] == 'send': if data: with open("libjiagu_vip_64_dump.so", "wb") as f: f.write(data) print(f"[*] dumped {len(data)} bytes to libjiagu_vip_64_dump.so") else: print(f"[send] {message['payload']}") elif message['type'] == 'log': print(f"[log] {message['payload']}") else: print(f"[{message['type']}] {message}")
device = frida.get_device_manager().add_remote_device("127.0.0.1:26666") pid = device.spawn(["com.lucky.luckyclient"]) session = device.attach(pid)
with open("dump_jiagu_vip_so.js", "r", encoding="utf-8") as f: script = session.create_script(f.read())
script.on('message', on_message) script.load() device.resume(pid) sys.stdin.read()
|
执行后输出:
1 2 3 4 5 6 7 8 9 10
| (frida15) λ python dump.py dlopen: /system/framework/oat/arm64/org.apache.http.legacy.odex dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/oat/arm64/base.odex dlopen: /data/data/com.lucky.luckyclient/.jiagu/libjiagu_vip_64.so dlopen: /data/app/~~6Vv5vvy2YluInaYi8sWXzQ==/com.lucky.luckyclient-_g8ZZzNrB3vdy9F9hQOAjA==/lib/arm64/libjgdtc.so dumping libjiagu_vip_64.so base=0x7099c1d000 size=3776512 fixed 44 pages with Memory.protect [*] dumped 3776512 bytes to libjiagu_vip_64_dump.so dump sent to python === Exception: access-violation ===
|
IDA 加载 Dump 文件
- IDA 打开
libjiagu_vip_64_dump.so
- Processor type 选择 ARM Little-endian [ARM]
![image]()
- 勾选 64-bit mode (AArch64)
- Loading offset 填
0
注意:不要选 x86!如果看到 int 3、loop、xchg 等指令,说明选错了架构。
分析代码
- 按
G 跳转到目标偏移(如 0x25696c)
- 如果显示为数据(
dq),按 C 强制转为代码
- 按
P 在函数入口创建函数
- 按
F5 反编译查看伪代码
如果跳转到目标地址能看到正常的 ARM64 指令(如 MOV、BL、STR、LDR),说明 dump 成功:
![image]()
1 2 3
| ROM:0000000000256964 LDR X8, [X8] ROM:0000000000256968 BLR X8 ← 崩溃点:X8 为 null ROM:000000000025696C MOV W2, #2 ← lr 指向这里
|