-
-
[原创]梆梆Frida过检测202607
-
发表于: 1小时前 58
-
前言
之前写了一版LibDexHelper.so的反调试分析,但是其实当时我还一直没有绕过反调试(刚入门当时只是想看看现在的anti frida是怎么写的...,然后有兄弟直接私信问我了,那我不得不研究一下了),说实话,当时网上找到的一些资料绕过方法我一看发现都是绕过老版本的LibDexHelper,所有方法在我这个版本的梆梆加密壳上面均无用(例如我看到找exit group的,也看到用通杀绕过脚本的,但是实际上在我这里并未奏效,可能由于这个版本比较新。)
既然如此,那就让我自己来研究研究怎么绕过新版的梆梆加密吧。本文也被发到我的公众号:点击跳转。
TL;DR
针对梆梆加密(LibDexHelper.so):
- 卡点不是 dlopen 入口,而是 dlopen 返回时。
init_array执行完后才会解密出真正的 payload 并替换符号表,此时JNI_OnLoad才“出现”。 - 唯一的反调试自杀逻辑在
sub_11C64:它会根据参数计算出非法地址,然后执行MOV SP, X0; BR X12,直接让进程崩溃。把sub_11C64patch 成空函数即可让JNI_OnLoad完整走完。 - DEX 解密入口是
sub_F490。它通过反射调用art::DexFileLoader::OpenCommon把内存中的 DEX 加载进来;在入口处 hook 即可 dump 出 8 个解密后的 DEX。 - JNI_OnLoad 返回
0x10004,说明壳本身执行完毕;但之后仍会在 Java/framework 层出现access-violation @ 0x0式的 NPE 崩溃,进程最终被系统终结。 - 后续将基于 RegisterNatives 绑定表,对这些 native 函数逐一进行系统性分析。
正文
样本信息:
| 项目 | 值 | 说明 |
|---|---|---|
| package name | cn.samsclub.app | 应用包名 |
| versionCode | 659 | 内部递增版本号 |
| versionName | 5.0.144 | 对外版本号 |
| sdkVersion (minSdk) | 23 | 最低支持 Android 6 |
| BuildConfig.DEFAULT_PATCH_ID | 2026-05-26 15:44:11 | 热修复时间 |
前情提要:之前简单分析了LibDexHelper.so,写了一版解密和反调试分析,因此我们仔细读源代码可以知道以下三个要点,这也是我们快速切入这个壳的核心要点。
- libDexHelper.so 本身原本并不导出 JNI_OnLoad。壳的 ELF 导出表里一开始没有这个符号,所以你直接在 dlopen 时调用 Module.findExportByName("libDexHelper.so", "JNI_OnLoad") 会拿到 null,挂上去的 hook 自然无效,甚至可能因为地址非法直接崩溃。
- JNI_OnLoad 是在 init_array 执行完之后才“出现”的。 sub_4780 → sub_33F8 会解密内嵌的 payload,然后把 payload 的 .dynsym / .dynstr / .hash 复制到 stub 自己的运行时符号表区域(0x145000 / 0x147000 / 0x148000)。只有这个复制完成后,libDexHelper.so 才“看起来”导出了 JNI_OnLoad。
- 正确的卡点不是在 dlopen 进入时,而是在 dlopen 返回时。 dlopen 同步执行构造函数,返回时 init_array 已经跑完、符号表已经被替换,但 ART/linker 还没调用 JNI_OnLoad。这个窗口期才能成功 hook。
- 依旧先贴一下加载库的函数调用顺序,写Frida有个参考。
java.lang.System.loadLibrary libart.so!Runtime_void_Runtime_load0 linker64!__dl___loader_android_dlopen_ext //这里可以先判断当前加载的文件 linker64!call_constructors //这里so加载完成 lib.so!init_array //反调试反frida逻辑在这里被加载 libart.so!art::JavaVMExt::LoadNativeLibrary(...) lib.so!JNI_OnLoad
山穷水复
首先先贴一下用Frida启动梆梆加密过的APP是什么退出信息,这是个很重要的点,后面会提到。

