Lazy loaded image
Frida学习笔记(十八):反调试与反检测对抗(下)· 招商银行三套 SDK 实战
Words 7907Read Time 20 min
2026-7-10
2026-7-10
type
Post
status
Published
date
Jul 10, 2026
slug
summary
上篇(第 17 篇)的网易云音乐易盾,最磨人的是「进程没死、agent 却被悄悄卸载」——它用 inline svc 绕过 libc 让你挂在 libc 上的 hook 集体失明、靠 SIGTRAP 自检照出插桩,还会直接 store 损坏 agent 内存、或在 MSA 构造期自爆。但它终究是反应式的:你不下钩,它未必动手。这篇换招商银行,台子更硬——它不止被动检测,还会主动反击:anti_thread 开机就起一条线程去干掉 frida 的 worker,dlopen wrapper 用 mprotect 废掉 agent。保护也不止一套,CmbShield、RootBeer、DEC 三套 SDK 并联,任意一套命中都弹窗。打法也从上篇的「nop / 静态补丁检测函数」升级到「环境伪装 / UI 兜底 / 粗暴 patch」之间的取舍。
tags
frida
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:43 AM
comment
AI 总结
上篇(第 17 篇)的网易云音乐易盾,最磨人的是「进程没死、agent 却被悄悄卸载」——它用 inline svc 绕过 libc 让你挂在 libc 上的 hook 集体失明、靠 SIGTRAP 自检照出插桩,还会直接 store 损坏 agent 内存、或在 MSA 构造期自爆。但它终究是反应式的:你不下钩,它未必动手。这篇换招商银行,台子更硬——它不止被动检测,还会主动反击:anti_thread 开机就起一条线程去干掉 frida 的 worker,dlopen wrapper 用 mprotect 废掉 agent。保护也不止一套,CmbShield、RootBeer、DEC 三套 SDK 并联,任意一套命中都弹窗。打法也从上篇的「nop / 静态补丁检测函数」升级到「环境伪装 / UI 兜底 / 粗暴 patch」之间的取舍。 标杆样本cmb.pb(招商银行 13.2.0+,base.apk + split\_arm64 + split\_xxxhdpi),Pixel 6 Pro / Android 13 / Magisk root,frida-server 16.7.19,radare2 + jadx。目标:让招行自己判定环境 clean(环境伪装路线),而非挡掉 UI。 配套脚本:本篇产出的整套招商银行三套 SDK 绕过脚本已整理打包。关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。

一、招行的三套独立反检测体系

招商银行的反检测体系由 3 套独立 SDK 并联运行,任意一套命中都会弹窗:
| SDK | 文件 | 检测内容 | 触发 UI | 覆盖 | | :-- | :-- | :-- | :-- | :-- | | CmbShield | libCmbShield.so(自研) | frida 自检测 + maps 扫描 + anti-ptrace + dl_iterate_phdr | 写信号文件 + Toast | ✅ | | RootBeer | dex(com.scottyab.rootbeer 改装)+ libtoolChecker.so | 7 项 root 检测(包名 / 文件 / exec / Build / prop) | AlertDialog「安全风险」 | ✅ | | DEC | 动态加载 dex(com.a.b.c.*,第三方 SDK) | 周期性轮询设备环境检查 | Activity「注入风险」 | ✅ |
三套互不依赖,「注入风险」和「安全风险」是两个独立弹窗,来自不同 SDK,结果也不互通。就像一栋楼装了门磁、红外、摄像头三套报警,各报各的,你拆了一套,另两套照响。解决一套另一套照样弹,是招行最磨人的地方,也是这篇要一套套拆的原因。
招行三套独立反检测体系并联,CmbShield / RootBeer / DEC 各自检测、各自弹窗(Toast「检测到风险」/ AlertDialog「安全风险」/ Activity「注入风险」),互不共享结果,任意一套命中都弹。
notion image
三套 SDK 并联

1.1 包结构与入口

