libjiagu.so 内存 Dump(libjiagu_vip_64.so)
zsk Lv4

背景

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.solibjgdtc.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://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

=== 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) {
// 填 0,跳过
}
}
}
console.log("fixed " + fixed + " pages with Memory.protect");
send("dumping", buffer);
console.log("dump sent to python");
}

Dump 时机

关键:必须在 SO 自解密完成后再 dump。

jiagu 加固的解密时序:

  1. libjiagu_vip_64.so 加载 → 此时代码段还未完全解密
  2. libjgdtc.so 加载 → 此时 jiagu 已完全解密

所以要在 libjgdtc.soonLeave 中触发 dump,如果在 libjiagu_vip_64.soonLeave 就 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) {
// libjgdtc.so 加载完 = jiagu 已解密
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 文件

  1. IDA 打开 libjiagu_vip_64_dump.so
  2. Processor type 选择 ARM Little-endian [ARM]
    image
  3. 勾选 64-bit mode (AArch64)
  4. Loading offset 填 0

注意:不要选 x86!如果看到 int 3loopxchg 等指令,说明选错了架构。

分析代码

  • G 跳转到目标偏移(如 0x25696c
  • 如果显示为数据(dq),按 C 强制转为代码
  • P 在函数入口创建函数
  • F5 反编译查看伪代码

如果跳转到目标地址能看到正常的 ARM64 指令(如 MOVBLSTRLDR),说明 dump 成功:
image

1
2
3
ROM:0000000000256964  LDR   X8, [X8]
ROM:0000000000256968 BLR X8 ← 崩溃点:X8 为 null
ROM:000000000025696C MOV W2, #2 ← lr 指向这里
 评论