本文记录了使用 GLM-5.3-FLASH 辅助完成的一次银行 APP 加固绕过完整逆向实战:某银行移动客户端采用第三方企业级加固壳保护,启动即闪退,处于"无从下手"状态。借助 GLM-5.3-FLASH——当前最强逆向大模型,无道德审查、无使用限制、价格低廉,从定位检测线程、Stalker 指令级追踪退出路径、逐层上找检测源头,到最终精准定位触发检测的 Frida memfd: 特征并实现多层防御绕过,完整呈现银行级加固壳的反调试对抗全过程。
整个过程深度运用 Frida Stalker 动态代码重写引擎进行指令级追踪——这是定位 SIGSEGV 退出路径、逐层回溯检测链路的核心手段,也是本文区别于普通 hook 绕过文章的深度所在。GLM-5.3-FLASH 在其中展现了远超通用大模型的逆向能力:无道德审查使其能直接生成反检测、内存修改、线程拦截等"敏感"脚本;无使用限制让其可以长时间稳定输出完整 Stalker transform 回调;低廉价格支撑了十余轮迭代式脚本生成而不必担心成本。它能根据自然语言需求直接生成可运行的 Frida 脚本,会在脚本中添加详细注释和使用说明,甚至在遇到性能问题时主动给出优化建议(如 30ms 批量打印、指令去重)。每一步只需描述"要做什么",GLM-5.3-FLASH 就能生成"怎么做"的完整代码,大幅降低了逆向工程的门槛。
银行类移动应用因涉及资金安全与合规要求,普遍采用第三方企业级加固壳(如梆梆、爱加密、360 加固、通付盾等)对 DEX、SO 进行保护。这类加固壳的核心防御手段之一就是反调试与反 Frida 检测:一旦检测到调试器或 Frida 注入,立即杀死进程,使逆向者无从下手。本文分析的即是一例典型银行客户端——其加固壳在 JNI_OnLoad 阶段与异步线程中布设了多重检测,启动即闪退。绕过这类加固是银行 APP 安全测试、风控分析、接口逆向的前置条件。
Frida 是一个跨平台动态插桩工具,它将 JavaScript 引擎(V8)注入目标进程,允许在运行时修改函数行为、读写内存、跟踪执行流。在 Android 逆向中,Frida 常用于:
关键点:SO 文件被 dlopen 加载后,Android 会自动调用其导出的 JNI_OnLoad 函数。许多加固方案(如乐固、梆梆、爱加密)将反调试逻辑放在 JNI_OnLoad 中,因为它在 Application.onCreate 之前执行,此时 Java 层尚未完全初始化,调试者很难介入。
Frida Stalker 通过动态代码重写实现指令级跟踪:它将目标线程的每条指令复制到一块"影子内存"中,在每条指令后插入回调通知,然后让线程跳转到影子内存执行。这样可以实时输出执行的每条指令地址和助记符,是定位退出路径的利器。但 Stalker 会带来较大性能开销,对循环函数需配合去重和批量输出使用。
深入理解 Stalker 的几个关键点(本文深度依赖):
transform 回调以基本块为单位:Stalker 不是逐条指令调用 transform,而是在每个基本块(basic block,以分支/跳转指令结尾的线性指令序列)首次执行时调用一次 transform。在 transform 内部用 iterator.next() 遍历块内每条指令,iterator.keep() 保留原指令,可在此插入 iterator.putCallout() 自定义回调。
影子内存与代码缓存:重写后的代码存放在 Stalker 分配的可执行内存中,原内存只读不受影响。这意味着即使原 SO 的代码段做了完整性校验,Stalker 依然能跟踪——因为执行流已跳到影子内存。
跨模块指令过滤:Stalker 会跟踪线程内所有代码,包括 ART、libc、linker 等。实战中必须用 instruction.address 与目标模块基址/大小比较,只输出目标 SO 内的指令,否则会被海量无关指令刷屏。本文所有 Stalker 脚本均采用此过滤策略。
性能优化:批量输出 + 去重:console.log 走 Frida IPC 通道,逐条打印会严重拖慢甚至卡死进程。本文采用 30ms 定时批量输出(缓冲到数组,定时 join 一次打印)+ 可选指令去重(同一地址只打印首次)的双重优化,是 Stalker 实战的标配手法。
unfollow 与 flush:跟踪结束必须调用 Stalker.unfollow(tid) 停止重写,并调用 Stalker.flush() 冲刷缓冲区中剩余的指令事件,否则尾部指令会丢失——这对定位"退出前最后几条指令"至关重要。
直接使用 Frida 启动目标银行应用(包名 com.tzb.mobilehub,下文统称"目标银行 APP"),发现进程启动后不久便自动退出,但不是立即退出——这暗示加固壳的检测逻辑运行在某个异步线程中,而非 JNI_OnLoad 同步执行。这种"延迟退出"是银行级加固壳的典型特征:故意错开启动瞬间,增加逆向者定位难度。
启动 Frida 服务并附加目标进程:
程序延迟退出而非立即退出,说明检测在线程中执行。判断思路:既然是线程检测,可以尝试用 Frida 拦截 pthread_create,阻止目标模块创建线程,观察程序是否还会退出。这引出了下一步——杀线程。
pthread_create 是创建线程的核心函数,原型为:
第三个参数 start_routine 是线程函数地址。通过 Interceptor.replace 替换 pthread_create,检查 start_routine 是否来自目标模块,若是则直接返回 0(假装创建成功),即可阻止线程执行。
帮我生成 frida 拦截函数,当线程函数来自 libDexHelper.so 模块时:打印日志(模块名、函数偏移、参数)并返回 0(模创建成功,实际阻止线程执行),其他模块创建的函数正常放行
向 GLM-5.3-FLASH 描述需求后,它直接生成了完整的脚本,包含详细注释和使用方式说明。