12 个 dex(multidex),无第三方加固壳(没有 libjiagu/libshell/libDexHelper)——所以本篇不像上篇那样有「解密 SO」的环节,难点全在检测逻辑本身。关键 native 库:
| SO | 大小 | 角色 | | :-- | :-- | :-- | | libCmbShield.so | 894 KB | 主防护库——frida 检测 / 反 ptrace / maps 扫描 | | libCmbGuard.so | 819 KB | hook 检测专项 | | libtoolChecker.so | 5.4 KB | 开源 RootBeer 的 C 实现 | | libdobby.so | — | Dobby(自身保护性 hook) |
入口是一个替身 ApplicationAndroidManifestandroid:name="cmb.shield.AW"android:appComponentFactory="cmb.shield.AP"(Android 9+ 最早期入口)。AW 通过反射加载真正的 com.pb.main.ShellApplication。所以注入**必须用 spawn(-f)不能 attach**——早期检测在 attachBaseContext 阶段就跑了。

二、CmbShield:native 反 frida 的四条路径

libCmbShield.so 的反 frida 由 4 条独立路径并联,任意一条命中都会「破坏 frida」或「标记 inject」。两条主动、两条被动,先看速查表:
| 路径 | 主动/被动 | 触发时机 | 对策 | | :-- | :-- | :-- | :-- | | #1 anti\_thread | 主动 | run_secsdk 启动后持续 | 静态 patch run_secsdk 入口 → 整个调度器禁用 | | #2 kill\_inject | 主动 | 每次 dlopen | 静态 patch check_inject 永远返 0 | | #3 maps 扫描 | 被动 | JNI_OnLoad 内 + 后续多次 | hook fread 行级净化 maps | | #4 dl\_iterate\_phdr | 被动 | 同上(替代路径) | hook dl_iterate_phdr,包装它的回调 |
得先分清这四条的性质:#1、#2 直接破坏 frida 的运行环境,不 patch 这两条,frida 根本活不到 Java.perform;#3、#4 则是给招行喂干净数据,让它的内部判定走 clean 分支。比上篇的网易易盾更主动一截:易盾是反应式的——你不插桩它未必动手,真检到了也多是损坏 agent 内存或在构造期自爆;招行是开机就拉一条 anti_thread 主动扫描进程内所有线程,逮着 frida worker 当场就打。
下图体现 CmbShield 反 frida 的四条路径,左栏主动攻击(#1 anti\_thread 栈耗尽攻击、#2 kill\_inject 用 mprotect 破坏 agent),右栏被动检测(#3 maps 扫描、#4 dl\_iterate\_phdr 枚举),各配对策。
notion image
CmbShield 四路径

2.1 路径 #1:anti\_thread\_of\_process\_debug(主动反击)

入口在 JNI_OnLoad 内:JNI_OnLoad (0x2d0e4) → fcn.0x2e550 → run_secsdk (fcn.0x404bc)run_secsdk 通过字符串 RUN:protect_ptrace / RUN:protect_polling / RUN:env_check 定位:
protect_pollingfcn.0x56270)内部启动 **anti_thread_of_process_debug** 工作线程,主动扫描进程内所有线程,发现可疑线程(frida-agent worker)就发起破坏性操作。logcat 实测:
process[5260] 是 frida-server 主进程,tid[5290] 是 frida-agent 在目标进程内的 worker 线程。攻击手段推测是 pthread_kill / tgkill 给 worker 发信号,配合 signal handler 让其栈炸裂——主线程的 H.load() 出现 StackOverflowError 强烈暗示某种栈耗尽攻击。
**对策:直接禁用整个 run_secsdk 调度器**(两条指令):
等价于 int run_secsdk(){ return 0; }。caller 不依赖返回值也不期望栈状态变化,无副作用。为什么不只 patch protect_polling?因为 protect_ptrace 分支也会启动 ptrace 阻断,影响后续调试,整体 patch 一劳永逸。

2.2 路径 #2:kill\_inject(dlopen wrapper + mprotect 破坏 agent)

libCmbShield 通过 inline hook 替换了 libc 的 dlopen,所有 SO 加载都过 fcn.0x5f090
kill_agentfcn.0x2c840)的参数模式 (0x40, hash, 0xfff)0xfff 是 4KB 页对齐掩码,推测是 mprotect((addr & ~0xfff), page, PROT_NONE)——mprotect 是改内存页权限的系统调用,把 frida-agent 那块代码页设成「禁止执行」,CPU 下一次执行到那里就当场撞墙(SIGSEGV)。实测现象印证:frida CLI 报 Process terminated,但 adb shell pidof cmb.pb 仍有 PID——是 frida-agent 被打掉了,目标进程还活着。这正是上篇第 10 章退出诊断表里「Process terminatedpidof 还在 → agent 被损坏、进程没事」那一行。
对策:patch 调用点,让两个 checker 永远返 0
bl 换成 mov w0,#0 后,下一条 cmp w0,1; cset w8,ne; tbnz w8,0,跳过 永远走 tbnz,直接跳过 kill 分支。不 patch kill_agent 本身,是因为它可能被别的合法路径共用,patch 调用点更安全。

