找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖
Claude、GPT 海外模型 API 接入Claude skills 从入门到精通 吴恩达亲授 AI Agent 核心技能2026 瞪哥公务员考试全攻略 行测申论一站式系统备考
Agent 文心智能蒸馏模型实战 90G 课程智泊 AI 大模型训练营 基于 LangChain 的 RAG 与提示工程实战构建企业级 AI 大脑:大模型微调与 RAG / Agent 全栈实战

5771

积分

0

好友

728

主题
发表于 3 天前 | 查看: 0| 回复: 0

一、问题定位: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,看起来像动态跳转,但结合打印出来的调用栈就能轻松定位。

JNI_OnLoad 中的动态跳转与汇编代码

可以考虑借助 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。

sub_436bb8 附近反汇编与高亮调用

0x37144 → 0x38024(未命中)
   ↓
0x37a3c → 0x37a40 → 0x385ec

到 0x385ec 的时候很明显发现只有 2000 多条 Trace,但结尾也不是类似 kill 指令那种。

这里明显还动了 pthread_create,看起来是将 pthread_create 拿到之后进行了调用,后面也做了一些 fd 之类的检测。

sub_436bb8 内部对 pthread_create 的调用与检测逻辑

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

sub_440a3c 中 sys_openat 与 /proc/self/maps 相关代码

三、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 位对应检测名映射如下:

Frida 相关检测位图

  • 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 最终绕过方案

BYPASS 开关与对应目标/手段/说明表

关键取舍:

  • 优先替换核心原语,而不是逐个堵 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 全部执行同一流程:

    1. 从 xmmword_30760 取 28 字节密文;
    2. 用 0x30794 起的 3 个 dword 逐字节 XOR;
    3. mmap(0, 0x1C, PROT_READ|WRITE|EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    4. 拷贝 + __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 过多导致环境问题,自行删除打印即可。

    女性动漫角色特写配图

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




    上一篇:Go 并发排查:接口都返回了,goroutine 为什么还卡在 channel 上?
    下一篇:WaterPlum假面试投毒:3万台主机失陷,千万美元加密货币被盗
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-9-25 04:05 , Processed in 2.523120 second(s), 46 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表