即使阻止了 libDexHelper.so 创建的所有线程,程序仍然退出了。这说明退出逻辑不仅在线程中,可能还在 SO 加载阶段就触发了。需要进一步定位是哪个 SO、在什么时机导致退出。下一步将 Hook android_dlopen_ext 监控所有 SO 加载。
Android 系统加载 SO 使用 android_dlopen_ext(比标准 dlopen 多一个 extinfo 参数)。Hook 此函数可以监控所有 SO 的加载时机,并在目标 SO 加载完成后立即安装进一步 Hook。接下来让GLM-5.3-FLASH帮我们验证确定下一步操作。
在脚本添加函数,hook android_dlopen_ext 函数,监控所有 so 文件加载,当 so 被加载时打印提示


加载 libDexHelper.so 成功,但之后没有其他 SO 加载程序就退出了。推测退出可能发生在 libDexHelper.so 的 JNI_OnLoad 函数中——因为 dlopen 返回后 Android 会自动调用 JNI_OnLoad,此时还没有后续 SO 加载。
既然 SO 加载后没有其他 SO 被加载就退出了,最大嫌疑就是 JNI_OnLoad。下一步需要在 dlopen 的 onLeave 中 Hook JNI_OnLoad,观察它的开始和结束时机。
JNI_OnLoad 是 SO 被 dlopen 加载后自动调用的初始化函数,原型为:
它在 dlopen 返回之前就被调用(因为 dlopen 内部会调用 SO 的构造函数和 JNI_OnLoad)。因此必须在 android_dlopen_ext 的 onEnter 或更早安装 Hook,才能捕获 JNI_OnLoad 的入口。但实践中在 onLeave 中 Hook 也能捕获后续多次调用的情况。
修改 hookAndroidDlopenExt 函数,在 dlopen 的 onLeave 中实际hook JNI_OnLoad,查看 JNI_OnLoad 加载的开始和结束

打印了 JNI_OnLoad >>> 开始执行,但没有打印 <<< 执行结束,进程就终止了。这确认了退出发生在 JNI_OnLoad 执行过程中。