2.3 路径 #3:/proc/self/maps 字串扫描

fcn.0x56a2c(parse\_self\_maps):fopen("/proc/self/maps","r") + 逐 1024B fread + 行内匹配 frida / memfd:frida-agent。命中样本:
frida-server 通过 memfd 注入 agent,文件名硬编码含 frida,没法在 frida 端隐藏,只能在读取侧净化。
对策:不阻止 fread,只对 buf 做长度严格不变的行级替换
长度不变是关键:① sscanf 按字段位置解析,长度变了后续行错位;② fread 返回的字节数不能变,否则 caller 可能拿「字节数异常」当命中信号;③ \n 数量保持原样。如果 SO 改用 open + read(而非 fopen + fread),还要同时拦 read——生产脚本两条路径都覆盖作保险。

2.4 路径 #4:dl\_iterate\_phdr(绕过 maps 的二次检测)

第二条 frida 自检测,完全绕过 maps 文件。调用点在巨型函数 fcn.0xa5184(207 KB,3193 BBs)内的 0xb41ac
dl_iterate_phdr 是 linker 提供的一个接口:linker 内部维护着一份「当前进程加载了哪些 SO」的清单,你调它,它就把清单逐条念给你听。它念的是内存里的清单、不读 /proc/self/maps 文件——所以上一节净化了 maps,这条路照样能发现 frida。具体来说,linker 对每个加载的 SO 调一次回调,把元信息传入;回调里检查 dlpi_name 是否含 frida,命中就把信息写回栈帧、返回 1 停止迭代。
**对策:hook dl_iterate_phdr 的 onEnter,把 args[0] 换成我们自己的包装回调**,对 frida 相关 SO 直接跳过:
这里有个坑:NativeCallback 一旦被 GC 回收,后续 linker 调用就是野指针、进程立即崩,所以得拿 Set 把它攥住。整套替换对招行是透明的——它的枚举逻辑跑得跟没被动过手脚一样,只是再也看不到 frida 的 SO 了。

2.5 时序关键:dlopen onLeave 是 patch 黄金窗口

libCmbShield.so.init_array 只有 C++ 静态构造(fcn.0x27ef4,13 个 bl,全是编译器生成的 ctors,不做反检测)。真正的反检测在 JNI_OnLoad 内,由 ART runtime 在 dlopen 完成后才显式调用。所以 hook android_dlopen_extonLeave、拿到 libCmbShield.so 基址后立即做 4 处字节 patch,时机刚好早于 JNI_OnLoad
下图是 dlopen 加载 libCmbShield.so 的时间轴——左段是 dlopen 内部(mmap / 重定位 / .init_array 静态构造,无害),中间 onLeave 是黄金窗口、打 4 处字节 patch(run_secsdk ×2 + check_inject / find_hook_feature),右段 JNI_OnLoad 及后续检测全部失活。onLeave 正好卡在「.init_array 已跑完」和「JNI_OnLoad 还没跑」之间,是唯一安全的注入点。
notion image
dlopen onLeave 黄金窗口时序:.init\_array 静态构造无害 → onLeave 打 4 处字节 patch → JNI\_OnLoad 及后续 maps/dl\_iterate\_phdr 检测全部失活
每条 patch 都先校验原 4 字节是否匹配预期,不匹配则 skip + WARN——保证 SO 版本变更时不会误 patch 别的指令。

2.6 命中之后:延迟反馈让弹窗栈「干净」

CmbShield 检测命中后不当场弹窗,而是把信号写进文件:native 通过 H.sha(key, path) 把信号文件路径塞进静态变量 Cts.A / Cts.B,等替身 Application cmb.shield.AW 跑到 hanDeRe() 时再 dRbf() 读这两个文件,存在就 Toast 弹「检测到风险」。这一步是故意的:你 hook Toast.makeText 抓调用栈,看到的是一条干干净净的 Java 栈,完全看不到 native 检测的影子。而且 dRbf 读完立刻 file.delete()、每次启动重写,外加 ShieldService 每 30 秒(最长 150 秒)再扫一遍 Cts.B——所以有时你以为绕过了,过半分钟又弹一个。对付这种「检测点和弹窗点解耦」的设计,硬看弹窗调用栈是死路,只能用第 4.1 节的真凶追踪法:从看得见的弹窗反查到信号文件,再反查到写信号的 native 命中点。

