前言 之前写了一版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_11C64 patch 成空函数即可让 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. f52K9s2c8@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. d22K9s2c8@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)Z0x28f24 com/secneo/apkwrapper/H us (Landroid/content/Context;)V0x28f44 com/secneo/apkwrapper/H gha (Ljava/lang/String;)I0x2a6dc com/secneo/apkwrapper/H ghc (Ljava/lang/String;)J0x2a74c com/secneo/apkwrapper/H gah ()[Ljava/lang/Object;0x2a7b4 com/secneo/apkwrapper/H sha (Ljava/lang/String;I)V0x2ae4c com/secneo/apkwrapper/H he (I)V0x2ae84 com/secneo/apkwrapper/H gv ()I0x2aec4 com/secneo/apkwrapper/AW hn (Landroid/content/Context;Landroid/app/Application;)V0x2fc18 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;)I0x3df48
新的问题 到目前为止,我们已经完成了**加密壳的“反调试绕过 + 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内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 2026-8-2 09:29
被kanxue编辑
,原因: 脱敏