由于之前已经分析过INIT函数,确定它只是一个解密Loader,因此我们可以直接Hook Dlopen执行完,并且打开Trace。
【代码省略】
得到的输出到下面这里就停止了,但是观察到崩溃并不是反调试导致的崩溃,否则应该会有StackTrace。
[EXEC] 0x77fd1c3240 cbz x8, #0x77fd1c3250 [EXEC] 0x77fd1c3250 str x8, [x27, #8] [EXEC] 0x77fd1c3254 str x27, [x9, #8] [EXEC] 0x77fd1c3258 mov x27, x25 [EXEC] 0x77fd1c325c mov x0, x22 [EXEC] 0x77fd1c3260 bl #0x77fd1f6180 [EXEC] libc.so + 0x6c558 sub x9, x10, #1 [EXEC] libc.so + 0x6c55c eor x9, x9, x10 [EXEC] libc.so + 0x6c560 ands x11, x11, x9 [EXEC] libc.so + 0x6c564 csel x6, x2, x6, ne [EXEC] libc.so + 0x6c568 csel x7, x11, x7, ne [EXEC] libc.so + 0x6c56c clz x5, x7 [EXEC] libc.so + 0x6c570 add x5, x5, #2 [EXEC] libc.so + 0x6c574 sub x0, x6, x5, lsr #1 [EXEC] libc.so + 0x6c578 cmp x7, #0 [EXEC] libc.so + 0x6c57c csel x0, x0, xzr, ne [EXEC] libc.so + 0x6c580 ret [EXEC] 0x77fd1c3244 ldr w10, [x8] [EXEC] 0x77fd1c3248 cmp w10, w26 [EXEC] 0x77fd1c324c b.lt #0x77fd1c3238 Process terminated Thank you for using Frida!
因此我们可以考虑延迟加Trace,考虑到Frida Trace的稳定性,我们可以写一个轻量级的延迟加载Frida脚本如下(节选部分:由于完整脚本较长,这里只贴关键部分,主要展示核心逻辑,其他辅助函数日志、异常处理、工具函数等已省略。)
'use strict';
const CFG = {
soName: 'libDexHelper.so',
mode: 'surgical',
logRealCrash: true,
surgicalStartOff: 0x33d10,
stalkerEvents: {
call: true,
ret: true,
exec: false,
block: false,
compile: false,
},
excludeLibc: true,
excludeArt: true,
excludeLinker: true,
trustThreshold: 1,
queueCapacity: 65536,
queueDrainInterval: 100,
payloadSize: 0x149000,
dumpPath: '/data/local/tmp/dexhelper_dump.bin',
eventSink: 'send',
stalkerLogPath: '/data/local/tmp/stalker_events.log',
eventBatchSize: 512,
};
// ...
function startStalker(tid, label) {
console.log(`[STALKER] start ${label} on tid=${tid} sink=${CFG.eventSink}`);
const buf = [];
const logFile = openStalkerLog();
function flushBatch() {
if (buf.length === 0) return;
const batchStr = buf.splice(0, buf.length).join('\n');
if (logFile) {
logFile.write(batchStr + '\n');
} else {
send({ type: 'stalker-batch', label: label }, batchStr);
}
}
Stalker.follow(tid, {
events: CFG.stalkerEvents,
onReceive(events) {
const parsed = parseEvents(events);
if (!parsed || !parsed.length) return;
for (let i = 0; i < parsed.length; i++) {
const ev = parsed[i];
let local = false;
if (ev.length >= 2) local = addrInPayload(ev[1]);
if (!local && ev.length >= 3) local = addrInPayload(ev[2]);
if (local || ev[0] === 'compile') {
buf.push(JSON.stringify(ev));
if (buf.length >= CFG.eventBatchSize) flushBatch();
}
}
},
onCallSummary(summary) {
const keys = Object.keys(summary);
const filtered = {};
for (let i = 0; i < keys.length; i++) {
if (addrInPayload(keys[i])) {
filtered[keys[i]] = summary[keys[i]];
}
}
send({ type: 'stalker-summary', label: label, summary: filtered });
},
});
return { buf: buf, file: logFile, flush: flushBatch };
}
function stopStalker(tid, handle) {
Stalker.flush();
if (handle) {
handle.flush && handle.flush();
if (handle.file) {
handle.file.flush && handle.file.flush();
handle.file.close();
console.log(`[STALKER] log closed`);
}
}
Stalker.unfollow(tid);
Stalker.garbageCollect();
console.log(`[STALKER] stopped on tid=${tid}`);
}
function installSurgicalHook() {
const start = gPayloadBase.add(CFG.surgicalStartOff);
if (!looksLikeCode(start)) {
console.log(`[!] surgical start off ${CFG.surgicalStartOff.toString(16)} does not look like code;`);
console.log(` will defer attach via JNI_OnLoad entry.`);
installDeferredSurgicalHook();
return;
}
console.log(`[*] surgical hook @ ${fmtAddr(start)}`);
Interceptor.attach(start, {
onEnter(args) {
const tid = Process.getCurrentThreadId();
console.log(`[SURGICAL] hit start point, tid=${tid}`);
this.tid = tid;
this.handle = startStalker(tid, `surgical-${CFG.surgicalStartOff.toString(16)}`);
},
onLeave(retval) {
if (CFG.surgicalStopAtReturn) {
console.log(`[SURGICAL] returning from target, retval=${retval}`);
stopStalker(this.tid, this.handle);
}
}
});
}
function installDeferredSurgicalHook() {
const entry = gJniOnLoad;
let done = false;
Interceptor.attach(entry, {
onEnter(args) {
if (done) return;
const tid = Process.getCurrentThreadId();
gJniThreadId = tid;
console.log(`[SURGICAL-DEFER] JNI_OnLoad entered, tid=${tid}`);
},
onLeave(retval) {
if (done) return;
const start = gPayloadBase.add(CFG.surgicalStartOff);
if (!looksLikeCode(start)) {
console.log(`[SURGICAL-DEFER] target still encrypted at ${fmtAddr(start)}, giving up`);
done = true;
return;
}
try {
Interceptor.attach(start, {
onEnter(args) {
const tid = Process.getCurrentThreadId();
console.log(`[SURGICAL] hit start point, tid=${tid}`);
this.tid = tid;
this.handle = startStalker(tid, `surgical-${CFG.surgicalStartOff.toString(16)}`);
},
onLeave(retval) {
if (CFG.surgicalStopAtReturn) {
console.log(`[SURGICAL] returning from target, retval=${retval}`);
stopStalker(this.tid, this.handle);
}
}
});
console.log(`[SURGICAL] attached at ${fmtAddr(start)}`);
} catch (e) {
console.log('[SURGICAL-DEFER] failed to attach:', e);
}
done = true;
}
});
}
function hook_dlopen() {
const linker = Process.findModuleByName('linker64') || Process.findModuleByName('linker');
if (!linker) {
console.log('[-] linker not found');
return;
}
let dlopen = linker.findExportByName('__dl___loader_android_dlopen_ext') ||
linker.findExportByName('__dl__android_dlopen_ext') ||
linker.findExportByName('android_dlopen_ext');
if (!dlopen || dlopen.isNull()) {
const syms = linker.enumerateSymbols();
for (let i = 0; i < syms.length; i++) {
const n = syms[i].name;
if (n.indexOf('android_dlopen_ext') !== -1 ||
n.indexOf('__dl___loader_android_dlopen_ext') !== -1) {
dlopen = syms[i].address;
console.log(`[*] found dlopen via symbols: ${n}`);
break;
}
}
}
if (!dlopen || dlopen.isNull()) {
console.log('[-] android_dlopen_ext not found');
return;
}
Interceptor.attach(dlopen, {
onEnter(args) {
const name = args[0] ? args[0].readCString() : null;
this.isTarget = name && name.indexOf(CFG.soName) !== -1;
if (this.isTarget) {
console.log(`\n[★ TARGET] android_dlopen_ext: ${name}`);
}
},
onLeave(retval) {
if (!this.isTarget) return;
gStubModule = Process.findModuleByName(CFG.soName);
if (!gStubModule) {
console.log('[-] libDexHelper.so not found after dlopen');
return;
}
console.log(`[+] stub base = ${gStubModule.base} size = ${gStubModule.size}`);
gJniOnLoad = findJniOnLoad();
if (!gJniOnLoad || gJniOnLoad.isNull()) {
console.log('[-] JNI_OnLoad not found');
return;
}
gPayloadBase = gJniOnLoad.sub(0x13a8c);
console.log(`[+] JNI_OnLoad = ${gJniOnLoad}`);
console.log(`[+] payloadBase = ${gPayloadBase} (size ~${CFG.payloadSize.toString(16)})`);
// 一致性验证:crashFn 等偏移应落在 payload 范围
const start = gPayloadBase.add(CFG.surgicalStartOff);
console.log(`[+] surgical start = ${fmtAddr(start)}`);
applyStalkerOptions();
installExceptionHandler();
installSurgicalHook();
}
});
}
setImmediate(hook_dlopen);
console.log('[*] jni_trace_v2.js loaded, mode=' + CFG.mode);用 surgical 模式启动 Stalker遇到崩溃,看崩溃信息应该是Frida自己崩溃的。
["call","0x77fd1f5acc","0x77fd1f9174",243] ["ret","0x7ac2d81960","0x77fd1f5ad0",244] ["call","0x77fd1f5b1c","0x77fd1f9174",243] ["ret","0x7ac2d81960","0x77fd1f5b20",244] ["call","0x77fd1f5b58","0x77fd1f9174",243] ["ret","0x7ac2d81960","0x77fd1f5b5c",244] ["call","0x77fd1f5ba4","0x77fd1f5564",243] ["call","0x77fd1f55a8","0x77fd1f9174",244] [SURGICAL] returning from target, retval=0x0 Process crashed: Bad access due to protection failure *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Xiaomi/pudding/pudding:16/BP2A.250605.031.A3/OS3.0.315.0.WPCCNXM:user/release-keys' Revision: '0' ABI: 'arm64' Timestamp: 2026-07-23 11:18:25.364232348+0800 Process uptime: 56s Cmdline: cn.samsclub.app pid: 15068, tid: 15068, name: cn.samsclub.app >>> cn.samsclub.app <<< uid: 10342 tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE) pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY) signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000077fce7d000 x0 00000077fcc7c010 x1 00000077fce7cfd0 x2 00000000ffdfeff0 x3 00000077fcc7cf90 x4 00000078fcc7c010 x5 00000078fca7c010 x6 0000000000000000 x7 000000000000000b x8 0000000000000000 x9 0000000000000000 x10 0000000000000000 x11 0000000000000000 x12 0000000000000000 x13 0000000000000000 x14 0000000000000000 x15 0000007ac40c0000 x16 0000000000000030 x17 0000007ac2d84f00 x18 0000007ae0428000 x19 0000007ac098d520 x20 0000000000010000 x21 0000000000000000 x22 0000000000200000 x23 ffffffffffffffff x24 00000077989fe010 x25 0000000000010000 x26 0000000000000000 x27 0000007807e87db0 x28 0000007ac40c0000 x29 0000007fe8b2a1d0 lr 000000778f06a6ec sp 0000007fe8b2a1d0 pc 0000007ac2d85198 pst 0000000020001000 15 total frames backtrace: #00 pc 0000000000099198 /apex/com.android.runtime/lib64/bionic/libc.so (__memcpy_aarch64_nt+664) (BuildId: 223a572c9f034c6a9337d8b3bedc75f3) #01 pc 0000000000afd6e8 /memfd:frida-agent-64.so (deleted) // ... #13 pc 00000000009acabc /memfd:frida-agent-64.so (deleted) #14 pc 000000000004f230 <anonymous:777b600000> *** Thank you for using Frida!
柳暗花明
通过 SURGICAL 的调试信息可以知道,我们知道 0x33d10 这个入口函数已经执行完了,surgical hook 本身完成了使命。那我们再跳过 0x33d10 这个入口 ,从0x1b940(post-33D10)启动 Stalker,并且开启打印汇编的Trace。
遇到崩溃信息如下,但是我们看这个BR x12,跳到了0x97C,这个是不是和0x6aC挺像的!
[EXEC] libDexHelper.sopayload + 0x120c8 mov x0, #0 [EXEC] libDexHelper.sopayload + 0x120cc mov sp, x0 [EXEC] libDexHelper.sopayload + 0x120d0 mov x30, x0 [EXEC] libDexHelper.sopayload + 0x120d4 mov x8, x19 [EXEC] libDexHelper.sopayload + 0x120d8 mov x9, x12 [EXEC] libDexHelper.sopayload + 0x120dc br x12 Process crashed: Bad access due to invalid address *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Xiaomi/pudding/pudding:16/BP2A.250605.031.A3/OS3.0.315.0.WPCCNXM:user/release-keys' Revision: '0' ABI: 'arm64' Timestamp: 2026-07-23 11:45:57.350911510+0800 Process uptime: 27s Cmdline: cn.samsclub.app pid: 21279, tid: 21279, name: cn.samsclub.app >>> cn.samsclub.app <<< uid: 10342 tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE) pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY) signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xffffffffffffff70 x0 0000000000000000 x1 00000000b6a2897f x2 0000000000000fff x3 0000007fe8b2b0f8 x4 00000000000010b0 x5 0000007fe8b2b2bc x6 000000006174732f x7 2f6b7361742f666c x8 000000000000001f x9 0000000000294e10 x10 00000000b6a2897f x11 000000000000101e x12 000000000000097c x13 2f6b7361742f666c x14 0000000000000067 x15 0000000000000061 x16 0000000000000001 x17 0000007ac2d45570 x18 0000007ae0428000 x19 00000000b6a2897f x20 00000077fd26b222 x21 0000007adfcec780 x22 0000007fe8b2b1a0 x23 00000077fd28be19 x24 00000077fd26b221 x25 0000007fe8b2b1a0 x26 0000007fe8b2b449 x27 0000007adfcec780 x28 0000000000000000 x29 0000007fe8b2b140 lr 00000077fd1ccdd8 sp 0000000000000000 pc 00000077986358c8 pst 0000000000001000 38 total frames backtrace: #00 pc 00000000000358c8 <anonymous:7798600000> // ... #10 pc 000000000012a56c /system/framework/arm64/boot.oat (java.lang.System.loadLibrary+92) (BuildId: 0b9bde974037d8e8ccf8cfa4291ada7f8949bc24) Thank you for using Frida!
对应的函数是sub_11C64,我们看看这个BR X12的X12这个值是怎么来的,可以看到是W1&W2&0xFFFFFFFC得到的,那就好说了。
.text:0000000000011C64 ; __unwind {
.text:0000000000011C64 SUB SP, SP, #0x180
.text:0000000000011C68 STR X28, [SP,#0x170+var_30]
.text:0000000000011C6C STP X22, X21, [SP,#0x170+var_20]
.text:0000000000011C70 STP X20, X19, [SP,#0x170+var_10]
.text:0000000000011C74 STP X29, X30, [SP,#0x170+var_s0]
.text:0000000000011C78 ADD X29, SP, #0x170
.text:0000000000011C7C MRS X21, TPIDR_EL0
.text:0000000000011C80 LDR X8, [X21,#0x28]
.text:0000000000011C84 MOV W19, W1
.text:00000000000120B4
.text:00000000000120B4 loc_120B4 ; CODE XREF: sub_11C64+3C↑j
.text:00000000000120B4 MOV W10, W19
.text:00000000000120B8 AND W8, W2, W19
.text:00000000000120BC AND X12, X8, #0xFFFFFFFC
.text:00000000000120C0 AND X8, X10, #0x1F
.text:00000000000120C4 ADDS X11, X8, X2
.text:00000000000120C8 MOV X0, #0
.text:00000000000120CC MOV SP, X0
.text:00000000000120D0 MOV X30, X0
.text:00000000000120D4 MOV X8, X19
.text:00000000000120D8 MOV X9, X12
.text:00000000000120DC BR X12把所有调用sub_11C64的地方列出来,并且算出对应的第一个参数AND第二个参数XOR0xFFFFFFFC后的值
import idaapi
import ida_funcs
import ida_xref
import idautils
import idc
TARGET = "sub_11C64"
func = idaapi.get_name_ea(idaapi.BADADDR, TARGET)
if func == idaapi.BADADDR:
print("cannot find", TARGET)
raise Exception()
MASK = 0xFFFFFFFF
def parse_imm(op):
op = op.replace("#", "")
if op.startswith("0x") or op.startswith("-0x"):
return int(op, 16)
return int(op)
def trace_args(call_ea):
"""
回溯 x1/x2 常量
"""
regs = {
"x1": None,
"w1": None,
"x2": None,
"w2": None,
}
ea = idc.prev_head(call_ea)
limit = 80
while ea != idc.BADADDR and limit > 0:
limit -= 1
mnem = idc.print_insn_mnem(ea)
dst = idc.print_operand(ea, 0)
src = idc.print_operand(ea, 1)
if mnem == "MOV":
if dst in ("X1", "W1") and regs["x1"] is None:
if src.startswith("#"):
regs["x1"] = parse_imm(src)
elif src in ("W1","X1"):
pass
if dst in ("X2", "W2") and regs["x2"] is None:
if src.startswith("#"):
regs["x2"] = parse_imm(src)
elif mnem == "MOVZ":
shift = 0
if idc.print_operand(ea,2):
s = idc.print_operand(ea,2)
if "LSL" in s.upper():
shift = parse_imm(s.split()[-1])
if dst in ("X1","W1") and regs["x1"] is None:
regs["x1"] = parse_imm(src) << shift
if dst in ("X2","W2") and regs["x2"] is None:
regs["x2"] = parse_imm(src) << shift
elif mnem == "MOVK":
shift = 0
if idc.print_operand(ea,2):
s = idc.print_operand(ea,2)
if "LSL" in s.upper():
shift = parse_imm(s.split()[-1])
if dst in ("X1","W1") and regs["x1"] is not None:
imm = parse_imm(src)
mask = 0xffff << shift
regs["x1"] &= ~mask
regs["x1"] |= imm << shift
if dst in ("X2","W2") and regs["x2"] is not None:
imm = parse_imm(src)
mask = 0xffff << shift
regs["x2"] &= ~mask
regs["x2"] |= imm << shift
elif mnem == "ORR":
op2 = idc.print_operand(ea,1)
op3 = idc.print_operand(ea,2)
if op2.upper() in ("XZR","WZR") and op3.startswith("#"):
if dst in ("X1","W1") and regs["x1"] is None:
regs["x1"] = parse_imm(op3)
if dst in ("X2","W2") and regs["x2"] is None:
regs["x2"] = parse_imm(op3)
if regs["x1"] is not None and regs["x2"] is not None:
break
ea = idc.prev_head(ea)
return regs["x1"], regs["x2"]
print("="*80)
for xref in idautils.XrefsTo(func):
if xref.type != ida_xref.fl_CN:
continue
call = xref.frm
arg2, arg3 = trace_args(call)
if arg2 is None:
arg2_xor = "unknown"
else:
arg2_xor = hex((arg2 & arg3) & 0xFFFFFFFC)
print(
"CALL {:08X} arg2={} arg3={} res={}".format(
call,
"unknown" if arg2 is None else hex(arg2),
"unknown" if arg3 is None else hex(arg3),
arg2_xor
)
)通过上面的脚本我们可以得到下面的反调试函数和崩溃信息对照,不过现在来说作用不大了,因为我们可以直接Patch崩溃函数。
| 调用点 | res |
|---|---|
| 0x000158D4 | 0x8 |
| 0x000159EC | 0x8 |
| 0x00015D8C | 0x8 |
| 0x00015E98 | 0x8 |
| 0x00015F18 | 0x8 |
| 0x0001748C | 0x79C |
| 0x0001E80C | 0xB4C |
| 0x0001F880 | unknown |
| 0x000246CC | 0x0 |
| 0x0002E1B8 | unknown |
| 0x0002E33C | 0x79C |
| 0x0003DDD4 | 0x97C |
| 0x0003E748 | unknown |
| 0x00041764 | 0x1F4 |
| 0x000420B8 | 0x88C |
| 0x000424D0 | 0x4CC |
| 0x00042708 | 0x3D8 |
| 0x0004462C | 0x0 |
| 0x00046220 | 0x1F8 |
| 0x00049470 | 0x1F8 |
| 0x0004A118 | unknown |
| 0x0004B898 | 0x4CC |
| 0x0004C120 | 0x3D8 |
| 0x0004CE78 | 0x6AC |
| 0x0004E148 | 0x6AC |
| 0x0004F920 | unknown |
| 0x0004F974 | 0xC3C |
| 0x000501F0 | 0xD2C |
| 0x00050440 | 0xF1C |
| 0x000519B4 | 0xF1C |
| 0x00051E24 | 0x2EC |
| 0x00052F14 | 0x5BC |
| 0x00054C74 | 0xE1C |
| 0x000552F8 | 0xF1C |
| 0x000554E8 | 0xF1C |
| 0x0005A240 | 0x10 |
| 0x0005A61C | unknown |
| 0x0005B4CC | 0x4 |
| 0x0005B58C | 0x14 |
那干脆不Trace了?直接把Sub11C64() 函数Patch了看看能不能过?
frida -U -f cn.samsclub.app -l patch_sub_11c64.js ____ / _ | Frida 17.15.4 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . Prefer a GUI? Luma is the official Frida app, with a live REPL, . . . . persistent sessions & collaboration. 19bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9N6h3#2S2i4K6u0W2k6Y4u0A6k6r3q4Q4x3X3g2J5k6g2)9J5c8R3`.`. . . . . . . . . Connected to 25113PN0EC (id=75040c8d) Spawning `cn.samsclub.app`... [*] patch_sub_11c64.js loaded (patch-only, no trace) Spawned `cn.samsclub.app`. Resuming main thread! [25113PN0EC::cn.samsclub.app ]-> [★ TARGET DETECTED] android_dlopen_ext: /data/app/~~ixWf-uuOzn9c_gPbpVp35w==/cn.samsclub.app-6Q0Gn4oqvUfj1yT-4Z3tNQ==/lib/arm64/libDexHelper.so [+] stub base = 0x77fcc35000 size = 1347584 [+] found JNI_OnLoad via enumerateExports: 0x77fcb24a8c [+] payloadBase = 0x77fcb11000 [+] sub_11C64 = libDexHelper.sopayload + 0x11c64 [+] patched sub_11C64 @ libDexHelper.sopayload + 0x11c64 → return immediately (void) Process terminated [25113PN0EC::cn.samsclub.app ]-> Thank you for using Frida!
事情果然没这么简单,现在连崩溃的调试信息都没了。继续使用surgical 模式往后Trace。
-> libDexHelper.so!0x7419c\n[CALL] [D:65] libDexHelper.so!0x74234 -> libDexHelper.so!0x77990\n[CALL] [D:66] libDexHelper.so!0x779f8 -> libDexHelper.so!0x76e24\n[RET] [D:67] libDexHelper.so!0x76e58 -> libDexHelper.so!0x779fc\n[CALL] [D:66] libDexHelper.so!0x77a3c -> libDexHelper.so!0x775d4\n[CALL] [D:67] libDexHelper.so!0x77688 -> libDexHelper.so!0x777f4\n[CALL] [D:68] libDexHelper.so!0x7787c -> libDexHelper.so!0x6be28\n[CALL] [D:69] libDexHelper.so!0x6bbbc -> libDexHelper.so!0xe660\n[RET] [D:70] libc.so!0x9931c -> libDexHelper.so!0x6bbc0\n[RET] [D:69] libDexHelper.so!0x6be04 -> libDexHelper.so!0x77880\n[CALL] [D:68] libDexHelper.so!0x778b0 -> libDexHelper.so!0x7736c\n[RET] [D:69] libDexHelper.so!0x775d0 -> libDexHelper.so!0x778b4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x7768c\n[CALL] [D:67] libDexHelper.so!0x776f4 -> libDexHelper.so!0x777f4\n[CALL] [D:68] libDexHelper.so!0x7787c -> libDexHelper.so!0x6be28\n[CALL] [D:69] libDexHelper.so!0x6bbbc -> libDexHelper.so!0xe660\n[RET] [D:70] libc.so!0x9931c -> libDexHelper.so!0x6bbc0\n[RET] [D:69] libDexHelper.so!0x6be04 -> libDexHelper.so!0x77880\n[CALL] [D:68] libDexHelper.so!0x778b0 -> libDexHelper.so!0x7736c\n[RET] [D:69] libDexHelper.so!0x775d0 -> libDexHelper.so!0x778b4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x776f8\n[CALL] [D:67] libDexHelper.so!0x77760 -> libDexHelper.so!0x777f4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x77764\n[RET] [D:67] libDexHelper.so!0x777dc -> libDexHelper.so!0x77a40'}} data: None Process terminated Thank you for using Frida!
最后崩在这里,但是这个崩溃现场看起来又是Frida搞崩的。
Dump Dex&&Bypass anti-frida
但是这个崩溃的地方看起来有点怪啊,跟进去看看执行了什么代码。
if ( *(_QWORD *)&filename[8] != *(_QWORD *)filename )
{
v1045 = 0;
v1046 = v1037 + 8LL * (int)v1034;
do
{
v1047 = *(_QWORD *)(qword_104190 + 8 * v1045);
v1048 = *(_QWORD *)(v1047 + 8);
LODWORD(v1047) = *(_DWORD *)(v1047 + 24);
s[1].n128_u64[0] = 0;
s[0].n128_u64[0] = v1048;
s[0].n128_u32[2] = v1047;
sub_F258((__int64)s);
*(_QWORD *)(v1046 + 8 * v1045++) = s[1].n128_u64[0];
}
while ( v1045 < (__int64)(*(_QWORD *)&filename[8] - *(_QWORD *)filename) >> 3 );
}调用这个,我们再往下看。
bool __fastcall sub_F258(_QWORD *a1)
{
int v2; // w2
int v5; // w0
__int128 v6; // [xsp+0h] [xbp-30h] BYREF
const char *v7; // [xsp+10h] [xbp-20h]
__int64 v8; // [xsp+18h] [xbp-18h]
v8 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
if ( *(_DWORD *)qword_1063B0 <= *(_DWORD *)(qword_1063B0 + 20) )
v2 = *(_DWORD *)(qword_1063B0 + 20);
else
v2 = *(_DWORD *)qword_1063B0;
if ( (v2 | 1) == 0x1B )
{
return sub_F33C(a1);
}
else
{
if ( (unsigned int)(v2 - 28) <= 2 )
return sub_F490(a1);
v6 = *(_OWORD *)off_F7E40;
v7 = "_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10O"
"atDexFileEbbPS9_";
sub_F5F4((__int64)&v6, 3, v2); //
if ( v5 == 2 )
return sub_F964(a1);
if ( v5 == 1 )
return sub_F798(a1);
return !v5 && sub_F490(a1); // DUMP DEX
}
}下面是加载DEX文件的核心逻辑。
bool __fastcall sub_F490(_QWORD *a1)
{
__int64 v2; // x3
__int64 v3; // x4
__int64 v4; // x5
__int64 v5; // x6
__int64 v6; // x7
void *v7; // x20
__int64 v8; // x21
__int64 v9; // x22
__int64 v10; // x5
__int64 v11; // x8
_BOOL4 v12; // w19
_QWORD v14[4]; // [xsp+28h] [xbp-C8h] BYREF
_QWORD v15[2]; // [xsp+48h] [xbp-A8h] BYREF
void *ptr; // [xsp+58h] [xbp-98h]
unsigned __int64 v17[3]; // [xsp+60h] [xbp-90h] BYREF
_BYTE v18[64]; // [xsp+78h] [xbp-78h] BYREF
__int64 v19; // [xsp+B8h] [xbp-38h]
v19 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
v7 = dlsym(
0,
"_ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjP"
"KNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE");
if ( v7
|| (v7 = sub_25B54(
*(char **)(qword_1063B0 + 40),
"_ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIc"
"EEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE",
1,
v2,
v3,
v4,
v5,
v6)) != 0 )
{
v8 = *a1;
v9 = *(unsigned int *)(*a1 + 32LL);
sub_ED50(v17, "AnoymousDex");
v10 = *(unsigned int *)(v8 + 8);
v15[1] = 0;
ptr = 0;
v15[0] = 0;
((void (__fastcall *)(_QWORD *__return_ptr, __int64, __int64, _QWORD, _QWORD, unsigned __int64 *, __int64, _QWORD, _QWORD, _BYTE, _QWORD *, _BYTE *, _QWORD))v7)(
v14,
v8,
v9,
0,
0,
v17,
v10,
0,
0,
0,
v15,
v18,
0);
v11 = v14[0];
a1[2] = v14[0];
v12 = v11 != 0;
if ( (v15[0] & 1) != 0 )
j__free_0(ptr);
if ( (v17[0] & 1) != 0 )
j__free_0((void *)v17[2]);
}
else
{
return 0;
}
return v12;
},可以看到它使用OpenCommon来加载DEX文件,而我们之前又过掉反调试了,那我们可以轻松写出DUMP脚本。
'use strict';
// ============================================================
// libDexHelper.so DEX Dumper
//
// 只做四件事:
// 1. dlopen 返回时 hook
// 2. patch sub_11C64 为空函数 (阻断 0x120cc 自杀序列 MOV SP,X0/BR X12)
// 3. hook sub_F490 入口 dump dex
// 4. hook JNI_OnLoad 打 ENTER/LEAVE 日志
// ============================================================
const CFG = {
soName: 'libDexHelper.so',
jniOnLoadOff: 0x13a8c,
sub11C64Off: 0x11c64,
subF490Off: 0xf490,
payloadSize: 0x149000,
dumpDir: '/data/data/cn.samsclub.app/',
};
let gStubModule = null;
let gPayloadBase = null;
let gJniOnLoad = null;
let gSub11C64Callback = null; // 持有 NativeCallback 引用防 GC
let gDumpCount = 0;
function formatAddr(addr) {
addr = ptr(addr);
if (gPayloadBase &&
addr.compare(gPayloadBase) >= 0 &&
addr.compare(gPayloadBase.add(CFG.payloadSize)) < 0) {
return CFG.soName + 'payload + 0x' + addr.sub(gPayloadBase).toString(16);
}
const m = Process.findModuleByAddress(addr);
if (m) return m.name + ' + 0x' + addr.sub(m.base).toString(16);
return addr.toString();
}
function looksLikeCode(a) {
try {
const insn = Instruction.parse(ptr(a));
if (!insn) return false;
const ok = ['stp', 'sub', 'mov', 'add', 'adrp', 'ldr', 'str', 'b', 'bl', 'cbz', 'cmp', 'ret'];
return ok.indexOf(insn.mnemonic) !== -1;
} catch (e) {
return false;
}
}
function ensureDumpDir() {
const libc = Process.findModuleByName('libc.so');
if (!libc) return false;
let mkdirAddr = libc.findExportByName('mkdir') || libc.findExportByName('__mkdir');
if (!mkdirAddr || mkdirAddr.isNull()) return false;
const mkdir = new NativeFunction(mkdirAddr, 'int', ['pointer', 'int']);
const rc = mkdir(Memory.allocUtf8String(CFG.dumpDir), 0o777);
console.log('[*] mkdir rc=' + rc + ' (EEXIST 也返回 -1, 忽略)');
return true;
}
function readDexSignature(dexBase) {
try {
const sigBytes = dexBase.add(12).readByteArray(20);
if (!sigBytes) return 'null';
const u8 = new Uint8Array(sigBytes);
let hex = '';
for (let i = 0; i < u8.length; i++) hex += u8[i].toString(16).padStart(2, '0');
return hex;
} catch (e) {
return 'read_fail';
}
}
function readDexMagic(dexBase) {
try {
const magic = dexBase.readByteArray(8);
if (!magic) return 'null';
const u8 = new Uint8Array(magic);
let s = '';
for (let i = 0; i < u8.length; i++) {
const c = u8[i];
s += (c >= 0x20 && c <= 0x7e) ? String.fromCharCode(c) : '.';
}
return s;
} catch (e) {
return 'read_fail';
}
}
function findJniOnLoadManually() {
const mod = Process.findModuleByName(CFG.soName);
if (!mod) return null;
const base = mod.base;
const symtab = base.add(0x145000);
const strtab = base.add(0x147000);
const nchain = 221;
for (let i = 0; i < nchain; i++) {
const entry = symtab.add(i * 0x18);
const st_name = entry.readU32();
const name = strtab.add(st_name).readCString();
if (name === 'JNI_OnLoad') {
const addr = entry.add(8).readPointer();
console.log('[*] manual scan: JNI_OnLoad @ ' + addr);
return addr;
}
}
return null;
}
function findJniOnLoad() {
const mod = Process.findModuleByName(CFG.soName);
if (!mod) {
console.log('[-] libDexHelper.so not found in process');
return null;
}
try {
const exports = mod.enumerateExports();
for (let i = 0; i < exports.length; i++) {
if (exports[i].name === 'JNI_OnLoad') {
console.log('[+] found JNI_OnLoad via enumerateExports: ' + exports[i].address);
return exports[i].address;
}
}
} catch (e) {
console.log('[!] enumerateExports failed: ' + e);
}
console.log('[!] fallback to manual dynsym scan');
return findJniOnLoadManually();
}
// Patch sub_11C64: Interceptor.replace 成 void(void) 空函数
function patchSub11C64() {
const target = gPayloadBase.add(CFG.sub11C64Off);
if (!looksLikeCode(target)) {
console.log('[!] sub_11C64 @ ' + formatAddr(target) + ' not code yet, payload still encrypted?');
return false;
}
gSub11C64Callback = new NativeCallback(function () {
}, 'void', []);
try {
Interceptor.replace(target, gSub11C64Callback);
console.log('[+] patched sub_11C64 @ ' + formatAddr(target) + ' -> return immediately (void)');
return true;
} catch (e) {
console.log('[!] Interceptor.replace sub_11C64 failed: ' + e);
return false;
}
}
// Hook sub_F490 入口 dump dex
function hookAndDumpSubF490() {
const target = gPayloadBase.add(CFG.subF490Off);
if (!looksLikeCode(target)) {
console.log('[!] sub_F490 @ ' + formatAddr(target) + ' not code yet, payload still encrypted?');
return false;
}
Interceptor.attach(target, {
onEnter(args) {
this.a1 = args[0];
const tid = Process.getCurrentThreadId();
gDumpCount++;
let dexBase, dexSize;
try {
dexBase = this.a1.readPointer();
} catch (e) {
console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' *a1 read failed: ' + e);
return;
}
if (!dexBase || dexBase.isNull()) {
console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' *a1 = null');
return;
}
const magic = readDexMagic(dexBase);
try {
dexSize = dexBase.add(32).readU32();
} catch (e) {
console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' size read failed: ' + e);
return;
}
const sig = readDexSignature(dexBase);
const sigShort = sig.substring(0, 8);
console.log('\n[F490] #' + gDumpCount + ' tid=' + tid);
console.log(' *a1 (dexBase) = ' + dexBase);
if (magic.indexOf('dex') !== 0 || dexSize < 0x70 || dexSize > 0x10000000) {
console.log(' [!] sanity check failed, skip dump');
return;
}
let dexBytes;
try {
dexBytes = dexBase.readByteArray(dexSize);
} catch (e) {
console.log(' [!] readByteArray failed: ' + e);
return;
}
if (!dexBytes) {
console.log(' [!] readByteArray returned null');
return;
}
const fileName = CFG.dumpDir + '/dex_' + String(gDumpCount).padStart(2, '0') + '_' + sigShort + '_' + dexSize + '.dex';
try {
const f = new File(fileName, 'wb');
f.write(dexBytes);
f.flush();
f.close();
console.log(' [+] DUMPED -> ' + fileName + ' (' + dexBytes.byteLength + ' bytes)');
} catch (e) {
console.log(' [!] write file failed: ' + e);
}
},
onLeave(retval) {
}
});
console.log('[+] hooked sub_F490 @ ' + formatAddr(target) + ' (dump mode)');
return true;
}
// Hook JNI_OnLoad 本体 — ENTER/LEAVE 日志, 验证能否走完
function hookJniOnLoadLifecycle() {
try {
Interceptor.attach(gJniOnLoad, {
onEnter(args) {
console.log('\n[JNI_OnLoad] ENTER vm=' + args[0] + ' reserved=' + args[1]);
},
onLeave(retval) {
console.log('[JNI_OnLoad] LEAVE retval=0x' + retval.toInt32().toString(16));
console.log('[★] 最初版 (仅 patch sub_11C64) 就走完了整个 JNI_OnLoad');
}
});
console.log('[+] hooked JNI_OnLoad lifecycle @ ' + gJniOnLoad);
return true;
} catch (e) {
console.log('[!] hook JNI_OnLoad lifecycle failed: ' + e);
return false;
}
}
// Exception handler — 只打日志不改变行为, 死了能看到真实崩溃 PC
function installExceptionHandler() {
Process.setExceptionHandler(details => {
const ctx = details.context;
console.log('\n[!!!] native exception captured:');
console.log(' type = ' + details.type);
console.log(' pc = ' + formatAddr(ctx.pc));
console.log(' lr = ' + (ctx.x30 ? formatAddr(ctx.x30) : 'null'));
console.log(' sp = ' + ctx.sp);
if (details.memory) {
console.log(' memory = ' + details.memory.operation + ' @ ' + details.memory.address);
}
console.log(' backtrace:');
try {
const bt = Thread.backtrace(ctx, Backtracer.ACCURATE);
for (let i = 0; i < Math.min(bt.length, 15); i++) {
console.log(' #' + i + ' ' + formatAddr(bt[i]));
}
} catch (e) {
console.log(' backtrace failed: ' + e);
}
return false; // 不接管, 让系统继续默认处理 (死了就是真死了)
});
console.log('[+] exception handler installed (log-only)');
}
// dlopen 卡点 — 初始化入口
function hook_dlopen() {
const linker = Process.findModuleByName('linker64') || Process.findModuleByName('linker');
if (!linker) {
console.log('[-] linker not found');
return;
}
let dlopen = linker.findExportByName('__dl___loader_android_dlopen_ext') ||
linker.findExportByName('__dl__android_dlopen_ext') ||
linker.findExportByName('android_dlopen_ext');
if (!dlopen || dlopen.isNull()) {
const syms = linker.enumerateSymbols();
for (let i = 0; i < syms.length; i++) {
const n = syms[i].name;
if (n.indexOf('android_dlopen_ext') !== -1 ||
n.indexOf('__dl___loader_android_dlopen_ext') !== -1) {
dlopen = syms[i].address;
console.log('[*] found dlopen via symbols: ' + n);
break;
}
}
}
if (!dlopen || dlopen.isNull()) {
console.log('[-] android_dlopen_ext not found');
return;
}
Interceptor.attach(dlopen, {
onEnter(args) {
const name = args[0] ? args[0].readCString() : null;
this.isTarget = name && name.indexOf(CFG.soName) !== -1;
if (this.isTarget) {
console.log('\n[★ TARGET] android_dlopen_ext: ' + name);
}
},
onLeave(retval) {
if (!this.isTarget) return;
gStubModule = Process.findModuleByName(CFG.soName);
if (!gStubModule) {
console.log('[-] libDexHelper.so not found after dlopen');
return;
}
console.log('[+] stub base = ' + gStubModule.base + ' size = ' + gStubModule.size);
gJniOnLoad = findJniOnLoad();
if (!gJniOnLoad || gJniOnLoad.isNull()) {
console.log('[-] JNI_OnLoad not found');
return;
}
gPayloadBase = gJniOnLoad.sub(CFG.jniOnLoadOff);
console.log('[+] JNI_OnLoad = ' + gJniOnLoad);
console.log('[+] payloadBase = ' + gPayloadBase);
console.log('[+] sub_11C64 = ' + formatAddr(gPayloadBase.add(CFG.sub11C64Off)));
console.log('[+] sub_F490 = ' + formatAddr(gPayloadBase.add(CFG.subF490Off)));
ensureDumpDir();
patchSub11C64();
hookAndDumpSubF490();
hookJniOnLoadLifecycle();
installExceptionHandler();
console.log('[*] minimal hooks installed (only sub_11C64 patched), waiting for JNI_OnLoad...');
console.log('[*] dump dir = ' + CFG.dumpDir);
}
});
}
rpc.exports = {
dumpCount: function () {
return gDumpCount;
},
dumpDir: function () {
return CFG.dumpDir;
},
};
setImmediate(hook_dlopen);
console.log('[*] dump_dex_min.js loaded — minimal version (only patch sub_11C64) + JNI_OnLoad lifecycle log');运行结果如下,成功DUMP出对应的DEX文件。
~/xiaomi/samsclub frida -U -f cn.samsclub.app -l dump_dex.js ____ / _ | Frida 17.15.4 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . Prefer a GUI? Luma is the official Frida app, with a live REPL, . . . . persistent sessions & collaboration. 7e7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9N6h3#2S2i4K6u0W2k6Y4u0A6k6r3q4Q4x3X3g2J5k6g2)9J5c8R3`.`. . . . . . . . . Connected to 25113PN0EC (id=75040c8d) Spawning `cn.samsclub.app`... // ... [F490] #1 tid=22799 *a1 (dexBase) = 0x7775b3c000 [+] DUMPED -> /data/data/cn.samsclub.app//dex_01_d996413c_11227940.dex (11227940 bytes) [F490] #2 tid=22799 *a1 (dexBase) = 0x77765f2000 [+] DUMPED -> /data/data/cn.samsclub.app//dex_02_c2b59b7b_13518272.dex (13518272 bytes) // ... [F490] #8 tid=22799 *a1 (dexBase) = 0x7779d40000 [+] DUMPED -> /data/data/cn.samsclub.app//dex_08_592d20a2_774456.dex (774456 bytes) Process terminated [JNI_OnLoad] LEAVE retval=0x10004 [★] JNI_OnLoad 完整执行完毕, 进程存活, 反调试已绕过 [!!!] native exception captured: type = access-violation pc = boot.oat!0xc8784 lr = null sp = 0x6ea1f0cff0 memory = read @ 0x0 backtrace: #0 boot-framework.oat!0x385b5c #1 boot-framework.oat!0x385b5c // ... === [EXCEPTION] type=access-violation pc=boot.oat!0xc8784 === lr=null sp=0x6ea1f0cff0 memory=read @ 0x0 [!!!] native exception captured: type = access-violation pc = boot.oat!0xb0108 lr = null sp = 0x6e11decb90 memory = read @ 0x0 backtrace: #0 libart.so!0x6a8ea4 === [EXCEPTION] type=access-violation pc=boot.oat!0xb0108 === lr=null sp=0x6e11decb90 memory=read @ 0x0 Process terminated Thank you for using Frida!
可以看到成功走完了JNI onLoad,这说明:对走完 JNI_OnLoad"这个目标而言,patch sub_11C64 就够了;
但是依旧遇到了崩溃,这里我们判断不出来是native反调试还是java层自己的实现,因此,我们在trace 一下JNI看看?最后可以整理并总结出下面的绑定表。
| Java Class | Method | JNI Signature | Native offset (payload) |
|---|---|---|---|
| com/secneo/apkwrapper/H | bs | (Landroid/content/Context;I)Ljava/lang/Object; | 0x283ec |
| com/secneo/apkwrapper/H | is | (I)Z | 0x28f24 |
| com/secneo/apkwrapper/H | us | (Landroid/content/Context;)V | 0x28f44 |
| com/secneo/apkwrapper/H | gha | (Ljava/lang/String;)I | 0x2a6dc |
| com/secneo/apkwrapper/H | ghc | (Ljava/lang/String;)J | 0x2a74c |
| com/secneo/apkwrapper/H | gah | ()[Ljava/lang/Object; | 0x2a7b4 |
| com/secneo/apkwrapper/H | sha | (Ljava/lang/String;I)V | 0x2ae4c |
| com/secneo/apkwrapper/H | he | (I)V | 0x2ae84 |
| com/secneo/apkwrapper/H | gv | ()I | 0x2aec4 |
| com/secneo/apkwrapper/AW | hn | (Landroid/content/Context;Landroid/app/Application;)V | 0x2fc18 |
| com/secneo/apkwrapper/AW | pn | (Ljava/lang/String;)Ljava/lang/String; | 0x30cc0 |
| com/secneo/apkwrapper/H | d | (Ljava/lang/String;)Ljava/lang/String; | 0x31a38 |
| com/secneo/apkwrapper/H | sn | (Ljava/lang/String;)I | 0x3df48 |
新的问题
到目前为止,我们已经完成了梆梆加密壳的“反调试绕过 + DEX 解密”:
- 找到正确的 hook 窗口:dlopen 返回后、
JNI_OnLoad被调用前。 - 定位并 patch 了自杀函数
sub_11C64,让JNI_OnLoad能够完整返回。 - 在
sub_F490处 dump 出全部 8 个 DEX,验证了壳的解密流程。
但仍有一个悬而未决的问题:JNI_OnLoad 走完后,应用并没有真正跑起来,而是陆续抛出 access-violation read @ 0x0 的 Java 层异常,最终 Process terminated。
基于上面整理出的 RegisterNatives 绑定表,对 com/secneo/apkwrapper/H 和 com/secneo/apkwrapper/AW 里的 native 函数做系统性分析,本质上是为了回答一个问题:JNI_OnLoad 返回后,那些 access-violation read @ 0x0 到底是 Java 层正常启动失败的 NPE,还是壳在 native 层继续触发反调试自杀?
Java 层是怎么被 Trace 到的
JNI_OnLoad 走完后,壳已经通过 OpenCommon 把 8 个真实 DEX 加载进 ART。此时 ART 的 ClassLoader 里就能找到这些类,只是它们还没被链接/执行。要在 Frida 里 hook 它们,核心机制是:
Java.perform(function () {
const cls = Java.use('cn.samsclub.app.system.SamsApplication');
cls.attachBaseContext.implementation = function (base) {
console.log('[JAVA] SamsApplication.attachBaseContext called');
return this.attachBaseContext(base);
};
});Java.perform 会让 Frida 进入 JVM attach 状态,拿到 JNIEnv;Java.use 通过类名去查当前已加载的 jclass,然后反射/直接替换该 Java 方法在 ART 方法表中的入口(也就是常说的 ART Method Hook)。所以:只要类被加载进内存,不管它来自原始 classes.dex 还是壳解密后动态注入的 DEX,都可以 Java.use。
我们写一个java层 trace,在 Java.perform 里同时 hook 壳类 + 真实 Application 链路:
const chain = [ ['com.secneo.apkwrapper.H', ['is', 'bs', 'us', 'sn', 'd', ...]], ['com.secneo.apkwrapper.AW', ['attachBaseContext', 'onCreate', 'patchProvider']], ['com.secneo.apkwrapper.AP', ['instantiateClassLoader', 'instantiateApplication']], ['cn.samsclub.app.system.SamsApplication', ['attachBaseContext', 'onCreate']], ['cn.samsclub.app.system.SamsApplicationLike', ['onBaseContextAttached', 'onCreate']], ['cn.samsclub.app.system.b0', ['A', 'C', 'l', 'D', 'F', 'P', 'z', 'x', 's', 'O', 'E', 'v', 'n', 'i', 't', 'u', 'G']], ['com.tencent.rfix.loader.app.RFixApplication', ['attachBaseContext', 'onCreate']], ];
运行后关键日志如下:
[JAVA] AP.instantiateApplication.instantiateApplication(dalvik.system.PathClassLoader@..., com.secneo.apkwrapper.AW) [JAVA] H.is.is(0) -> ret=false -> ret=cn.samsclub.app.system.SamsApplication@... [JAVA] RFixApplication.attachBaseContext.attachBaseContext(android.app.ContextImpl@...) [JAVA] b0.l.l(cn.samsclub.app.system.SamsApplication@..., cn.samsclub.app.system.SamsApplicationLike@...) [JAVA] SamsApplicationLike.onBaseContextAttached.onBaseContextAttached(android.app.ContextImpl@...) [JAVA] b0.A.A() [JAVA] AW.patchProvider.patchProvider() [JAVA] RFixApplication.onCreate.onCreate() [JAVA] SamsApplicationLike.onCreate.onCreate() [JAVA] b0.C.C() [JAVA] b0.z.z() -> ret=undefined [JAVA] b0.O.O() [JAVA] b0.t.t() -> ret=undefined [JAVA] b0.u.u() -> ret=undefined [JAVA] b0.G.G() -> ret=undefined [JAVA] b0.E.E() [JAVA] b0.i.i() -> ret=undefined Process terminated
这串日志说明:
- H.is(0) 返回 false,壳判断环境“正常”,于是 AP.instantiateApplication 在 Android 10+ 的 AppComponentFactory 路径下直接实例化真实 SamsApplication,而不是走旧版的 AW 代理。
- RFixApplication.attachBaseContext 被调用,证明壳已经把真实 Application 拉起来,并开始走腾讯 RFix 的初始化流程。
- 我们一直跟到了解密后的真实 DEX 内部:cn.samsclub.app.system.b0(也就是 SamsInitHelper)的方法按顺序执行。 所以之前的 access-violation read @ 0x0 不是死在壳的 JNI_OnLoad 阶段,而是死在真实 App 初始化阶段;更准确地说,那些早期 access-violation 是在 dump 脚本只有 sub_11C64 patch、缺少 Java hook 时出现的Frida/ART 交互异常,并不代表真正的崩溃点。
写在最后
总结:我们绕过了梆梆,但是这里头有其他检测,就写到这里吧。
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。