三、RootBeer:root 检测的 7 项与 2 个易漏点

CmbShield 那四条路径压下去,frida 总算能在招行进程里活下来。但主界面还会蹦一个「安全风险」弹窗——这是第二套,RootBeer,一套改装过的开源 root 检测。调用链——isRoot() 检出 root 后一路冒泡到「安全风险」弹窗;isRoot() 并联 RootBeer.isRooted()(7 子检测的 OR)与 checkRootOld()。5 项常规(包名 / 文件 / prop / Build.TAGS)第 16 篇已覆盖,2 项最易漏拦——checkSuExists 用数组形式 exec(new String[]{"which","su"})checkForRootNativefopen 探测,朴素 hook 都会漏。全套伪装后 isRooted() 真实返 false(非 UI 兜底)。
notion image
RootBeer 调用链
7 个子检测大多是常规的(包名扫描、文件扫描、prop 检查、Build.TAGS),第 16 篇已覆盖通用绕过。这里只点两个最容易漏拦的:

3.1 易漏点一:checkSuExists 用数组形式

常规的 hook 只检查 cmds[0],而 cmds[0] = "which" 不在 su 黑名单——直接漏掉。**正确做法:对整个 String[] 数组里任一元素是 su/busybox/magisk 都拦**,Runtime.exec 的 4 个重载(String / String[] / 带 env 的两个)和 ProcessBuilder.start() 都要覆盖。

3.2 易漏点二:checkForRootNative 用 fopen 探测

libtoolChecker.soJava_com_scottyab_rootbeer_RootBeerNative_checkForRoot 内部:
常规的 native hook 只覆盖 stat / lstat / access / faccessat,会漏掉 fopen 这条路。**正确做法:把 fopen 也加入伪装层,对 root 路径返 NULL**。
应用全套伪装后,isRooted() 真实返回 false(不是 UI 兜底),Magisk 也不再弹「超级用户请求」——因为 Runtime.exec / native execve 都拦在源头,根本没真去申请 su。

四、DEC:动态加载 dex + 跨 ClassLoader hook

本以为搞定 RootBeer 就清净了,App 偏偏还弹「注入风险」——第三套 SDK 浮出水面。这一套是我整轮分析里最难揪出来的:类名混淆成 com.a.b.c.*,dex 还不在 apk 里。

4.1 真凶追踪法:从 UI 反查到检测源

我 hook 住 Instrumentation.execStartActivity,把弹窗 Activity 的启动栈抓了下来:
真凶定位为 com.a.b.c.DeviceEnvironmentCheck。这套「hook UI 显示点 → 抓 Java 调用栈 → 反查触发链 → 定位检测函数 → 找它的数据源」的流程,就是真凶追踪法
下图从看得见的弹窗顺调用栈一路反查,经 getCheckResult(伪装拦截点)追到最底层数据源 envc.push 文件。
notion image
真凶追踪反查链

4.2 跨 ClassLoader 强制 hook

com.a.b.c.DeviceEnvironmentCheck 不在 base.apk 任何 dex 里(binary grep + dexdump -l 全 0),主 factory 下 Java.use 直接失败。ClassLoader 相当于类的户口本:Java.use 默认只翻主 App 那一本,DEC 这种运行时动态加载的类登记在另一本上,自然查不到。把所有 ClassLoader 枚举一遍,找到能 findClass 它的那本,拿它建个专用 factory 就行:
动态加载 dex、插件化、热修复塞进来的类,主 Java.use 一概找不到,套路都是这一招:enumerateClassLoadersSync 枚举出登记它的那本户口本,再 ClassFactory.get 建一个专用 factory。

4.3 检测信号源:envc.push 文件

DEC 的 getCheckResult(key) 只做一件事:读 /data/user/0/cmb.pb/envc.push,解析后返回 0/1。在 getCheckResult 的 onEnter/onLeave 设全局标记 inCheckKey、让 native IO hook 只在标记非空时记录,精确捕获:
envc.push 是 16 字节 AES-128 密文(Base64 编码),每次启动随机化。它的写者绕开了整个 libc 写 API,普通 hook 一个都抓不到——这条线我追了很久也没追到底,下一节专门讲怎么对付这种对手。好在伪装根本不用解开它:直接从 Java 层最高点拦 getCheckResult 就行。

