一、问题定位:App 的 Frida 检测集中在两个 so
这个 App 的 Frida 检测机制集中在 libDexHelper.so 和 libmsaoaidsec.so 两个 so 里。主要思路是一步步 dump、trace 到相关检测函数入口位置,再交给 AI 静态分析关键逻辑,写出绕过脚本。
说实话,AI 现在确实有点“力大砖飞”的意思了,逆向工作流正在被明显改变。
二、libDexHelper.so:先 dump,再 trace
首先 Hook so 的加载,发现加载到 DexHelper 就闪退了。使用 hook_init.js 去 hook 拿到输出,分析出大致流程如下:
so加载 -> init -> 多次call_constructors -> android_dlopen_ext结束 -> dlopen("libc.so", RTLD_NOW) -> Process terminated
而在 dlopen 到又一次 call_constructors 的调用都指向一个 offset:
0x70f3c57544 libDexHelper.so + 0x4a544
更完整一点的调用栈是:
libDexHelper.so + 0x4a544
libDexHelper.so + 0x4a544
libDexHelper.so + 0x37a40
libDexHelper.so + 0x3596c
libart.so + 0x46ae64
libopenjdkjvm.so + 0x5360
boot.oat + 0x9c940
毫无疑问,这个 so 肯定有 SMC,需要 dump。直接在 android_dlopen_ext 返回之后 dump,发现 hook 到之后还没 dump 就被 kill 了。猜测它很可能自 Hook 了 open、write 这类函数,这也是很多安全/游戏厂商的常用手段。
[SoDump]output: /data/local/tmp/libDexHelper.so_0x7171aa3000_memdump.so
Error: Permission denied
at dumpModule (E:\Test\Work\--------\6.7.7_anti_frida\dump_so.js:79)
at onLeave (E:\Test\Work\--------\6.7.7_anti_frida\dump_so.js:166)
Process terminated
[2312DRAABC::com.--------.mobile ]->
改用 syscall 去 dump 即可,部分没有权限的内存空间也要 dump 下来,防止遗漏。用 syscall_dump.js 成功 dump。其实也想在 JNI_OnLoad 加载之后去 dump,也就是用 syscall_Load_dump.js,但发现 Frida 就是死在 JNI_OnLoad 里面。
[JniDump] ========================================
[JniDump] JNI_OnLoad enter
[JniDump] addr: 0x70f6c4a018
[JniDump] offset: libDexHelper.so + 0x33018
[JniDump] vm: 0xb400007188a22e00
[JniDump] reserved: 0x0
[JniDump] caller: 0x7185185e64
[JniDump] caller offset: libart.so + 0x46ae64
[JniDump] ========================================
Process terminated
改一下脚本,在 JNI_OnLoad 之前 dump,这应该是我们能 dump 的最晚时机了,解密也会更完全一点。
[JniDump] ========================================
[JniDump] dump reason: JNI_OnLoad_enter
[JniDump] module: libDexHelper.so
[JniDump] base: 0x70f2e98000
[JniDump] size: 0x129000
[JniDump] path: /data/app/~~rQzi3tAFqHBlWrFpOm7m5A==/com.--------.mobile-exvsiXgJRbXfwGfAbBJQwQ==/lib/arm64/libDexHelper.so
[JniDump] out : /data/data/com.--------.mobile/files/libDexHelper.so_0x70f2e98000_after_JNI_OnLoad_memdump.so
[JniDump] fd: 90
[JniDump] mprotect whole module readable
[JniDump] mprotect pages total=297 ok=297 fail=0
[JniDump] dump finished
[JniDump] saved: /data/data/com.--------.mobile/files/libDexHelper.so_0x70f2e98000_after_JNI_OnLoad_memdump.so
[JniDump] unreadable before retry: 0
[JniDump] zero filled pages: 0
[JniDump] ========================================
Process terminated
现在大致流程就是这样,将 dump 下来的 so 拿到 Binary Ninja 里面分析:
libDexHelper.so 加载完成
-> JNI_OnLoad 进入
-> 入口处 dump 成功
-> JNI_OnLoad 内部继续执行
-> 进程被 kill / terminate
dump 下来的 so 使用 sofixer 修复之后,大部分地方都可以反编译了。
之前通过 svc 在 JNI_OnLoad 结束之后进行了 dump,直接复用之前的 so,然后加载 Hook 脚本,再加几个函数做个大满贯 hook。
使用带了 init_array 调用监控的脚本去 hook,发现崩溃不仅出现在 init_array 第一个调用结束之后,而且指向匿名内存,并且第二个调用还未开始,很有可能是 init_array 第一个函数中创建线程去 kill。Claude 看过 linker64,说脚本肯定没问题。
[android_dlopen_ext] path :/data/app/~~rQzi3tAFqHBlWrFpOm7m5A==/com.--------.mobile-exvsiXgJRbXfwGfAbBJQwQ==/lib/arm64/libDexHelper.so
[android_dlopen_ext] flags:0x2
[android_dlopen_ext] extinfo:0x7fd78bdf20
>>> [#1] CALL init_array @ 0x779cc8d650 (libDexHelper.so + 0x2f650) for 'libDexHelper.so'
<<< [#1] DONE init_array @ 0x779cc8d650 (libDexHelper.so + 0x2f650) for 'libDexHelper.so'
[android_dlopen_ext] handle:0xad0715354bd48a2b
==============================
Process crashed:Bad access due to invalid address
......
lr 00000078e6b0610c sp 0000007fd78bdee0 pc 00000078e6b06130 pst 0000000080001000
1 total frames
backtrace:
#00 pc 0000000000000130 <anonymous:78e6b06000>
***
[2312DRAABC::com.--------.mobile ]->
这里 App 版本更新到 6.7.8 了,重新 dump 并且 hook 一遍,结果不变,修一下拿到 BN 里面看。
[android_dlopen_ext] path : /data/app/~~h0YzYCcRX4xFSmKUejHKAA==/com.--------.mobile-0382fGhoiO1SDD5DI9qaPQ==/lib/arm64/libDexHelper.so
[android_dlopen_ext] flags: 0x2
[android_dlopen_ext] extinfo: 0x7fe6e77c00
>>> [#1] CALL DT_INIT @ 0x7aa7729098 (libDexHelper.so + 0x128098) for 'libDexHelper.so'
<<< [#1] DONE DT_INIT @ 0x7aa7729098 (libDexHelper.so + 0x128098) for 'libDexHelper.so'
>>> [#2] CALL DT_INIT_ARRAY @ 0x7aa7630650 (libDexHelper.so + 0x2f650) for 'libDexHelper.so'
<<< [#2] DONE DT_INIT_ARRAY @ 0x7aa7630650 (libDexHelper.so + 0x2f650) for 'libDexHelper.so'
[android_dlopen_ext] handle: 0xf5ec6bbf7a0cd67d
==============================
奇怪,找不到线程创建,而且 kill 发生在 dlopen 返回之后。其实写到这里突然想到,还有 JNI_OnLoad 没看。
通过字符串找到两个比较可疑的函数:
const TARGET_FUNCS = [
{
name: 'sub_431bc4',
offset: 0x31bc4,
retType: 'void',
},
{
name: 'sub_457c58',
offset: 0x57c58,
retType: 'int64',
},
];
不出所料,调用来自 JNI_OnLoad:
========== ENTER sub_431bc4 ==========
addr = 0x7aa48c2bc4 (libDexHelper.so + 0x31bc4)
arg1 = 256
arg2 = -1230861953
arg3 = 0xfff
---- registers ----
pc = 0x7aa48c2bc4
lr = 0x7aa48ca560
sp = 0x7fe6e76430
Backtrace:
#0 0x7aa48ca560 0x7aa48ca560 libDexHelper.so!0x39560
#1 0x7aa48ca560 0x7aa48ca560 libDexHelper.so!0x39560
#2 0x7aa48c696c 0x7aa48c696c libDexHelper.so!JNI_OnLoad+0x2954
来到这里发现这是一个 jump,看起来像动态跳转,但结合打印出来的调用栈就能轻松定位。

可以考虑借助 Frida Stalker 进行 Trace,从 JNI_OnLoad 调用开始 Trace,而且似乎会在 sub_432774 中有非常大量的循环。看了一下,这里面是对 Java 层 API 的批量入口点检查,避免 Java 函数被 Hook。
这里是最上层的入口,里面内部进行大量的循环检测:
libDexHelper.so+0x33364 0x7a1a450364 add x2, sp, #0xa8
libDexHelper.so+0x33368 0x7a1a450368 mov x0, xzr
libDexHelper.so+0x3336c 0x7a1a45036c bl #0x7a1a44f774
libDexHelper.so+0x32774 0x7a1a44f774 stp x28, x27, [sp, #-0x60]!
libDexHelper.so+0x32778 0x7a1a44f778 stp x26, x25, [sp, #0x10]
对应调用就是这里:
0043337c if (sub_432774(nullptr, 0x503e12, &var_c78) & 1
0043337c && (uint32_t)var_a67 != 0x77)
0043337c {
00433380 int64_t x0_20 = var_c78;
⚠️0043338c int64_t var_c70;
0043338c 0x42cfc0(x0_20, var_c70 - x0_20, 3);
0043337c }
随便用二分法找个靠后的 offset 开始 Trace,看看哪些会被触发。慢慢跟着 offset 向后追,如果发现进了循环,就找顶层的 Trace 向后设置 Trace。BN 反编译代码确实有点抽象,汇编和伪 C 混在一起比较乱。
如果卡住了就多等一会儿再 Trace。一路跟踪发现进入 sub_436bb8 之后被 kill 掉,在 0x3596c 下 Hook 无法 Trace 到,所以需要继续进入到里面去 Trace。

0x37144 → 0x38024(未命中)
↓
0x37a3c → 0x37a40 → 0x385ec
到 0x385ec 的时候很明显发现只有 2000 多条 Trace,但结尾也不是类似 kill 指令那种。
这里明显还动了 pthread_create,看起来是将 pthread_create 拿到之后进行了调用,后面也做了一些 fd 之类的检测。

sub_452944 内部也很明显发现了疑似 maps 扫描和 sleep 相关的调用。这里大致定位之后就可以交给 Claude 了,重点入口函数就是 sub_436bb8。

三、libDexHelper.so 反 Frida 机制分析与绕过报告
目标:com.--------.mobile 6.7.8
加固壳:libDexHelper.so(爱加密 / SecNeo 系)
平台:Android 14(API 34),arm64
镜像基址:0x400000,Binary Ninja 中地址 = 文件 offset + 0x400000
状态:libDexHelper.so 反调试已绕过,App 可继续启动;下一关为 libmsaoaidsec.so
3.1 总体结论
libDexHelper.so 在 JNI_OnLoad 阶段执行一整套反调试/反 Hook 逻辑,入口为 sub_436bb8。它的防护是多线程、多手段、分散触发的:
- 检测到 Frida 后不一定立刻退出,而是通过「跳非法地址崩溃 / 写加密文件上报 / 破坏 ART 执行 / 静默 exit」等多种方式,且分布在主线程与多个检测线程中。
- 核心是一个通用的「代码是否被 inline hook」检测原语
sub_432774,被 4 个上层检测复用。
- 因此单点封堵 kill 无效,根治方式是让核心检测原语
sub_432774 与 isHooked 统一返回「未 Hook」。
最终以 6 组 Hook 通过该库全部检测。
3.2 执行入口与调用链
System.loadLibrary("DexHelper")
→ libart JavaVMExt::LoadNativeLibrary
→ JNI_OnLoad
→ sub_436bb8 # 反调试总入口(巨型函数, ~0x436bb8-0x43c8xx)
├─ sub_452944 # inline-hook 检测(检 libc!pthread_create 等)
├─ sub_448f14 # IO-hook/PLT 替换框架 + 反 heap-dump
├─ /proc/self/task 扫描 → sub_431bc4(0x100 frida) # Frida 线程名检测
├─ /proc/self/fd 扫描 → linjector 检测
├─ sub_433028 # 批量 Java 入口点检测(内部调 sub_432774)
├─ 多个 pthread_create # 常驻检测线程(maps 扫描/ptrace/sleep 等)
└─ ART 层 dex 解密加载 + Runtime 字段改写
3.3 核心检测机制详解
3.3.1 sub_431bc4(category, magic, 0xfff) —— 中央 kill / 上报原语
flags = *(*(0x502de0) + 0x164); // 全局反调试配置位图
if ((flags & category) == 0) { // 该检测项未在配置启用
sp = 0; lr = 0; // 清栈指针
jump((magic & 0xfff) & 0xfffffffc); // 跳非法低地址 → 主动崩溃
} else { // 已启用
// 按 category 选检测名字符串, 校验 integrity, 写 envc.push 加密文件上报
sub_430aac(name, 1, magic);
return;
}
- 调用到该函数时检测已成立,两个分支都是「处理威胁」。
magic & 0xfff & 0xfffffffc 这类结果是非法低地址,对应早期「Bad access due to invalid address」崩溃。
category 位对应检测名映射如下:

sub_430aac / sub_431094 操作一个名为 envc.push 的加密文件,用于持久化上报检测结果。
3.3.2 sub_432774(arg1, code_addr, out) —— 核心 hook 检测原语
检测 code_addr 处的代码开头是否被 inline hook,返回 &1 表示检测到 Hook。它被 4 处复用:
| 调用者 |
检测对象 |
isHooked(0x4326e0) |
Java 方法的 ArtMethod entry_point |
sub_433028(0x43336c) |
批量 Java 入口点 / 内部数据 |
sub_452944(0x452a74) |
native 库导出符号 |
sub_4612cc(0x463634/670/718) |
其它检测 |
各调用者在 sub_432774 返回 0 时均走「环境干净」分支,不读输出 buffer,因此恒返回 0 即可安全绕过所有 hook 检测。
3.3.3 android::art::ArtMethod::isHooked(env, method) —— 0x4326e0
ArtMethod* m = FromReflectedMethod(method);
void* entry = *(m + entry_point_offset); // 方法 AOT/解释器入口
int r = sub_432774(NULL, entry, &buf); // 检测 entry 是否被 hook
return r & (buf.byte == 0);
- 作为 native 方法通过
RegisterNatives 注册,供 Java 反调试代码调用。
- 检测的 framework 方法 entry 位于
boot-framework.oat;Frida 在场时 sub_432774 读该 entry 越界,导致崩溃 pc 落在 boot-framework.oat。
3.3.4 sub_452944(version, libname, symname) —— inline-hook 检测
- 用
process_vm_readv 读进程内存中符号开头字节,与磁盘 ELF 原始字节比对,不一致即判定被 inline hook,返回 1。
- 调用点
0x4385ec 检测 libc.so!pthread_create,返回 1 即跳非法地址走 kill。
3.3.5 sub_448f14 —— IO-hook / PLT 替换框架 + 反 heap-dump
- 给
libc/libbinder/libutils/libcutils/libart/libperfetto_hprof 的 read/pread/mmap/write/open/pthread_create 等批量挂 PLT/GOT 钩子,重定向到自身 sub_450xxx,并处理文件访问重定向规则。
- 反 heap-dump 解析
libperfetto_hprof.so 的 g_signal_pipe_fds,写 -1 禁用它。
- 崩溃点
sub_448f14+0x104c 即 0x449f5c,对应 *x0_112 = 0xffffffff。Frida 环境下符号解析地址异常导致写崩溃。
3.3.6 线程名 / 注入物检测
- 遍历
/proc/self/task/*/comm,匹配 Frida 特征线程名 gmain / gum-js-loop / gdbus,命中后调用 sub_431bc4。
- 遍历
/proc/self/fd,readlink 匹配 linjector。
- 读取使用 raw syscall,绕过 libc 层 Hook。
3.4 崩溃点演进与定位过程

备注:绕过后日志里 boot*.oat 的 access-violation 是 ART 正常的隐式 SIGSEGV,由 ART 自身 handler 恢复。异常处理器已改为仅关注 libDexHelper 内异常。
3.5 关键地址速查表
镜像基址为 0x400000。

3.6 最终绕过方案

关键取舍:
- 优先替换核心原语,而不是逐个堵 kill:检测点分散且多线程,堵不完;
sub_432774 / isHooked 是所有 hook 判定的收敛点。
- 异常处理器只看 libDexHelper:ART 大量使用 SIGSEGV 做隐式检查,全量拦截既是噪音也影响稳定,改为定向放行。
运行命令:
frida -U -f com.--------.mobile -l hook.js --no-pause
3.7 遗留 / 后续方向
libmsaoaidsec.so 是字节 anti-frida 库,libDexHelper 通过后,启动流程会加载它,是下一个卡点。它的常见手段包括:
- 常驻线程轮询
/proc/self/maps、/proc/self/task 检测 frida-agent / gum / 线程名;
init_array 中提前反调试;
pthread_create 反注册,隐藏线程名。
- 建议同样思路:先
init_array + pthread_create 监控定位检测线程,再定点封堵。
- 根治线程名检测:Frida 的
gmain / gum-js-loop / gdbus / pool-frida-* 线程名是最强特征,考虑用 gadget/改名方案从源头消除。
- 配置位图
*(*(0x502de0)+0x164) 可在稳定时机 dump,反推服务端下发了哪些检测项。
3.8 判断「已绕过」的标志
日志出现以下业务库加载即表示 libDexHelper 关卡通过:
libframework-connectivity-jni.so / libforcedarkimpl.so
libDWIMECore.so / libfntvcrash.so / libsecuritylib.so ...
下面是绕过脚本的核心代码。需要根据本机 linker64 的 call_constructors 内部调用 .init / .init_array 的位点地址进行调整。
'use strict';
// Frida 15/16/17 兼容层 【新增】
if (typeof Module.findExportByName === "undefined") {
Module.findExportByName = function(modName, symbolName) {
if (modName === null) {
// 全局查找符号:遍历全部模块匹配导出(模拟旧行为)
const modules = Process.enumerateModules();
for (const m of modules) {
const addr = m.getExportByName(symbolName);
if (addr !== null) return addr;
}
return null;
} else {
const mod = Process.getModuleByName(modName);
return mod ? mod.getExportByName(symbolName) : null;
}
};
}
/*
* frida -U -f com.--------.mobile -l hook_func.js
*
* 适配:
* Android arm64
* Frida 17.x
* linker64 反汇编基址 0x400000
*/
const TARGET_SO = 'libDexHelper.so';
/*
* IDA 里显示:
* sub_431bc4 @ 0x431bc4
* sub_457c58 @ 0x457c58
*
* 若 IDA image base = 0x400000,则 Frida offset 为:
* 0x431bc4 - 0x400000 = 0x31bc4
* 0x457c58 - 0x400000 = 0x57c58
*/
const TARGET_FUNCS = [
{
name: 'sub_431bc4',
offset: 0x31bc4,
retType: 'void',
},
{
name: 'sub_457c58',
offset: 0x57c58,
retType: 'int64',
},
];
let targetHooked = false;
let linkerHooked = false;
let seq = 0;
/*
* 如果你确认 IDA 没有 0x400000 image base,而 0x431bc4 本身就是 RVA,
* 把上面的 offset 改回:
*
* sub_431bc4: 0x431bc4
* sub_457c58: 0x457c58
*/
function log(s) {
console.log('[DexHelperHook] ' + s);
}
function safeReadCString(p) {
try {
if (p && !p.isNull()) {
return Memory.readCString(p);
}
} catch (e) {}
return null;
}
function shortSoName(path) {
if (!path) {
return 'unknown';
}
const idx = path.lastIndexOf('/');
if (idx >= 0) {
return path.substring(idx + 1);
}
return path;
}
function ptrInRange(mod, addr) {
return addr.compare(mod.base) >= 0 &&
addr.compare(mod.base.add(mod.size)) < 0;
}
function moduleOffsetString(addr) {
try {
const m = Process.findModuleByAddress(addr);
if (!m) {
return '<unknown module>';
}
return m.name + ' + 0x' + addr.sub(m.base).toString(16);
} catch (e) {
return '<unknown module>';
}
}
function printBacktrace(context) {
let bt = [];
try {
bt = Thread.backtrace(context, Backtracer.ACCURATE);
} catch (e) {
try {
bt = Thread.backtrace(context, Backtracer.FUZZY);
} catch (_) {
console.log('Backtrace failed: ' + e);
return;
}
}
console.log('Backtrace:');
bt.forEach(function (addr, i) {
let sym = '';
try {
sym = DebugSymbol.fromAddress(addr).toString();
} catch (e) {
sym = moduleOffsetString(addr);
}
console.log(' #' + i + ' ' + addr + ' ' + sym);
});
}
function dumpTargetFuncArgs(name, args) {
if (name === 'sub_431bc4') {
/*
* void sub_431bc4(int32_t arg1, int32_t arg2, int64_t arg3)
*/
console.log('arg1 = ' + args[0].toInt32());
console.log('arg2 = ' + args[1].toInt32());
console.log('arg3 = ' + args[2]);
return;
}
if (name === 'sub_457c58') {
/*
* int64_t sub_457c58(int64_t arg1, int32_t arg2, void* arg3)
*/
console.log('arg1 = ' + args[0]);
console.log('arg2 = ' + args[1].toInt32());
console.log('arg3 = ' + args[2]);
return;
}
console.log('x0 = ' + args[0]);
console.log('x1 = ' + args[1]);
console.log('x2 = ' + args[2]);
console.log('x3 = ' + args[3]);
}
function hookTargetFunctions(reason) {
if (targetHooked) {
return true;
}
const mod = Process.findModuleByName(TARGET_SO);
if (!mod) {
return false;
}
log('Hooking ' + TARGET_SO + ', reason=' + reason);
log('base=' + mod.base + ', size=0x' + mod.size.toString(16) + ', path=' + mod.path);
const targets = [];
for (let i = 0; i < TARGET_FUNCS.length; i++) {
const item = TARGET_FUNCS[i];
const addr = mod.base.add(item.offset);
log(item.name + ' offset=0x' + item.offset.toString(16) + ', addr=' + addr);
if (!ptrInRange(mod, addr)) {
log('[-] ' + item.name + ' out of module range, skip all hooks');
log(' module range: ' + mod.base + ' - ' + mod.base.add(mod.size));
return false;
}
targets.push({
name: item.name,
addr: addr,
retType: item.retType,
});
}
for (let j = 0; j < targets.length; j++) {
const t = targets[j];
Interceptor.attach(t.addr, {
onEnter(args) {
console.log('');
console.log('========== ENTER ' + t.name + ' ==========');
console.log('addr = ' + t.addr + ' (' + moduleOffsetString(t.addr) + ')');
dumpTargetFuncArgs(t.name, args);
console.log('---- registers ----');
console.log('pc = ' + this.context.pc);
console.log('lr = ' + this.context.lr);
console.log('sp = ' + this.context.sp);
printBacktrace(this.context);
},
onLeave(retval) {
if (t.retType !== 'void') {
console.log('========== LEAVE ' + t.name + ' ==========');
console.log('retval = ' + retval);
}
}
});
log('[+] attached ' + t.name + ' @ ' + t.addr);
}
targetHooked = true;
log('[+] target hooks installed');
return true;
}
function describeInitCall(func, sonamePtr) {
let soname = safeReadCString(sonamePtr);
if (!soname) {
soname = 'unknown';
}
const shortName = shortSoName(soname);
let moduleName = '';
let offset = '';
let modulePath = '';
try {
const m = Process.findModuleByAddress(func);
if (m) {
moduleName = m.name;
modulePath = m.path;
offset = '0x' + func.sub(m.base).toString(16);
}
} catch (e) {}
return {
func: func,
soname: shortName,
sonameRaw: soname,
module: moduleName,
modulePath: modulePath,
off: offset,
};
}
function isTargetInitInfo(info) {
if (!info) {
return false;
}
if (info.soname === TARGET_SO) {
return true;
}
if (info.module === TARGET_SO) {
return true;
}
if (info.sonameRaw && info.sonameRaw.indexOf(TARGET_SO) !== -1) {
return true;
}
if (info.modulePath && info.modulePath.indexOf(TARGET_SO) !== -1) {
return true;
}
return false;
}
function locStr(info) {
if (info.module) {
return info.module + ' + ' + info.off;
}
return '<unknown module>';
}
function getThreadStack(map, tid) {
let s = map[tid];
if (!s) {
s = [];
map[tid] = s;
}
return s;
}
function hookLinkerInitArray() {
if (linkerHooked) {
return;
}
/*
* 全部偏移依据 linker64 反汇编核对,IDA base = 0x400000:
*
* __dl__ZN6soinfo17call_constructorsEv @ 0x461290
* RVA = 0x61290
*
* DT_INIT:
* 0x461444 blr x20 => RVA 0x61444
* 0x461448 返回落点 => RVA 0x61448
* func = x20
* soname = x21
*
* DT_INIT_ARRAY:
* 0x461580 blr x28 => RVA 0x61580
* 0x461584 返回落点 => RVA 0x61584
* func = x28
* soname = x20
*/
const HOOKS = [
{
tag: 'DT_INIT',
call: 0x56874,
ret: 0x56878,
funcReg: 'x20',
nameReg: 'x21',
},
{
tag: 'DT_INIT_ARRAY',
call: 0x568BC,
ret: 0x568C0,
funcReg: 'x28',
nameReg: 'x20',
},
];
const linker = Process.findModuleByName('linker64');
if (!linker) {
log('[-] linker64 not found');
return;
}
log('[+] linker64 @ ' + linker.base + ', size=0x' + linker.size.toString(16));
log('[+] hook DT_INIT / DT_INIT_ARRAY call sites');
const pendingByTid = {};
HOOKS.forEach(function (h) {
const callAddr = linker.base.add(h.call);
const retAddr = linker.base.add(h.ret);
if (!ptrInRange(linker, callAddr)) {
log('[-] ' + h.tag + ' callAddr out of range: ' + callAddr);
return;
}
if (!ptrInRange(linker, retAddr)) {
log('[-] ' + h.tag + ' retAddr out of range: ' + retAddr);
return;
}
log('[+] ' + h.tag + ' call hook @ ' + callAddr + ' linker64 + 0x' + h.call.toString(16));
log('[+] ' + h.tag + ' ret hook @ ' + retAddr + ' linker64 + 0x' + h.ret.toString(16));
/*
* 调用前:
* 当前 PC 命中 blr 指令地址。
* 这时 x20/x28 仍然保存着即将被调用的 init 函数地址。
*
* 关键点:
* 如果这个 init 函数属于 libDexHelper.so,就在 blr 真正执行前安装目标函数 hook。
*/
Interceptor.attach(callAddr, {
onEnter(args) {
const tid = this.threadId;
const stack = getThreadStack(pendingByTid, tid);
let func = ptr(0);
let sonamePtr = ptr(0);
try {
func = this.context[h.funcReg];
sonamePtr = this.context[h.nameReg];
} catch (e) {}
const info = describeInitCall(func, sonamePtr);
info.id = ++seq;
info.tag = h.tag;
info.tid = tid;
stack.push(info);
const line =
'>>> [#' + info.id + '] CALL ' + h.tag +
' @ ' + info.func +
' (' + locStr(info) + ')' +
" for '" + info.soname + "'" +
' tid=' + tid;
if (isTargetInitInfo(info)) {
console.log('');
console.log('[DexHelperHook] [TARGET INIT] ' + line);
/*
* 这里是最关键的位置:
* libDexHelper.so 已经 map 完成,constructor 还没真正 blr 进去。
* 此时 hook base + offset,能覆盖 init 中即将调用的目标函数。
*/
hookTargetFunctions('before ' + h.tag + ' constructor call');
console.log('[DexHelperHook] Target init caller backtrace:');
printBacktrace(this.context);
} else {
/*
* 如果你想看所有 so 的 init 调用,可以取消下面这行注释。
*/
// console.log(line);
}
}
});
/*
* 调用返回后:
* 如果某个 constructor 内反调试导致崩溃/退出/卡死,
* 对应的 DONE 不会出现。
*/
Interceptor.attach(retAddr, {
onEnter(args) {
const tid = this.threadId;
const stack = getThreadStack(pendingByTid, tid);
const info = stack.pop();
if (!info) {
return;
}
if (isTargetInitInfo(info)) {
console.log(
'[DexHelperHook] <<< [#' + info.id + '] DONE ' + info.tag +
' @ ' + info.func +
' (' + locStr(info) + ')' +
" for '" + info.soname + "'" +
' tid=' + tid
);
}
}
});
});
linkerHooked = true;
}
function hookDlopenFallback() {
const names = [
'android_dlopen_ext',
'dlopen',
];
names.forEach(function (name) {
const addr = Module.findExportByName(null, name);
if (!addr) {
return;
}
log('[+] hook ' + name + ' @ ' + addr);
Interceptor.attach(addr, {
onEnter(args) {
this.path = null;
try {
if (args[0] && !args[0].isNull()) {
this.path = Memory.readCString(args[0]);
}
} catch (e) {}
if (this.path && this.path.indexOf(TARGET_SO) !== -1) {
log(name + ' onEnter: ' + this.path);
}
},
onLeave(retval) {
if (this.path && this.path.indexOf(TARGET_SO) !== -1) {
log(name + ' onLeave: ' + this.path + ', retval=' + retval);
/*
* 注意:
* 这里通常已经晚于 DT_INIT / DT_INIT_ARRAY。
* 只是兜底,防止 linker call-site hook 没命中。
*/
hookTargetFunctions(name + '.onLeave fallback');
}
}
});
});
}
function hookLinkerSymbolFallback() {
/*
* 有些系统 linker64 的 call site 偏移不一致。
* 这个 fallback 尝试通过符号名找 call_constructors / call_array / call_function。
* 如果系统符号被裁剪,可能找不到,没关系。
*/
const linker = Process.findModuleByName('linker64');
if (!linker) {
return;
}
let symbols = [];
try {
symbols = linker.enumerateSymbols();
} catch (e) {
return;
}
symbols.forEach(function (sym) {
const n = sym.name || '';
const interesting =
n.indexOf('call_constructors') !== -1 ||
n.indexOf('call_array') !== -1 ||
n.indexOf('call_function') !== -1;
if (!interesting) {
return;
}
log('[+] linker symbol fallback found: ' + n + ' @ ' + sym.address);
});
}
function main() {
log('script loaded');
/*
* 如果脚本加载时目标 so 已经在内存中,先尝试直接 hook。
* 这种情况可能已经错过 init,但能覆盖后续调用。
*/
hookTargetFunctions('already loaded');
/*
* 关键 hook:
* 在 linker64 执行 DT_INIT / DT_INIT_ARRAY 的 blr 前拦截。
*/
hookLinkerInitArray();
/*
* 打印一下符号 fallback 信息,辅助确认当前系统 linker 情况。
*/
hookLinkerSymbolFallback();
/*
* 兜底。
*/
hookDlopenFallback();
log('init done');
}
setImmediate(main);
下面是完整的绕过脚本,包含 libDexHelper.so 主要检测点替换、libmsaoaidsec.so 的 P0/P1/P2 patch 逻辑。
/*
* libDexHelper.so 6.7.8 anti-frida 分析 / 绕过脚本
* (Frida 17.x API 版本)
*
* 镜像基址 = 0x400000 (BN 中地址 - offset)
* sub_431bc4 (kill/report 原语) offset 0x31bc4
* sub_452944 (inline-hook 检测) offset 0x52944
* sub_432774 (Java 入口点批量检查) offset 0x32774
* sub_436bb8 (顶层反调试入口) offset 0x36bb8
*
* 用法:
* frida -U -f com.--------.mobile -l hook.js --no-pause
* 或 attach:
* frida -U com.--------.mobile -l hook.js
*/
'use strict';
// ============ 配置 ============
const MODULE = 'libDexHelper.so';
const IMAGE_BASE = 0x400000;
// 是否绕过 kill: 让 sub_431bc4 在“未检测到”分支不跳非法地址,直接安全返回
const BYPASS_KILL = true;
// 是否绕过 inline-hook 检测: 让 sub_452944 恒返回 0
const BYPASS_INLINE_CHK = true;
// 是否打印 backtrace
const PRINT_BT = true;
// !!! 侵入式 hook 开关 !!!
// sub_436bb8 是 sub_448f14 的父函数, sub_432774 调用极频繁;
// attach 它们的 trampoline 会与加固壳自身的 .text 校验/PLT patch 冲突, 导致 0x449f5c 崩溃。
// 默认关闭, 只保留 sub_431bc4 / sub_452944 两个必要 hook。
const HOOK_TOP = false; // hook sub_436bb8
const HOOK_JAVASCAN = false; // hook sub_432774
// 异常处理器: 若崩在 libDexHelper.so 内, 试探性地把 pc+4 跳过该指令继续执行
const SKIP_LIBDEX_FAULT = false;
// 修复 0x449f5c 崩溃: hook sub_441bc4(ELF符号解析器), 把 g_signal_pipe_fds 返回值
// 重定向到合法可写内存, 使 "*x0_112 = 0xffffffff" 不再崩溃
const FIX_PERFETTO = true;
// 监控/拦截进程退出: 抓 "谁 kill 了进程"
const WATCH_KILL = true; // hook exit/abort/kill/tgkill 打印来源
const BLOCK_SELF_KILL = true; // 吞掉 kill/tgkill/tkill/pthread_kill 的自杀调用
const BLOCK_EXIT = true; // 吞掉来自 libDexHelper 的 exit/_exit/abort (危险, 但用于探测)
const OFF = {
kill: 0x31bc4,
inlineChk: 0x52944,
javaScan: 0x32774,
top: 0x36bb8,
symResolve: 0x41bc4, // sub_441bc4: 自实现 ELF 符号解析
isHooked: 0x326e0, // ArtMethod::isHooked(env, method): 检测 ART 方法 entry 是否被 hook
};
// 绕过 ArtMethod::isHooked, 直接返回 0(未 hook), 避免 sub_432774 读 entry_point 越界崩溃
const BYPASS_ISHOOKED = true;
// 绕过核心 hook 检测原语 sub_432774, 恒返回 0(未 hook)。
// 它被 isHooked / sub_433028(批量) / sub_452944(inline) / sub_4612cc 调用, 是所有 hook 检测的根。
const BYPASS_SUB432774 = true;
// category bit -> 检测名
const CATEGORY = {
0x1: 'root',
0x2: 'usb?',
0x4: 'emu',
0x8: 'appmon?',
0x10: 'proxy',
0x20: 'polling',
0x40: 'inject',
0x80: 'xposed',
0x100: 'frida',
0x200: 'hook',
0x400: 'integrity',
0x800: 'signature',
0x1000: 'debug',
0x2000: 'rom',
0x4000: 'display',
0x8000: 'bl',
0x10000: 'developer',
0x20000: 'unsource',
0x40000: 'location',
};
function catName(v) {
return CATEGORY[v.toInt32 ? v.toInt32() : v] || ('0x' + Number(v).toString(16));
}
function bt(ctx, base) {
if (!PRINT_BT) return '';
try {
return Thread.backtrace(ctx, Backtracer.FUZZY)
.map(a => {
const off = a.sub(base);
const inMod = off.compare(0) >= 0 && off.compare(0x200000) < 0;
return ' ' + a + (inMod ? (' ' + MODULE + '+0x' + off.toString(16)) : (' ' + (DebugSymbol.fromAddress(a) || '')));
})
.join('\n');
} catch (e) { return ' <bt err ' + e + '>'; }
}
function hexArg(a) {
if (a === undefined || a === null) return 'null';
return a.toString();
}
function safeCStr(p) {
try {
if (p.isNull()) return 'null';
return JSON.stringify(p.readUtf8String());
} catch (e) {
try { return JSON.stringify(p.readCString()); } catch (e2) { return '<' + p + '>'; }
}
}
function install(base) {
console.log(' ' + MODULE + ' base = ' + base);
globalThis.__DEX_BASE = base;
const killAddr = base.add(OFF.kill);
const inlineAddr = base.add(OFF.inlineChk);
const javaScanAddr = base.add(OFF.javaScan);
const topAddr = base.add(OFF.top);
// ---- sub_431bc4: kill / report ----
// void sub_431bc4(int category, int magic, int arg3)
if (BYPASS_KILL) {
// 用 replace 完全接管: 打印后直接返回, 既不上报也不 jump 非法地址
const origType = new NativeFunction(killAddr, 'void', ['int', 'int', 'int']);
Interceptor.replace(killAddr, new NativeCallback(function (cat, magic, arg3) {
const c = cat >>> 0, m = magic >>> 0, a3 = arg3 >>> 0;
console.log('\n========== sub_431bc4 (KILL/REPORT) [BYPASSED] ==========');
console.log(' category = 0x' + c.toString(16) + ' (' + catName(c) + ')');
console.log(' magic = 0x' + m.toString(16));
console.log(' jumpTargetIfCrash = 0x' + ((m & a3 & 0xfffffffc) >>> 0).toString(16));
// 不调用原函数, 直接返回
}, 'void', ['int', 'int', 'int']));
void origType;
} else {
Interceptor.attach(killAddr, {
onEnter(args) {
this.cat = args[0].toInt32() >>> 0;
this.magic = args[1].toInt32() >>> 0;
this.arg3 = args[2].toInt32() >>> 0;
const jumpTarget = (this.magic & this.arg3 & 0xfffffffc) >>> 0;
console.log('\n========== sub_431bc4 (KILL/REPORT) ==========');
console.log(' category = 0x' + this.cat.toString(16) + ' (' + catName(this.cat) + ')');
console.log(' magic = 0x' + this.magic.toString(16));
console.log(' arg3 = 0x' + this.arg3.toString(16));
console.log(' -> 若走崩溃分支, jump target = 0x' + jumpTarget.toString(16) + ' (非法地址)');
console.log(bt(this.context, base));
},
onLeave(retval) {
console.log(' <== sub_431bc4 returned (未崩溃)');
}
});
}
// ---- sub_452944: inline-hook 检测 ----
Interceptor.attach(inlineAddr, {
onEnter(args) {
this.ver = args[0].toInt32();
this.lib = safeCStr(args[1]);
this.sym = safeCStr(args[2]);
console.log('\n---- sub_452944 (INLINE-HOOK CHECK) ----');
console.log(' version = ' + this.ver);
console.log(' lib = ' + this.lib);
console.log(' symbol = ' + this.sym);
console.log(bt(this.context, base));
},
onLeave(retval) {
console.log(' sub_452944 ret = ' + retval + ' (1=检测到hook)');
if (BYPASS_INLINE_CHK && retval.toInt32() === 1) {
console.log(' [BYPASS] 强制返回 0');
retval.replace(0);
}
}
});
// ---- sub_432774: Java 入口点批量检查 (量大, 只计数; 默认关闭, 易冲突) ----
if (HOOK_JAVASCAN) {
let javaScanCount = 0;
Interceptor.attach(javaScanAddr, {
onEnter(args) {
javaScanCount++;
if (javaScanCount <= 3) {
console.log('[sub_432774] Java入口点检查 #' + javaScanCount);
} else if (javaScanCount % 500 === 0) {
console.log('[sub_432774] 调用次数 = ' + javaScanCount);
}
}
});
}
// ---- sub_436bb8: 顶层入口 (默认关闭, 是崩溃函数的父函数, 极易冲突) ----
if (HOOK_TOP) {
Interceptor.attach(topAddr, {
onEnter(args) {
console.log('\n############ sub_436bb8 ENTER (顶层反调试) ############');
console.log(bt(this.context, base));
},
onLeave(retval) {
console.log('############ sub_436bb8 LEAVE ret=' + retval + ' ############');
}
});
}
void javaScanAddr; void topAddr;
// ---- sub_441bc4: ELF 符号解析器, 修复 perfetto g_signal_pipe_fds 崩溃 ----
if (FIX_PERFETTO) {
const symAddr = base.add(OFF.symResolve);
const fakePerfetto = Memory.alloc(64); // 合法可写, 供 *x0_112=-1 / x0_112[1]=-1 写入
fakePerfetto.writeByteArray(new Array(64).fill(0));
Interceptor.attach(symAddr, {
onEnter(args) {
this.sym = safeCStr(args[1]);
},
onLeave(retval) {
if (this.sym && this.sym.indexOf('g_signal_pipe_fds') !== -1) {
console.log('\n[FIX_PERFETTO] sub_441bc4("g_signal_pipe_fds") 原返回=' + retval
+ ' -> 重定向到合法内存 ' + fakePerfetto);
retval.replace(fakePerfetto);
}
}
});
}
// ---- dump 全局反调试配置 flags: [[base+0x102de0]] + 0x164 ----
try {
const gotSlot = base.add(0x102de0); // 0x502de0
const cfgPtr = gotSlot.readPointer(); // -> 全局结构
const cfg = cfgPtr.readPointer(); // -> 实际 config
const flags = cfg.add(0x164).readU32();
console.log('\n 反调试配置 flags @[[0x502de0]]+0x164 = 0x' + flags.toString(16));
const enabled = [];
Object.keys(CATEGORY).forEach(k => {
const bit = parseInt(k);
if (flags & bit) enabled.push(CATEGORY[k] + '(0x' + bit.toString(16) + ')');
});
console.log(' 启用的检测项: ' + (enabled.length ? enabled.join(', ') : '(无, 命中即崩溃)'));
} catch (e) {
console.log('[!] dump config 失败: ' + e);
}
// ---- sub_432774: 核心 hook 检测原语, 恒返回 0 ----
if (BYPASS_SUB432774) {
const p = base.add(OFF.javaScan);
Interceptor.replace(p, new NativeCallback(function (a1, a2, a3) {
return 0; // 0 = 未检测到 hook, 所有调用者走"环境干净"分支
}, 'int', ['pointer', 'pointer', 'pointer']));
console.log(' sub_432774 @ ' + p + ' 已接管 (核心检测原语, 恒返回 0)');
}
// ---- ArtMethod::isHooked: 强制返回 0 (未 hook) ----
if (BYPASS_ISHOOKED) {
const p = base.add(OFF.isHooked);
let cnt = 0;
Interceptor.replace(p, new NativeCallback(function (env, method) {
cnt++;
if (cnt <= 5) console.log('[BYPASS_ISHOOKED] isHooked() 调用 #' + cnt + ' -> 返回 0');
return 0;
}, 'int', ['pointer', 'pointer']));
console.log(' isHooked @ ' + p + ' 已接管 (恒返回 0)');
}
if (WATCH_KILL) installKillWatch(base);
console.log(' hooks installed. BYPASS_KILL=' + BYPASS_KILL + ' BYPASS_INLINE_CHK=' + BYPASS_INLINE_CHK);
}
// ============ 进程退出监控 / 拦截 ============
function installKillWatch(base) {
const MY_PID = Process.id;
const LETHAL = [4, 6, 9, 11, 15, 19]; // ILL/ABRT/KILL/SEGV/TERM/STOP
function raFrom(ctx) {
// 用浅 backtrace 判断是否来自 libDexHelper
try {
const frames = Thread.backtrace(ctx, Backtracer.ACCURATE).slice(0, 8);
for (const a of frames) {
const off = a.sub(base);
if (off.compare(0) >= 0 && off.compare(0x200000) < 0) {
return MODULE + '+0x' + off.toString(16);
}
}
return frames.length ? ('' + frames[0]) : '?';
} catch (e) { return '?'; }
}
// ---- exit / _exit / _Exit / abort ----
// Frida 17: Module.findExportByName(null, name) 已移除, 改用 Module.findGlobalExportByName(name)
['exit', '_exit', '_Exit', 'abort'].forEach(name => {
const p = Module.findGlobalExportByName(name);
if (!p) return;
Interceptor.attach(p, {
onEnter(args) {
const from = raFrom(this.context);
let raStr = '?';
try {
const ra = this.returnAddress;
const o = ra.sub(base);
raStr = (o.compare(0) >= 0 && o.compare(0x200000) < 0) ? (MODULE + '+0x' + o.toString(16)) : ('' + ra);
} catch (e) {}
console.log('\n[KILL] ' + name + '(' + (name === 'abort' ? '' : args[0]) + ') ra=' + raStr + ' from=' + from);
console.log(bt(this.context, base));
const fromDex = (from.indexOf(MODULE) === 0) || (raStr.indexOf(MODULE) === 0);
if (BLOCK_EXIT && fromDex) {
console.log(' [BLOCK] 挂起线程, 阻止 ' + name + ' 退出');
Thread.sleep(999999); // onEnter 永不返回 -> 原函数不执行
}
}
});
});
// ---- kill(pid, sig) ----
const killP = Module.findGlobalExportByName('kill');
if (killP) {
const orig = new NativeFunction(killP, 'int', ['int', 'int']);
Interceptor.replace(killP, new NativeCallback(function (pid, sig) {
console.log('\n[KILL] kill(pid=' + pid + ', sig=' + sig + ')');
if (BLOCK_SELF_KILL && (pid === MY_PID || pid === 0 || pid === -1) && LETHAL.indexOf(sig) !== -1) {
console.log(' [BLOCK] 吞掉自杀 kill');
return 0;
}
return orig(pid, sig);
}, 'int', ['int', 'int']));
}
// ---- tgkill(tgid, tid, sig) ----
const tgkillP = Module.findGlobalExportByName('tgkill');
if (tgkillP) {
const orig = new NativeFunction(tgkillP, 'int', ['int', 'int', 'int']);
Interceptor.replace(tgkillP, new NativeCallback(function (tgid, tid, sig) {
console.log('\n[KILL] tgkill(tgid=' + tgid + ', tid=' + tid + ', sig=' + sig + ')');
if (BLOCK_SELF_KILL && (tgid === MY_PID || tgid === 0) && LETHAL.indexOf(sig) !== -1) {
console.log(' [BLOCK] 吞掉自杀 tgkill');
return 0;
}
return orig(tgid, tid, sig);
}, 'int', ['int', 'int', 'int']));
}
// ---- tkill(tid, sig) ----
const tkillP = Module.findGlobalExportByName('tkill');
if (tkillP) {
const orig = new NativeFunction(tkillP, 'int', ['int', 'int']);
Interceptor.replace(tkillP, new NativeCallback(function (tid, sig) {
console.log('\n[KILL] tkill(tid=' + tid + ', sig=' + sig + ')');
if (BLOCK_SELF_KILL && LETHAL.indexOf(sig) !== -1) {
console.log(' [BLOCK] 吞掉 tkill');
return 0;
}
return orig(tid, sig);
}, 'int', ['int', 'int']));
}
// ---- pthread_kill(thread, sig) ----
const pkP = Module.findGlobalExportByName('pthread_kill');
if (pkP) {
const orig = new NativeFunction(pkP, 'int', ['pointer', 'int']);
Interceptor.replace(pkP, new NativeCallback(function (thr, sig) {
console.log('\n[KILL] pthread_kill(sig=' + sig + ')');
if (BLOCK_SELF_KILL && LETHAL.indexOf(sig) !== -1) {
console.log(' [BLOCK] 吞掉 pthread_kill');
return 0;
}
return orig(thr, sig);
}, 'int', ['pointer', 'int']));
}
// ---- raw syscall: exit_group(94) / kill(129) / tgkill(131) / tkill(130) ----
const scP = Module.findGlobalExportByName('syscall');
if (scP) {
Interceptor.attach(scP, {
onEnter(args) {
const nr = args[0].toInt32();
if (nr === 94 || nr === 93) { // exit_group / exit
console.log('\n[KILL] syscall(exit_group/exit=' + nr + ', code=' + args[1] + ') from=' + raFrom(this.context));
console.log(bt(this.context, base));
} else if (nr === 129 || nr === 130 || nr === 131) { // kill/tkill/tgkill
console.log('\n[KILL] syscall(nr=' + nr + ' kill-family) from=' + raFrom(this.context));
}
}
});
}
console.log(' kill-watch 已安装 (pid=' + MY_PID + ') BLOCK_SELF_KILL=' + BLOCK_SELF_KILL + ' BLOCK_EXIT=' + BLOCK_EXIT);
}
// ============ SIGSEGV 崩溃定位 ============
Process.setExceptionHandler(function (details) {
const base = globalThis.__DEX_BASE;
const pc = details.context.pc;
const pcMod = Process.findModuleByAddress(pc);
const pcModName = pcMod ? pcMod.name : null;
const isArtNoise = pcModName === 'libart.so' || (pcModName && pcModName.indexOf('boot') === 0 && pcModName.indexOf('.oat') !== -1);
if (isArtNoise) {
return false;
}
try {
console.log('\n!!!!!!!!!! EXCEPTION !!!!!!!!!!');
console.log(' type = ' + details.type);
console.log(' address = ' + details.address);
console.log(' pc = ' + pc);
if (base) {
const off = pc.sub(base);
if (off.compare(0) >= 0 && off.compare(0x200000) < 0) {
console.log(' pc in ' + MODULE + '+0x' + off.toString(16));
} else {
console.log(' pc module = ' + (pcMod ? (pcMod.name + '+0x' + pc.sub(pcMod.base).toString(16)) : '<anonymous/unknown>'));
}
} else {
console.log(' pc module = ' + (pcMod ? (pcMod.name + '+0x' + pc.sub(pcMod.base).toString(16)) : '<anonymous/unknown>'));
}
const ctx = details.context;
try {
const lr = ctx.lr, sp = ctx.sp;
console.log(' lr = ' + lr);
const lrMod = Process.findModuleByAddress(lr);
console.log(' lr module = ' + (lrMod ? (lrMod.name + '+0x' + lr.sub(lrMod.base).toString(16)) : '<unknown>'));
console.log(' sp = ' + sp);
const regs = [];
for (let i = 0; i <= 30; i++) {
const r = ctx['x' + i];
if (r === undefined) continue;
const rm = Process.findModuleByAddress(r);
regs.push(' x' + i + '=' + r + (rm ? (' (' + rm.name + '+0x' + r.sub(rm.base).toString(16) + ')') : ''));
}
console.log(regs.join('\n'));
} catch (e) { console.log(' reg dump err: ' + e); }
console.log(' backtrace:');
console.log(bt(details.context, base || ptr(0)));
if (SKIP_LIBDEX_FAULT && base) {
const off = pc.sub(base);
if (off.compare(0) >= 0 && off.compare(0x200000) < 0) {
console.log(' [SKIP] pc += 4, 跳过 ' + MODULE + '+0x' + off.toString(16));
details.context.pc = pc.add(4);
return true;
}
}
} catch (e) {
console.log('exc handler err: ' + e);
}
return false;
});
// ============ libmsaoaidsec.so 绕过 (基于 IDA 逆向报告, NagaLinker v8.83) ============
const SEC_MODULE = 'libmsaoaidsec.so';
const WATCH_SEC = true;
const NEUTER_SEC_THREADS = false;
const PATCH_SEC_P0 = true;
const PATCH_SEC_P1 = true;
const PATCH_SEC_P2 = false;
const SEC_OFF = {
// P0 — 杀进程执行器
exitExec1: 0x234E0,
exitExec2: 0x26334,
exitExec3: 0x269AC,
exitExec4: 0x260B0,
killPoint: 0x11FA4,
// P1 — 检测原语
tracerPid: 0x1AE48,
tracerPPid: 0x1AB54,
taskTState: 0x1B730,
taskNameScan: 0x1BFAC,
fdScan: 0x1C158,
mapsScan: 0x1C26C,
crcScan: 0x1678C,
// P2 — 低优先级
adbJudge: 0x19A58,
artCheck: 0x8CAC,
adbDetect: 0x19E0C,
forkPtrace: 0x1B380,
};
function patchSecModule(base) {
function noop(off, retType, argTypes, retVal) {
try {
Interceptor.replace(base.add(off), new NativeCallback(function () {
return retVal;
}, retType, argTypes));
console.log(' [PATCH] ' + SEC_MODULE + '+0x' + off.toString(16) + ' -> no-op(ret=' + retVal + ')');
} catch (e) {
console.log(' [!] patch 0x' + off.toString(16) + ' 失败: ' + e);
}
}
if (PATCH_SEC_P0) {
noop(SEC_OFF.exitExec1, 'void', ['uint32'], undefined);
noop(SEC_OFF.exitExec2, 'void', ['int64'], undefined);
noop(SEC_OFF.exitExec3, 'void', ['int64'], undefined);
noop(SEC_OFF.exitExec4, 'void', ['int64'], undefined);
noop(SEC_OFF.killPoint, 'void', [], undefined);
}
if (PATCH_SEC_P1) {
noop(SEC_OFF.tracerPid, 'int', [], 0);
noop(SEC_OFF.tracerPPid, 'int', ['uint32'], 1);
noop(SEC_OFF.taskTState, 'int', [], 0);
noop(SEC_OFF.taskNameScan, 'pointer', [], NULL);
noop(SEC_OFF.fdScan, 'pointer', [], NULL);
noop(SEC_OFF.mapsScan, 'int', [], 0);
noop(SEC_OFF.crcScan, 'int', [], 0);
}
if (PATCH_SEC_P2) {
noop(SEC_OFF.adbJudge, 'uint32', ['pointer'], 0);
noop(SEC_OFF.artCheck, 'int', [], 0);
noop(SEC_OFF.adbDetect, 'void', [], undefined);
noop(SEC_OFF.forkPtrace, 'int', ['pointer', 'pointer'], 0);
}
console.log(' ' + SEC_MODULE + ' P0/P1/P2 patch 完成 (P0=' + PATCH_SEC_P0 + ' P1=' + PATCH_SEC_P1 + ' P2=' + PATCH_SEC_P2 + ')');
}
function installSecModuleWatch() {
const pthreadCreate = Module.findGlobalExportByName('pthread_create');
if (!pthreadCreate) return;
const patchedThreadAddrs = new Set();
let secBasePatched = false;
Interceptor.attach(pthreadCreate, {
onEnter(args) {
const startRoutine = args[2];
const m = Process.findModuleByAddress(startRoutine);
if (!(m && m.name === SEC_MODULE)) return;
if (!secBasePatched) {
secBasePatched = true;
console.log('\n ' + SEC_MODULE + ' base = ' + m.base + ' (首次 pthread_create 命中, 开始 patch)');
patchSecModule(m.base);
}
const off = startRoutine.sub(m.base);
const key = startRoutine.toString();
console.log('[pthread_create] ' + SEC_MODULE + ' 检测线程 entry offset=0x' + off.toString(16));
if (NEUTER_SEC_THREADS && !patchedThreadAddrs.has(key)) {
patchedThreadAddrs.add(key);
try {
Interceptor.replace(startRoutine, new NativeCallback(function () {
console.log(' [NEUTER] ' + SEC_MODULE + '+0x' + off.toString(16) + ' 检测线程已被清空, 直接返回');
return NULL;
}, 'pointer', ['pointer']));
} catch (e) {
console.log(' [!] replace 失败(可能已被处理过): ' + e);
}
}
}
});
console.log(' ' + SEC_MODULE + ' 的 pthread_create 监控已装好 (NEUTER_SEC_THREADS=' + NEUTER_SEC_THREADS + ')');
}
if (WATCH_SEC) installSecModuleWatch();
// ============ 等待模块加载 ============
function tryInstall() {
const m = Process.findModuleByName(MODULE);
if (m) {
install(m.base);
return true;
}
return false;
}
if (!tryInstall()) {
const candidates = ['android_dlopen_ext', 'dlopen', '__loader_android_dlopen_ext'];
let done = false;
candidates.forEach(name => {
const p = Module.findGlobalExportByName(name);
if (!p) return;
Interceptor.attach(p, {
onEnter(args) {
try {
const path = args[0].readCString();
console.log("load SO: " + path);
this.isTarget = path && path.indexOf(MODULE) !== -1;
} catch (e) {}
},
onLeave(retval) {
if (done) return;
if (this.isTarget) {
if (tryInstall()) done = true;
}
}
});
});
console.log(' 等待 ' + MODULE + ' 加载...');
}
四、libmsaoaidsec.so:SMC 出的 exit_group
从此开始换了个 Android 16 的设备,下面脚本都是 Frida 17 版本。
把之前的脚本注入,全部按预期绕过,卡在 load SO: libc.so 后就 Process terminated。放开异常捕获,发现:
[pthread_create] libmsaoaidsec.so 检测线程 entry offset=0x1c544
[pthread_create] libmsaoaidsec.so 检测线程 entry offset=0x1b8d4
[pthread_create] libmsaoaidsec.so 检测线程 entry offset=0x26e5c
但是没打出 [NEUTER] 或 KILL,大概率是在 .init_array 之类的早期构造函数里同步执行检测 + kill。想用 Stalker 跟一下 syscall,结果直接崩。
看到上面 AI 表现这么好,这个 so 也比较经典了,干脆直接写提示词交给 AI 分析。感觉 AI 王朝真的来了。
不出所料,真凶是类似 SMC 解出来的 exit_group,检测时间在 .init_proc。
五、libmsaoaidsec.so 加载期反 Frida 分析报告
目标:libmsaoaidsec.so,Android arm64,ELF64 AArch64,base=0,所有偏移即文件偏移。
分析方式:IDA Pro 9.0 + ida-pro-mcp 直连反编译。
库身份:JNI_OnLoad 日志标签 NagaLinker v8.83,即娜迦/Naga 加固体系的加载期反调试库。
5.1 一句话结论
杀进程的真凶不是 kill / tgkill / exit 符号,而是运行时解密出的 28 字节内联 shellcode:
movz x8, #94 ; AArch64 __NR_exit_group = 94
svc #0
ret
由 sub_234E0 / sub_26334 / sub_269AC / sub_260B0 四个执行器 mmap RWX 后直接执行,完全不经过 libc 符号。
5.2 .init_array 与 DT_INIT 全貌
用 ELF 动态段解析得到:
| 项目 |
地址 |
内容 |
| DT_INIT |
0x14400 |
.init_proc,控制流平坦化状态机 |
| DT_INIT_ARRAY |
0x46F80 |
5 个有效指针 + 1 个 0 终止 |
| DT_FINI_ARRAY |
0x46FB0 |
0x83F0 start + 0 |
5.2.1 .init_array 五个函数
这五个函数均无检测逻辑,纯 C++ 静态初始化:
| 函数 |
地址 |
角色 |
| sub_83FC |
0x83FC |
注册两个 atexit(nullsub) |
| sub_8448 |
0x8448 |
清零 0x4D370 起的一组全局 |
| sub_8460 |
0x8460 |
pthread_key_create(&dword_5D3A8, sub_28DD8) + atexit(sub_28DBC),TLS 键 |
| sub_84B4 |
0x84B4 |
把 0x5D3C8~0x5D420 一组 qword 置 1 |
| sub_85A8 |
0x85A8 |
同上,0x5D428~0x5D480 一组 |
5.2.2 .init_proc 线性化主流程
读 canary
*off_47FB8 = sub_123F0(); // ro.build.version.sdk → 存全局
sub_12550(); // persist.sys.dalvik.vm.lib 是否含 "art"
sub_12440(); // release_or_codename 含 'S'/"12"/security_patch → 修正 SDK 值
if (*off_47FB8 > 23) *off_47ED8 = 1;
if (sub_25A48() 为奇数) return; // 门控:配置 dword_48850==218 时跑 sub_23B18 的 cmdline/maps 扫描
读取 /proc/<pid>/cmdline; v10 = strchr(buf, ':')
若含 ':' → 直接进 sub_13728 链(不设 dword_49014,线程2 会立即杀)
若无 ':' → sub_1BEC4: dword_49014 = getpid(); 然后 sub_1B924() ← 线程大管家
sub_13728() → sub_2701C()(线程3) + sub_198D8()(DEX CRC32 校验) + sub_95C8()
sub_23AD4(); v7 = sub_C830(); if (v7==1) { sub_95C8(); sub_9150()(线程5) }
所以 .init_proc 就是“同步检测 + 线程调度者”。
5.3 五个线程与派生关系
| 线程入口 |
作用 |
派生者 |
sub_1C544 @0x1C544 |
Frida 看门狗:扫 task/status 线程名、/proc/self/fd、/proc/self/maps+ELF 指纹 |
sub_1CEF8 |
sub_1B8D4 @0x1B8D4 |
TracerPid / PPid / T 态看门狗 |
sub_1B924 |
sub_26E5C @0x26E5C |
CRC32 模块特征扫描 |
sub_2701C |
sub_8CAC @0x8CAC |
ART 内部结构完整性校验 |
sub_9150 |
sub_19E0C @0x19E0C |
ADB 检测 |
sub_1B924 |
实际只观察到 3 个线程,后两个被配置表门控,在部分设备上不创建。
5.4 自解析 dlopen/dlsym
库的导入表没有 pthread_create。三个派生函数都是同一套路:运行时在栈上拼密文,用 3 字节密钥 99 A7 EC 解密出 "libc.so" 与 "pthread_create",然后 dlopen("libc.so",2) + dlsym(...),以 (attr, 0, 入口, arg) 调用。
- sub_1B924 @0x1BA5C / 0x1BA84
- sub_2701C @0x2721C
- sub_9150 @0x9548 / 0x9568
- sub_1CEF8 则
dlopen("libart.so") + dlsym 三个 _ZN3art...PrettyMethod... 变体,密钥 99 A7 A9,并把解析到的 ART 函数指针作为线程 1 的参数。
另有自实现 ELF 解析器,解析 PHDR/SHDR/.got/.dynstr/.rel. 及 DTANDROID 标签,以及自建符号注册表。
5.5 三种杀进程机制
5.5.1 机制 A:libc exit(0)
| 函数 |
exit(0) 位置 |
检测内容 |
| sub_1BFAC |
0x1C150 |
遍历 /proc/self/task/%s/status,命中 gum-js-loop 或 gmain |
| sub_1C158 |
0x1C264 |
遍历 /proc/self/fd 符号链接,命中 linjector |
| sub_1C26C |
0x1C53C |
解析 /proc/self/maps,命中 frida-agent 或 _AGENT_1.0 |
以上特征串在 sub_1C544 内用密钥 99 A7 A9 解密。
5.5.2 机制 B:内联 exit_group shellcode
sub_234E0、sub_26334、sub_269AC、sub_260B0 全部执行同一流程:
- 从
xmmword_30760 取 28 字节密文;
- 用 0x30794 起的 3 个 dword 逐字节 XOR;
mmap(0, 0x1C, PROT_READ|WRITE|EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
- 拷贝 +
__clear_cache + 调用 + munmap。
精确复刻解密算法后反汇编:
0xD2800BC8 movz x8, #94 ; AArch64 __NR_exit_group = 94
0xD4000001 svc #0
0xD65F03C0 ret
0xD503201F nop ×4
这就是符号级 Hook 全部扑空的原因。调用点包括:
sub_11FA4 统一杀点;
sub_1CEF8 解析 PrettyMethod 失败;
- 线程 1 主循环每轮调用
sub_26334;
- 线程 3 与
sub_8CAC 分别调用 sub_269AC / sub_260B0。
5.5.3 机制 C:fork + ptrace 反调试
sub_1B380 主要流程:
prctl(PR_SET_DUMPABLE, 1);
v28 = dlsym(libc, "fork")();
if (v28 < 1) return sub_11FA4();
// 父进程: 开 sub_1AB2C 线程管理子进程
// 子进程: 若 dword_49014<=0 或 getpid()==dword_49014 → exit(1)
// 否则 ptrace(PTRACE_CONT, <父pid>, 0, 信号) 循环 → 独占 ptrace 权限
配合线程 2 的判定:
sub_1AE48:读 /proc/<pid>/status 的 TracerPid:;
sub_1AB54:读 tracer 的 PPid: 是否等于自己;只有自己 fork 的子进程 ptrace 自己才放行;
sub_1B730:遍历 /proc/<pid>/task/*/stat,任何线程状态为 T 即触发杀。
5.6 关键 patch 点
| 优先级 |
偏移 |
作用 |
建议 hook |
| P0 |
0x234E0 |
内联 exit_group 执行器 |
replace 为 no-op |
| P0 |
0x26334 |
线程1 自杀倒计时 |
replace no-op |
| P0 |
0x269AC |
线程3/ART 线程 exit_group |
replace no-op |
| P0 |
0x260B0 |
ART 线程 exit_group |
replace no-op |
| P0 |
0x11FA4 |
统一杀点 |
replace no-op |
| P1 |
0x1AE48 |
查 TracerPid |
恒返回 0 |
| P1 |
0x1AB54 |
查 tracer 的 PPid |
恒返回 1 |
| P1 |
0x1B730 |
T 态任务扫描 |
恒返回 0 |
| P1 |
0x1BFAC |
task/status 线程名扫描 |
恒返回 NULL |
| P1 |
0x1C158 |
fd 符号链接扫描 |
恒返回 NULL |
| P1 |
0x1C26C |
maps + /data/local/tmp ELF 指纹 |
恒返回 0 |
| P1 |
0x1678C |
模块 CRC32 特征扫描 |
恒返回 0 |
| P2 |
0x19A58 |
ADB 线程判定 |
恒返回 0 |
| P2 |
0x8CAC |
ART 结构校验线程 |
replace no-op |
| P2 |
0x19E0C |
ADB 检测线程 |
replace no-op |
| P2 |
0x1B380 |
fork+ptrace 反调试 |
replace no-op |
依旧利用已有的 pthread_create,一次性把 P0 全部 patch 掉,再 patch P1 中直接调用 libc exit(0) 的点,P2 暂时不管。同时把所有的调用栈打印之类的全部去掉,这种 Trace 开销很大会把 agent 卡死。这个神秘问题卡了好久。
这样 libmsaoaidsec.so 的检测也过了。带调用栈打印的完整脚本已经在前文给出,若出现 Trace 过多导致环境问题,自行删除打印即可。

本文为逆向实战分析,仅供安全研究与学习交流。