确认退出发生在 JNI_OnLoad 内部,但不知道具体在哪条指令退出。需要用 Stalker 实时跟踪 JNI_OnLoad 执行的每条指令,找到导致退出的确切位置。
Stalker.follow(tid, {transform}) 会对指定线程的代码进行动态重写,transform 回调中可以遍历每条指令并插入自定义逻辑。为避免被 ART/libc 的指令刷屏,通常只输出目标模块内的指令。
为什么用 Stalker 而非普通 Hook? 普通的 Interceptor.attach 只能在函数入口/出口观察,无法看到函数内部哪条指令触发了退出。而银行加固壳常用"制造 SIGSEGV"的方式退出(清零 sp/lr 后跳转未映射地址),这种退出不走 exit/abort,普通 Hook 根本拦不到。只有 Stalker 能逐条指令追踪,捕获到退出前的最后几条指令(如 mov sp, #0; mov lr, #0; br xN),从而回溯定位检测点。这是本文使用 Stalker 的根本原因,也是指令级追踪的深度价值所在。
修改 hookAndroidDlopenExt 函数,通过 Stalker 实时捕获并输出 JNI_OnLoad 函数执行过程中的每条机器指令

Stalker 成功捕获到退出前的最后几条指令:

关键指令序列:
这是故意制造 SIGSEGV 退出:将 sp 和 lr 清零后跳转到一个未映射地址,触发段错误杀死进程,而非调用 exit/abort。
计算跳转目标地址:w11 = 0x10dc,movk w11, #0xb6a2, lsl #16 → x11 = 0xb6a210dc,但这是绝对地址。需要用 IDA 查看该位置对应的 SO 内偏移。

得到偏移 0x10dc,使用 IDA 跳转查看:

观察代码逻辑,确定是 sub_4B2E0 返回 1 时退出程序。

找到了导致退出的函数 sub_4B2E0(偏移 0x4B2E0):当它返回 1 时触发退出路径。下一步直接 Hook 这个函数,将返回值替换为 0,即可绕过检测。
Interceptor.attach 的 onLeave 回调中,retval.replace(0) 可以修改函数返回值。这是最简单直接的绕过方式——让检测函数始终返回"未检测到"的值。
在现有脚本的基础上创建新的脚本,将使用 Stalker 跟踪 JNI_OnLoad 执行过程替换为 hook 偏移量为 0x4B2E0 的目标函数,将返回值替换为 0,其他功能保留

成功过掉检测,程序正常运行不再退出!

绕过成功!但此时只是"盲绕"——我们并不知道 sub_4B2E0 到底检测到了 Frida 的什么特征才返回 1。银行加固壳会随版本更新迭代检测逻辑,"盲绕"随时可能失效,且无法沉淀为可复用的对抗方案。因此需要进入阶段2:用 Stalker 深度逆向分析检测链路,找到具体是哪个函数、匹配到什么特征导致返回 1,实现精准绕过。
阶段1 已实现"盲绕"——程序不再退出。但银行加固壳的检测逻辑会随版本更新而变化,"盲绕"随时可能失效。阶段2 的目标是逆向分析检测链路,搞清楚加固壳到底扫描了 Frida 的什么特征、在哪条指令返回 1,从而实现精准绕过而非"碰运气"。这一阶段将更深度地运用 Stalker 追踪元凶线程与检测主函数的完整执行流,是整篇文章技术含量的核心所在。
libDexHelper.so 创建了多个线程,通过逐个/分批阻止线程创建,观察程序是否退出,可以用排除法确定元凶线程。同时 Hook abort/exit/kill 等终止函数,程序被杀时打印调用者,辅助确认。
通过之前patch_dexhelper_0x4b2e0.js返回的值可以得到 libDexHelper.so 创建了哪些线程:
接下来分析哪个线程检测到 Frida。
通过之前脚本执行结果判断,libDexHelper.so 创建了函数偏移为 0x4e9d8, 0x4b614, 0x557c0, 0x57668, 0x5af74 的几个线程。在现有脚本的基础上创建新的脚本,修改 hookPthreadCreate 函数,以协助我判断,哪个线程中止了程序。
GLM-5.3-FLASH 直接生成了脚本,还告诉了脚本的使用建议——通过修改 ALLOW_OFFSETS 逐批测试。

根据 GLM-5.3-FLASH 生成的脚本使用建议,修改 ALLOW_OFFSETS 的值为待测试线程,经过逐个排除测试,最终确定是 **0x4b614 **线程终止了程序。
元凶线程确定为 0x4b614。下一步需要用 Stalker 追踪这个线程函数的执行过程,分析它是如何杀死程序的。
帮我生成新的 js 文件,hook 偏移量为 0x4B2E0 的目标函数,将返回值替换为 0,并杀掉偏移量为 0x4e9d8, 0x557c0, 0x57668, 0x5af74 几个线程。并使用 Stalker 跟踪偏移量为 0x4b614 函数执行过程中的每条机器指令

找到了线程退出的最后指令块在偏移 0x2dfa4 附近,同样是 mov sp, #0; mov lr, #0; br x12 的 SIGSEGV 退出模式。需要分析 0x2dfa4 这个函数的参数来计算跳转地址,逐层上找真正的检测源头。
分析最后一个代码块 0x2dfa4,发现它有 3 个参数。通过 Hook 打印参数值,可以计算出跳转目标地址,再用 IDA 查看目标位置。
帮我生成新的 js 文件,hook 偏移量为 0x4B2E0 的目标函数,将返回值替换为 0,并打印 sub_2DFA4 的参数 a1, a2, a3

计算 sub_2DFA4 的值得到下一步跳转地址:

跳转到 0x97c,使用 IDA 跳转查看,发现该位置直接跳转退出——这个函数就是用来退出的,没有分析意义。

分析 sub_2DFA4 上面一个函数 0x2DDA0,经分析也是退出用的:

继续往上找,来到 0x4bbd4,使用 IDA 跳转继续分析函数:

发现关键逻辑:sub_50450 返回 1 时,会跳往退出函数导致程序退出。让 sub_50450 不等于 1,程序即可正常运行。下一步需要分析 sub_50450 内部,找到是哪个位置使它返回 1。
帮我创建新的脚本,hook 偏移量为 0x4B2E0 的目标函数,将返回值替换为 0,并使用 Stalker 跟踪偏移量为 0x50450 函数执行过程中的每条机器指令

分析返回值发现:sub_50450 被调用了两次,第一次正常返回,第二次返回 1 导致程序退出。结合 IDA 和打印的汇编代码,需要进一步分析程序到底是在 sub_50450 内部的哪个位置检测到 Frida 并返回 1。
通过前述步骤已知 sub_50450 第二次调用返回 1 导致退出。现在需要深入 sub_50450 内部,找到具体是哪个子函数匹配到了什么特征。
结合 IDA 反编译分析 sub_50450 的内部逻辑,发现它是一个内存映射扫描函数,核心流程如下:
通过动态追踪确认,sub_50450 检测到以下 3 个 Frida 特征中的任意一个即返回 1:
在 /proc/self/maps 中,Frida 16.x 会创建如下映射:
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。