4.4 三层伪装

A+B+C 三层挂上,主界面就再没弹过窗。这里插一段我交过的学费:一开始我盯上了 SP key c4ede765...(一个 boolean),笃定它是 risk flag,顺着它追了很久,最后才发现它是 AndroidX WindowEmbedding(平板分屏)的开关,由 EmbeddingHelper 写入,跟 risk 检测八竿子打不着。教训:看到可疑的持久化数据,先 trace 它的 writer,别凭值的形态去猜用途。
三套 SDK 到此全部摆平,弹窗清零。但 DEC 那个 envc.push 写者,我始终没用普通 hook 抓到——这正好引出最后一件武器:当 hook 彻底失灵时该怎么办。

五、当 hook 失灵时:Stalker 与指令级追踪

回到上一节没解开的谜:DEC 读的 envc.push,到底是谁写的?我把能想到的 libc 写 API 全 hook 了一遍,结果全部 0 命中:
| 类别 | hook 了但 0 命中的 API | | :-- | :-- | | open 系列 | open openat openat2 creat __open_2 fopen ... | | write 系列 | write fwrite fputs pwrite writev ... | | 重命名 / 截断 | rename renameat truncate ftruncate |
我又在 doInBackground 里逐次 stat 这个文件,时序证据很清楚:它在某个异步窗口里凭空出现,却没经手任何一个 libc 写 API。到这一步我心里基本有数了——**写者多半用了 raw inline syscall(mov x8, #__NR_openat; svc #0)直接进内核,把整个 libc 函数层都绕了过去。** 这里要理解 hook 为什么失灵:正常写文件是「走 libc 这个前台办手续」,hook 了 libc 函数就能在前台把它拦下;raw syscall 是用一条 svc 汇编指令绕过前台、直接闯进内核办事,你守在前台自然一个都逮不到。这种对手,普通 hook 彻底失灵,只能祭出指令级 trace。

5.1 Stalker:函数级调用链追踪

Frida 的 Stalker 能在指定内存范围追踪所有函数调用(第 24 篇专题)。用它定位「哪个函数走完之后发生了写盘 / 崩溃」:
Stalker 是函数级(每条 call/ret 都记),适合定位「哪个函数后崩」。但如果检测逻辑内联在大函数里、不走独立函数调用(招行 fcn.0xa5184 那种 207 KB 巨型函数就是典型),Stalker 会丢线索。

5.2 指令级追踪:Stalker 不够时还有三条路

要揪出那条 svc 藏在 207 KB 巨型函数的第几行,函数级的 Stalker 就不够了,得逐指令记录。三条路可走:
  • **Frida Stalker 开 events: { exec: true }**:Stalker 本身就能逐指令记录,代价是日志爆炸、目标函数太大时性能急剧下降。适合先用第 5.1 节把范围缩到某个中等函数,再开 exec 精修。
  • Unicorn / unidbg 代执行:把 SO 搬进模拟器,补好 JNI / syscall 环境后单独「代跑」JNI_OnLoad 或可疑函数,逐指令 trace,撞上 mov x8, #__NR_openat; svc #0 当场抓现行。离线、可重放,是定位 raw syscall 最稳的路子。
  • 静态兜底:把整个 SO 反汇编(objdump -d 或 radare2 pd),grep 出所有 svc 指令,再回溯附近引用 envc.push 字符串的代码——不依赖运行时,SO 不大时最省事。
说句实在的——招行这个 envc.push 写者,我到收工都没追到底。hook 全失灵、确认是 raw syscall 之后,指令级定位就留作了未来研究。但这不影响绕过:伪装根本不用解开 envc.push,第 4.4 节 Tier A 直接从 getCheckResult 一刀切就够了。能从上层切干净的,就别陷进底层考古——这正是分层覆盖的实战价值,也是这篇反复强调「在数据源处伪装 > 逐层硬刚」的原因。

六、方法论升级:环境伪装 vs UI 兜底 vs 粗暴 patch

招行这套,最有嚼头的不是哪个偏移,是绕过路线怎么挑。同一个检测,能从三个地方下手:
| 路线 | 性质 | 优劣 | | :-- | :-- | :-- | | 粗暴 patch(patch native 检测函数返 0) | 让 App 不知道检测出了问题 | 服务器侧风控仍能从上报通道感知 | | UI 兜底(hook AlertDialog.show / startActivity) | 用户看不见弹窗 | App 内部仍判定 risk,后续登录 / 转账可能被拒 + 服务器风控 | | 环境伪装(hook 检测 API 改输入数据) | App 跑完所有检测,但读到的环境数据全干净 | App 真的判定自己处于干净环境,最彻底 |
招行的 risk 事件即使本机绕过弹窗,仍会通过 Clc.scl 上报到服务器侧风控,账号可能被服务器标记。所以环境伪装 > UI 兜底:让 App 真实判定 clean,比挡 UI 显示更有价值、也避开服务器风控。绕过研究应同步拦截上报通道。
三条绕过路线在「检测 → 判定 → 弹窗 → 上报」流水线上的拦截位置:环境伪装拦在第 ① 环(最彻底),粗暴 patch(②)与 UI 兜底(③)都漏服务器风控。
notion image
三条绕过路线对比

6.1 环境伪装的最小完备 API 覆盖

| 层 | 关键 API(处理方式) | | :-- | :-- | | Native(libc/libdl) | fopen/fread(maps 行级净化 + root 路径返 NULL)、stat/access(root 路径返 -1)、execve 系列(含 su 返 -1)、__system_property_get(敏感 prop 返伪装值)、dl_iterate_phdr(包装回调) | | Java framework | Runtime.exec 4 重载 + ProcessBuilder.start(含 su 抛 IOException,数组任一元素都查)、File.exists/canRead(root 路径返 false)、Build.TAGStest-keysrelease-keys)、PackageManager.getPackageInfo(root 包名抛 NameNotFoundException) | | DEC(专用 ClassFactory) | getCheckResult→0、Support.isSupport/isWarn/isWarnAndExit→false、CheckTask.saveInjectInfo→noop |

6.2 六条可复用方法论

  1. 环境伪装 > UI 兜底:让 App 真实判定 clean,避开服务器侧风控。
  1. 分层覆盖:native(syscall/libc)→ JNI → Java framework → 业务类,任一层漏拦都可能命中。
  1. 真凶追踪法:hook UI 显示点 → 抓 Java 栈 → 反查触发链 → 定位检测函数 → 找它的 IO/API 数据源 → 在数据源处伪装。
  1. 黄金窗口 patchandroid_dlopen_extonLeave 是 patch native SO 的最佳时机(.init_array 已跑完但 JNI_OnLoad 未跑)。
  1. 跨 ClassLoader hookJava.enumerateClassLoadersSync + Java.ClassFactory.get(loader) 突破主 factory 的限制。
  1. IO 关联 trace:在 Java 方法 onEnter 设全局标记,让 native hook 把 IO 关联到具体方法(第 4.3 节定位 envc.push 就靠它)。

七、工具链兜底:去指纹 server / 自编译 / 静态 patch

脚本层有些检测面注定打不赢——比如内存扫 LIBFRIDA 字符串、单字节读 maps。这时换一把「没指纹」的 Frida 构建。
| 触发条件 | 必须换 server | | :-- | :-- | | 加固 SO 内存扫 LIBFRIDA / gum-js-loop 字节序列 | 字符串无法运行时全部擦除 | | 加固 SO 检测 D-Bus AUTH 协议握手 | 改端口治标,去掉 AUTH 字符串才治本 | | 加固 SO 同时检测多个 libc/libart 函数的 Inline Hook | 自编译 Frida 改 trampoline 模式 |
| 项目 | 仓库 | 推荐场景 | | :-- | :-- | :-- | | strongR-frida-android | github.com/hzzheyang/strongR-frida-android | 默认首选(改名 + 改线程名 + D-Bus 关键字去除) | | Florida | github.com/Ylarod/Florida | strongR 仍被检出时(patch 更激进 + 随机化) |
client 与 server 版本必须严格同号(精确到 minor;16.5.x 只能配 16.5.x,不能配 16.4.x)。
更彻底的纵深(面向未来更激进的检测升级):① 用 apk-repack 静态 patch libCmbShield.so 重打包,不依赖 frida 真正「fix」这个 SO;② Magisk Zygisk DenyList + frida-gadget 嵌入 APK,去掉 frida-server 进程;③ patch linker 让 maps / dl_iterate_phdr 都「自然」看不到 frida-agent,最彻底的 OS 级伪装。

八、分层防御决策表:拿到任意 App 怎么试

把上下两篇浓缩成一张实战决策表,下次拿到新样本按这个层级试:
| 级别 | 方案 | 成本 | 覆盖 | 适用场景 | | :-- | :-- | :-- | :-- | :-- | | 1 | 改名 + 改端口 + spawn 模式 | 1 分钟 | 60% | 国内中小 App、未集成主流加固 | | 2 | 改名 frida-server + signalProbe 过 SIGTRAP / 线程名自检 | 即时 | 75% | libnesec 级信号 / 线程名自检(上篇第 3 节) | | 3 | 抢「干净窗口」attach + 晚挂分离检测面 | 30 分钟 | 85% | 受同意门控、检测「同意后」才全量(上篇第 5、8 节) | | 4 | r2 反查 exit + 12-NOP + 静态补丁检测函数返 0 | 1 小时 | 92% | 网易易盾、inline svc + 两层击杀(上篇全篇) | | 5 | 静态 patch 调度器 + 环境伪装 + Stalker/指令级 trace + strongR | 半天到一天 | 98% | 招行三套 SDK、主动反击型加固(本篇) |
下图为五级分层防御,成本从 L1 改名改端口(1 分钟 / 60%)递增到 L5 环境伪装 + Stalker/指令级 trace(半天到一天 / 98%),覆盖率随之爬升。
notion image
分层防御五级决策

8.1 先跑判定脚本

判定流程:① 只见 dlopen onLeavepthread_create 始终不触发 → 检测多在构造期或走 inline svc(上篇 libnesec 第 3 节、MSA 第 4 节都是这型);② 看到 pthread_create 起后台线程轮询 → ptrace 自调试 / 轮询型(上篇 MSA,第 4 节);③ 没下钩就被「干净 terminated」、libc 那圈 hook 零命中 → inline svc 绕 libc 的成熟防护,先抢干净窗口再分离检测面(上篇第 5、8 节);④ frida Process terminated 但 App 还活着、logcat 有 anti_thread → 本篇第 2 节主动反击。

8.2 两条铁律

  1. 介入时机要选对:招行这种检测在 attachBaseContext 早期就跑的,必须 spawn(frida -U -f <包名>)抢在它前面——一旦 attach 加固检测早跑完了;但上篇网易那种受隐私同意门控、检测「同意后」才全量启动的,反而要在同意页 -N attach 先占位。先用第 8.1 节的 quick_probe 摸清是哪种。
  1. 从外到内绕:主动反击 / 同步检测 > Hook 完整性校验 > 异步检测线程。颠倒顺序则外层先把你杀掉,里层脚本来不及生效。

九、总结

上下两篇用两个真实标杆,覆盖了「对抗 App 自我保护」的几乎全部档次:
  • 上篇(网易云音乐易盾):进程不死、agent 被悄悄卸载。SIGTRAP 自检 signalProbe、线程名扫描配 patched frida-server、inline svc 绕 libc、MSA 12-NOP(r2 反查 exit)、同意页 -N 抢干净窗口、六个单变量实验重建两层击杀模型(MSA 构造自爆 + agent 内存损坏)、慢层静态补丁、快层只钩非安全库、退出诊断表、能力排除法、跨设备敏感性。
  • 下篇(招商银行):主动反击 + 三套 SDK 并联。anti_thread 主动破坏 worker 线程、dlopen wrapper mprotect 杀 agent、dl_iterate_phdr 二次检测、RootBeer 两个易漏点、DEC 跨 ClassLoader、环境伪装方法论、Stalker 与指令级追踪兜底。
两篇底子是同一套:抓黄金窗口下钩、分清被动检测和主动反击、能伪装环境就别只挡 UI。难度和打法一路往上爬。拿到新样本,先用第 8.1 节的 quick_probe.js 摸清级别,再对号入座。

📦 获取本篇脚本

本篇产出的整套招商银行三套 SDK 绕过脚本已整理打包:
  1. 关注本公众号
  1. 私信回复关键词「脚本」
回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。
上一篇
【转载】如何正确看待商业项目:创业者、企业家与投资人的第一套判断框架
下一篇
Frida学习笔记(十七):反调试与反检测对抗(上)· 网易云音乐易盾实战

Comments
Loading...