type
Post
status
Published
date
Jul 10, 2026
slug
summary
解决一件事:App 在 Root 设备上启动即闪退时,让它继续跑。聚焦设备本地的 Root 痕迹检测——文件、Shell 命令、系统属性、包名、Native 层 syscall。
tags
frida
逆向工程
反爬绕过
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:41 AM
comment
AI 总结
解决一件事:App 在 Root 设备上启动即闪退时,让它继续跑。聚焦设备本地的 Root 痕迹检测——文件、Shell 命令、系统属性、包名、Native 层 syscall。 配套脚本:本篇产出的整套 Root 检测绕过脚本(Java 四维 Hook + Native 层 libc Hook + 酷狗 / 招行双实战绕过 + RUN.md 真机验证清单,复制即用)已整理打包。关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。
一、检测强度分级
- 一般商业 App:Java 层基础文件 + 包名检测,最简单的脚本即可。
- 游戏App:防 GameGuardian 修改内存、防模拟器刷号,Root + 模拟器双检为主。
- 金融App:合规要求驱动,普遍接入同盾、盾山、梆梆等第三方 SDK,Java + Native 双层 + 后台持续检测。

检测强度分级模型
边界:本文范围是设备本地检测——App 进程内的判定逻辑都可以靠 Hook 改返回值。但 2025 年 1 月 SafetyNet 关停后,银行/支付/政务类 App 主流已切换到 Play Integrity API + 硬件密钥认证 (Key Attestation) 双重把关: - Play Integrity 的 verdict(MEETS_BASIC / DEVICE / STRONG_INTEGRITY)是 Google 服务端签名后返回的 JWT,App 把它转发给业务后端校验。Hook App 本地无效——签名密钥不在 App 里。 - Hardware Key Attestation 通过KeyStore.attestKey()拿一条 TEE/StrongBox 出厂时刻入的硬件证书链,业务后端能据此判出"设备 bootloader 是否解锁过 / 是否换过 ROM"。几乎无法软件绕过,业内目前只能靠PlayIntegrityFixMagisk 模块伪造 BASIC/DEVICE verdict,STRONG 拿不到。 也就是说,本文对付的是自家测试 / CTF / 非 Google Play 渠道的商业 App。强制 STRONG 的银行 App 不靠"多 Hook 几个点"——靠 Magisk 模块 + 干净硬件指纹 + ZygiskNext 隐藏链。
二、Root 检测的四个维度
Root 痕迹分四类——
su 等特征文件、which su 类 Shell 命令、ro.build.tags=test-keys 等系统属性、Magisk Manager 这类包名。下图给出每类的关键证据和对应 Hook 点,第 2.1 节 – 第 2.4 节 一一对应展开:
Root 检测四维全景
同一个 App 经常两层都做一遍:Java 层走
File.exists / Runtime.exec;Native 层走 libc 的 access / stat / fopen,触发时机比 Java 层早。下图按 App 启动时间轴把两层并排画出来,每个检测点旁标了 Hook 入口:
Java 层与 Native 层检测架构
时间轴上 Native 检测在
JNI_OnLoad / SO 静态构造里就跑(T2),等 Java 检测线程起来(T3+)再 Hook 已经晚了——所以 Hook 必须赶在 native 检测之前就位,而 native 又早于 Java,唯一稳妥的办法是默认上 spawn(详见 第 4.2 节)。拿到样本第一步要决定从哪层下手——按症状选策略,免得用银行 App 那套去打一个 Java 层小工具。下图把"症状 → 入手层"拆成三条决策路径:

Hook 从哪一层下手的三条决策路径
简而言之:Java 层闪退或弹 Toast,先 第 2.1 节 - 第 2.4 节;只有 native 痕迹(栈干净 / 信号文件 / Process 异常)才下到 第 3 章;分不清就 spawn 起来双层都挂,等命中再砍多余的 Hook。
2.1 文件存在性检测
Root 操作会留下特征文件——
su 二进制、Magisk 管理文件、SuperSU 的 APK 等。App 通过 File.exists() 检查这些路径。常见路径清单(备查,实战 Hook 用下面的通配):
把上面这份清单写成精确匹配的数组挡不住 Android 10+ 的
$PATH 扫描——酷狗 SystemUtils.Q6() 直接读 System.getenv("PATH") 切分查 su,Pixel 上 PATH 含 /system_ext/bin,Magisk systemless 装的 su 就在那(详见 第 5 章)。Hook 改用通配判定,命中 /su 结尾或 magisk 子串就一律 false:File.exists() 无参,路径在 File 对象内部,用 this.getAbsolutePath() 取。通配只锁 /su$ 和 magisk——扩到 sys 会命中 /system/...、/sys/... 几乎所有系统路径,扩到 root / security 也会撞上一堆正常的库名和缓存目录,招行 dRbf 那种反复 new File(...).exists() 探业务文件的逻辑首当其冲。2.2 Shell 命令检测
which su——如果 su 在 PATH 中存在会返回路径,否则返回空。或者直接执行 su -c id,成功返回 uid=0 即视为已 Root。Hook
Runtime.exec(),拦截含危险关键字的命令:替换成
echo 而非返 null:Runtime.exec 返回 Process,调用方一般会读它的输出流,null 触发 NPE 会让 App 崩。echo 输出空、退出码 0,等价于 su 不存在。2.3 系统属性检测
ro.build.tags:debug build 是 test-keys,正式版是 release-keys,Root 的自定义 ROM 多半是 test-keys。读取有两条路径,都要拦:
SystemProperties.get("ro.build.tags")
android.os.Build.TAGS静态字段直读
比如酷狗 FireEye SDK 的
DeviceInfo.isDeviceRooted 走的是后者(String tags = Build.TAGS;),单 Hook SystemProperties.get 拦不住。API 30+ 反射可能失败:Android 11 把Field.modifiers列入 hidden API 黑名单,普通 App 拿不到modifiers字段去清final标志位;部分字段在 JIT/AOT 优化后会被 inline 成常量。两个补救路径: - 从源头:Build 字段的值大多来自SystemProperties.get("ro.product.model")等属性读取,把ro.build.tags/ro.product.model/ro.product.brand等键加进路径 A 的fakeValues,比改字段更稳 - 拦调用方:直接 Hook 那个读了Build.TAGS做判定的方法,从结果端绕过(参见 第 4.3 节 输出口法) 实测 Pixel 6 Pro / Android 13 / Magisk + Zygisk 上反射能成功(酷狗案例 RUN.md 记录这段反射写入的 Build 字段——TAGS/TYPE——写完读回一致),但未 root 设备 / 严格 hidden API 检查的 App 仍可能静默失败。判别方式:写完后立刻console.log(Build.TAGS.value)读回看,相等就成。
2.4 包名检测
PackageManager 检查 Magisk Manager(com.topjohnwu.magisk)、SuperSU(eu.chainfire.supersu)等是否安装:三、Native 层文件检测
SDK 在 Native 层直接调 libc 的
access() / stat() / open() / fopen(),绕过 Java 层。Android 11+ Bionic 主推 *at 系列(openat / faccessat / fstatat,POSIX.1-2008 atfd 风格),新 NDK 编译的 SO 大量走 *at——只 Hook access/stat 会漏。锁定 Frida 16;Frida 17 起Module.findExportByName已 deprecated,改用Process.getModuleByName("libc.so").getExportByName(...)。
少数高强度加固 SDK 会用 svc 0 直接走 syscall,整层绕开 libc Hook —— 它们不调access/stat/open*,而是用 ARM64 的mov x8, #SYS_openat; svc 0内联汇编直接发起系统调用,完全不经过 libc。Interceptor.attach("libc.so", "openat")在它们身上不会触发任何回调。这是个老技术(Windows 侧的 direct syscall、ARM64 侧的 svc 直调都存在多年),不是某年的新拐点;真正大量用它的也只是个别方案(见本节末)。 挡 svc 0 的两条路径: - Stalker 指令级追踪:用Stalker.follow()扫描每条指令,在svc指令前插桩。性能开销大,精度最高。详见第 24 篇。 - Interruptor 库:基于 Stalker 封装的 syscall 级 Hook,API 类似Interceptor但作用在 svc 上。GitHubFrenchYeti/interruptor,几行代码就能 strace 一个进程的全部 syscall 并按号过滤。 判定目标是否用了 svc 0 的最快方法:Hook 完上面这段后,如果 App 仍然准确判出"已 Root"且能定位到某个 SO 模块,在那个 SO 里r2 -A搜svc #0指令——命中数十处且围绕 Root 路径字符串,就是它:$ r2 -A libCheck.so [0x00012340]> /ad svc #0 # 搜 svc 0 指令 0x00018a40 d4000001 svc 0 # ← 旁边能看到 mov x8, #56 (SYS_openat) 0x00018c20 d4000001 svc 0 # ← 附近字符串 "/system/bin/su" 0x00019100 d4000001 svc 0 ... 28 hits in libCheck.so像招行libCmbShield.so这种只有零星几条 svc 0、主要靠 plt\_hook 自家 libc 的情况,上面脚本能拦住绝大部分。只有同盾、网易易盾的部分版本会大量用 svc 0 把 libc Hook 整层绕掉。
四、对抗升级与方法论
第 2、3 章 的代码模板抄了就能用,但跑之前先看三件事:
- 装了 Shamiko / Zygisk Assistant 的设备,root 痕迹在 mount namespace 层面已被挖掉,Java 层 Hook 大半冗余(第 4.1 节)
- 检测线程在
JNI_OnLoad就跑完,attach 来不及——默认上 spawn,有必要再拦线程(第 4.2 节)
- 加固 SDK 不逐点绕,找业务侧调用的输出方法改返回值(第 4.3 节)
第 5、6 章 两个实战分别落在这套方法论的"够用"和"边界"两侧。
4.1 反直觉:装了 Shamiko,第 2、3 章 大部分 Hook 反而冗余
如果你写了一套 200 行脚本,跑在自己设备上发现"啥都不用 Hook,App 也不闪退"——不是脚本生效了,是 Shamiko(或 Zygisk Assistant 对 KernelSU/APatch 同理)在更早的 zygote 注入阶段,直接把 root 痕迹从 App 进程的 mount namespace 里整体挖掉了。
具体做了什么:
bind mount用空目录覆盖/data/adb、/sbin/magisk等路径,App 真的看不到这些文件——File.exists()返回 false 是真返回 false,不是 Hook 出来的
- 把
com.topjohnwu.magisk从该 App 视图的 PackageManager 里隐藏(基于<queries>的精确点查也拿不到)
- 重置
/proc/self/status、/proc/self/mountinfo等暴露源
- 阻断 Zygisk API 在 DenyList 名单 App 内的暴露
影响:
- 第 2.1、2.2、2.4、3 章 在 Shamiko 设备上是重复保险——root 痕迹已在 mount namespace 层被挖掉,不写也大概率 OK。但这只在装了 Shamiko 时成立:酷狗(第 5 章)是纯 Magisk、没装 Shamiko 的环境,
/system_ext/bin/su这个 symlink 明摆在那照样命中——这种环境下第 2、3 章的 Hook 一个都不能省
- 第 2.3 节(系统属性 / Build 字段)不在重复保险之列——Shamiko 不改 build 属性,FireEye 直读
Build.TAGS还得靠 第 2.3 节 路径 B 反射拦
- 高强度 SDK 会上反 Shamiko 手段:行为指纹(
/proc/self/status与/proc/self/maps横向交叉验证)、timing 异常(open root 路径 vs 普通路径耗时差)、特定 mount 点扫描(找 Shamiko 自己的痕迹)。有矛就有盾——这些手段跟随 Shamiko 出现就陆续在上,程度不一。第 2、3 章 脚本真正起作用的就是拦这些残留
实际顺序:先把 root 方案配齐(Magisk/KernelSU + Zygisk Assistant + 目标 App 加入 DenyList),再上 Frida 处理残留。反过来做的话会写一堆没用的脚本。
4.2 多线程检测的时序问题
高强度方案在 App 启动时起后台线程循环检测。Hook 装得再全,只要检测线程在 Hook 之前先跑了一遍并记下结论,后面 Hook 就晚了。
下图把 App 启动过程切成 5 个时间点:Attach 最早只能在 T3 之后接入,T2 的
JNI_OnLoad Native 检测已经成为既定事实。
Spawn vs Attach 时序窗口
策略一:用 Spawn 模式,确保绕过脚本在 App 任何代码执行前就生效。
策略二:直接阻止检测线程的启动:
这套关键字会误杀业务线程,先把
return 改成只打日志跑一遍,确认哪些是检测线程再开拦截。招行案例 6 个检测都在 JNI_OnLoad 启动,attach 模式根本来不及——所有需要绕过强检测的脚本默认上 spawn。4.3 第三方安全 SDK 的应对思路
同盾、盾山、梆梆这些 SDK 内部混淆且加固,逐点分析不现实。直接绕过它的结果输出函数——SDK 最终总会通过某个方法返回
boolean 或风险评分,改这一个返回值就够。
加固 SDK 输出口四步法 · 主路径 4 步 + 两类找错回溯
定位输出方法的四步:
Step 1:定位 SDK 边界类——jadx 打开 APK,
Ctrl+Shift+F 输入包名前缀(如 com.tongdun、com.bangcle),找名字最朴素的那个(通常叫 SecurityClient / RiskDetector / SafeSDK)。Step 2:找出输出方法——边界类里搜返回
boolean 或返回 int 的 public 方法:Step 3:找业务侧调用点——
Find Usage 跳到调用位置,确认"返回什么值 = 安全":Step 4:一行 Frida Hook 收工:
找错输出口的表现:脚本加载成功、log 显示 Hook 生效,但 App 照样闪退——SDK 里还有别的输出函数。回 Step 2 把候选列出来对照。两种典型的"找错":
- 酷狗(第 5 章):grep 出来的第一个候选 FireEye
isDeviceRooted是"迷雾"——它确实在跑,但返 false(漏检),真凶是另一个SystemUtils.O6。靠 第 5 章 Toast 反查才定位到。
- 招行(第 6 章):命中点根本不在调用栈里——native 命中只写信号文件,Java
onCreate才File.exists()弹窗,Toast 栈干净到查不到 native 痕迹。这种延迟反馈得搜Cts.A/Cts.B常量的 caller,或 hook 所有new File().exists()看哪些路径被访问。
五、酷狗音乐
目标:
com.kugou.android v20649。环境:Pixel 6 Pro / Android 13 / Magisk。症状:启动后短暂弹 Toast「当前处于 root 环境,请注意账号安全」。不阻塞登录,但 SDK 会上报到服务器风控。下面只讲检测点和绕过——反查过程(grep / Toast.show 抽栈 / jadx 跳栈)就是第 4.3 节四步法 + 第 5 章 Toast 反查的应用,不展开。
5.1 检测点:SystemUtils.O6 = P6 || Q6
KgUserLoginAndRegActivity.onCreate 里调一次 SystemUtils.O6(),命中就弹 Toast:两层:
- P6 —— 固定 5 个目录拼
"su"后扫,exists() && canExecute()同时成立才算
- Q6 —— 读
System.getenv("PATH")切分,每个目录里查su
Pixel 6 Pro 的
PATH 包含 /system_ext/bin,Magisk systemless 装的 symlink 就在那:→ P6 五个固定目录都不命中(systemless 把老路径抹掉了),Q6 通过
$PATH 扫到 /system_ext/bin/su,O6 = true,Toast 弹。反查路径速记:先 grep 文案(注意strings默认只输出 ASCII,中文 UTF-8 字节会被切掉,要strings -e S或grep -aoF),找到classes16.dex后用第 5 章 Toast 反查抽栈定位到onCreate那一行。完整过程在酷狗的无损分析文档里,本节不展开。
5.2 两层防御 + ART AOT inline
A 拦根因(File.exists),B 拦业务输出口(O6/P6/Q6),不再叠 Toast hook(只遮盖症状不阻止检测):
关键论断:无论 B 是否生效,A 都把 toast 兜下来。这是本案例脚本最值得记的工程结论:
| B hook 状态 | O6/P6/Q6 原代码 | 内部 File.exists 调用 | toast 结果 |
| :-- | :-- | :-- | :-- |
| ✓ 生效(带 deopt 触发器) | 不跑(被 frida 替换) | 不发生 | 不弹(B 拦掉) |
| ✗ 被 ART AOT inline 跳过 | 照跑(展开在 onCreate 里) | 发生 → A 全返 false | 不弹(A 让
rooted=false) |实测两种状态:
- 装了 deopt 触发器:
[A]触发20次 [B]触发1次(A 少 14 条因为 P6/Q6 直接被 B 短路了)
- 没装 deopt 触发器:
[A]触发34次 [B]触发0次(B 形同虚设,A 是主力)
两种状态下 toast 都不弹。A 是这套脚本的硬兜底——File 是 Java 标准库,ART 不 inline,hook 必中;B 是锦上添花——
P6/Q6 万一改了名 A 也照样把底层 File.exists 截住。为什么 B 默认不工作 → ART AOT inline:O6/P6/Q6都是private static,体积小,是典型的 inline 候选。ART 在装包时把它们的字节码直接展开到KgUserLoginAndRegActivity.onCreate的 native 机器码里——Frida 的.implementation挂在 ArtMethod 派发入口,inline 后 CPU 不经过 ArtMethod,hook 永远不响。加Activity.onCreateno-op hook 让 Frida 触发该方法 deopt 后,ART 丢掉 AOT 机器码降级到解释器执行,解释器逐条解释字节码看到invoke-static O6才老实走 ArtMethod 派发,B 才复活。

ART AOT inline 让 Frida hook 失效 · dex / AOT 后机器码 / deopt 后解释器 三层状态对比
通用经验:hook 业务侧的 small-private-static 检测方法装上没报错但收不到事件,第一反应是 ART AOT inline——给持有它的调用方(一般是
Activity.onCreate)挂个 no-op hook 触发 deopt 即可。六、招商银行:3 套独立 SDK 并联
目标:
cmb.pb 13.2.0。症状:启动后两个不同弹窗——CmbShield Toast「检测到您当前设备可能存在风险」+ 后续 AlertDialog「安全风险」+ Activity「注入风险」。3 套独立反检测 SDK 并联,任意一套命中都弹窗,必须全部伪装:
| SDK | 文件 | 检测内容 | 触发 UI |
| :-- | :-- | :-- | :-- |
| CmbShield |
libCmbShield.so 自研 | frida 自检测 + maps 扫描 + anti-ptrace + dl\_iterate\_phdr + root 文件扫描 | 写信号文件 → AW.hanDeRe → Toast Cts.RTM |
| RootBeer + SecBaseFunc | dex(com.scottyab.rootbeer 改装)+ libtoolChecker.so | 7 项 root 检测(包名 / File.exists / Runtime.exec which su / Build / sysprop) | CheckRootTask → AlertDialog「安全风险」 |
| DeviceEnvironmentCheck | 动态加载 dex(com.a.b.c.*,不在 base.apk 内) | 周期轮询读 envc.push 文件 0/1 | CheckTask.onPostExecute → DECMsgDialog Activity「注入风险」 |非加固——12 个 dex 是标准 multidex,没有
libjiagu/libshell/libDexHelper,常规脱壳工具链在这没东西脱。开发把火力放在 3 套并联 + native 检测 + 动态加载 dex 上。下面三节逐套讲检测点和绕过。6.1 CmbShield 自研防护层
入口三件套(Manifest):
cmb.shield.AP → cmb.shield.AW.attachBaseContext → H.load() → System.loadLibrary("CmbShield") → JNI_OnLoad。JNI\_OnLoad 里启动一组 native 检测异步线程:
| 检测点 | 做什么 | 命中后 |
| :-- | :-- | :-- |
| parse\_self\_maps | 读
/proc/self/maps 扫 frida-agent / linjector / gum / re.frida 字符串 | 写信号文件 Cts.A |
| anti\_ptrace(run\_secsdk) | ptrace(PTRACE_TRACEME) 自己 ptrace 自己,让真调试器 attach 失败 | — |
| TracerPid 双进程 | fork app_process 子进程读自身 /proc/<ppid>/status 的 TracerPid,绕过自 ptrace 后仍能暴露调试器 | 写信号文件 |
| dl\_iterate\_phdr | linker 内部 SO 枚举(绕过 maps),过滤 dlpi_name 含 frida 的 entry | 写信号文件 |
| check\_sd\_root\_files | 扫 /system/bin/su 等 root 路径 | 写信号文件 Cts.B |
| kill\_inject | hook android_dlopen_ext,对 frida-agent.so 主动破坏 | 直接让 frida-agent 注入失败 |延迟反馈机制:native 命中后不直接弹窗,而是通过
H.sha(key, path) JNI 接口把一个信号文件路径写到静态变量 Cts.A / Cts.B。Java 端 super.onCreate() 才到弹窗判定中心 hanDeRe():三处精巧设计:
- Toast 调用栈完全干净——栈里只有
hanDeRe → dRbf → File.exists → Toast,看不到任何 native 检测痕迹。第 5 章酷狗案例的 Toast 反查工作流在这里失效
- 删后即焚——
dRbf触发后立即delete(),逆向者adb shell进去看的时候文件早没了
- 服务器侧风控上报——
Clc.scl异步 POST 到Cts.G,绕过本机但绕不过账号标记
绕过主路径 —— 掐弹窗判定中心 + 断上报:
Tier 1 灭弹窗,Tier 2 灭上报。Toast 没了不等于绕过——同一份内部状态会被服务器读到,必须用 mitmproxy 抓
Cts.G 域名确认上报真的被断了。TracerPid 双进程检测的对抗:第 18 篇fgets拦 TracerPid 行只覆盖当前进程;子进程要么 hookfork/posix_spawn之后在子进程也注入脚本(frida 17 的 gating 模式支持),要么 native 层 hook 子进程的open("/proc/.../status")。详见第 18 篇反调试。
6.2 RootBeer 7 个子检测 + 两个易漏点
RootBeer 是
com.scottyab 的开源 root 检测库,招行用它的改装版(外加 SecBaseFunc.checkRootOld File.exists 兜底)。7 个子检测:
| 方法 | 检测原理 |
| :-- | :-- |
|
detectRootManagementApps | PackageManager.getPackageInfo 扫 magisk/supersu 包名 |
| checkForBinary("su" / "busybox" / "magisk") | File.exists 扫 16 条 root 路径 |
| checkForDangerousProps | __system_property_get 读 ro.debuggable / ro.secure 等 |
| checkForRWPaths | File.canWrite("/system", "/data", ...) |
| detectTestKeys | Build.TAGS.equals("test-keys") |
| **checkSuExists | Runtime.exec(new String[]{"which","su"}) ★ 易漏 |
| checkForRootNative | JNI → libtoolChecker.so → fopen(path,"r")** ★ 易漏 |前 5 项第 2.1 节 / 第 2.3 节 / 第 2.4 节的 hook 直接覆盖。两个易漏点必须特别处理:
#### 6.2.1
checkSuExists 用数组形式部分按"只检查
cmds[0]"实现的 hook 会漏——cmds[0] = "which" 不在 su 黑名单。正确做法:对整个 String[] 数组里任一元素含 su / busybox / magisk 都拦。本文第 2.2 节的 hook 是先拼接数组成字符串再 indexOf,所以已经覆盖;写新 hook 时要留意这个坑。#### 6.2.2
checkForRootNative 用 fopen 而非 access/stat朴素 native hook(只覆盖
stat/lstat/access/faccessat)漏。第 3 章主代码块的 ④ fopen 拦截已经覆盖——**这是为什么第 3 章必须把 fopen 加进 isRootPath 通配**。完整伪装验证(应用全套 hook 后跑
research2.js):Magisk 也不再弹"超级用户请求"——因为 Runtime.exec / native execve 都拦在源头,没真去申请 su。
6.3 DeviceEnvironmentCheck:动态加载第三方 SDK
完整解决 RootBeer 后 App 仍弹「注入风险」——第三套独立 SDK:
包名混淆为
com.a.b.c.*,dex 不在 base.apk 任何位置(binary grep + dexdump -l plain 全 0 命中),是运行时动态加载的。ClassLoader 突破:
Java.use('com.a.b.c.DeviceEnvironmentCheck') 在主 factory 失败,要枚举所有 loader 找能 findClass 的:检测机制:
AsyncTask.doInBackground 周期循环(LOOP_TIME 间隔,最长 MAX_TIME)调 getCheckResult(key),返回 0/1。getCheckResult 内部**只做一件事——读 /data/user/0/cmb.pb/envc.push 文件**:envc.push 是 25 字节文件(24 char Base64 +
\n),解码后 16 字节正好 1 个 AES-128 block 密文,每次启动重新生成(pm clear 后),AES key 是随机 session key:envc.push 未解之谜:hook 了所有 libc 写 API(open/openat/openat2/creat/__open_2/fopen/write/fwrite/fputs/pwrite/rename/truncate等近 20 个),全部 0 命中。但时序证据显示 envc.push 在某次doInBackgroundEXIT 之后突然存在了。推测写者用了 raw inline syscall(mov x8, #__NR_openat; svc #0)绕过 libc 函数层,定位需 Stalker 或指令级 trace,留作扩展。
绕过路径:不需要破解 AES,从 Java 层最高点拦:
A+B+C 三层组合后,App 主界面完全没有任何风险弹窗,Magisk 也不再弹"超级用户请求被拒绝"。
6.4 环境伪装 vs UI 兜底
三套 SDK 都伪装完之后,回头看整篇思路。脚本采用的是环境伪装——hook 检测 API 改输入数据,让 App 跑完所有检测但读到的环境数据全是干净的,App 真的判定自己处于干净环境。
环境伪装的最小完备 API 覆盖清单——招行实战验证:
Native 层(libc / libdl):
Java 层:
DEC SDK(专用 ClassFactory):
第 2、3 章主代码块已经覆盖大部分。招行特有的两条扩展全文已经吸收:① 第 2.2 节
Runtime.exec 拼接后 indexOf 已覆盖数组任一元素;② 第 3 章 Native 主代码块的 ④ 已含 fopen。6.5 启动方式 + 必备配置
spawn 启动——检测在
attachBaseContext 就跑,attach 晚一拍全晚:设备配置:
- Zygisk + DenyList 把
cmb.pb加进去——避免 RootBeer 早早命中
- mitmproxy 抓
Cts.G域名验证 Tier 2 上报真的被断了
- 长时间运行还要处理周期检测:CmbShield 的
ShieldService(30s 一次)和 DEC 的CheckTask(LOOP_TIME间隔轮询,最长MAX_TIME)
总结
四个维度的 Hook 点速查(已同步 第 2、3 章 全部点):
| 维度 | 关键证据 | 着力点 |
| :-- | :-- | :-- |
| 文件 |
su / Magisk / KernelSU / SuperSU 文件 | Java:File.exists + canExecute + canRead 三件套;Native:access/faccessat + stat/lstat/fstatat + open/openat + fopen 共 10+ 函数。路径用 endsWith("/su") 通配 |
| Shell | which su / su -c id / mount | Runtime.exec 所有重载拦截 |
| 系统属性 | ro.build.tags=test-keys / ro.debuggable=1 / verified boot | SystemProperties.get 双重载 + Build.TAGS / Build.TYPE 静态字段反射 |
| 包名 | Magisk Manager / SuperSU / Xposed 包名 | getPackageInfo × 2 + getInstalledPackages × 2 + getInstalledApplications × 2,共 6 入口 |实战要点:
- Native 先于 Java + spawn——
JNI_OnLoad早于 Java 检测线程,招行 6 个检测都在这里启动,attach 来不及。每段 Hook 用try/catch兜,打失败.message方便下次定位。
- 路径通配 > 白名单——
endsWith("/su") || indexOf("magisk")比 20 条路径列表稳。酷狗Q6()扫$PATH,命中/system_ext/bin/su(Magisk systemless 常见位置),老清单挡不住。
- 加固 SDK 找输出口——第 4.3 节 四步法。grep 第一个候选可能是迷雾(酷狗 FireEye),用 第 5 章 Toast / Dialog 反查验证。Hook 装上没报错但收不到事件,多半是 ART AOT inline——给持有它的 Activity 方法挂个 no-op hook 触发 deopt(详见 第 5.2 节)。
- Toast 栈干净 ≠ 没检测——招行延迟反馈:native 命中只写信号文件,Java
onCreate才File.exists弹窗。搜常量 / 信号文件路径的 caller(第 6.3 节 ①),或 hook 全部new File().exists()看哪些路径被访问。
📦 获取本篇脚本
本篇整套 Root 检测绕过脚本(Java 四维 Hook + Native 层 libc Hook + 酷狗 / 招行双实战绕过,9 个脚本 + RUN.md 真机验证清单,复制即用)已打包:
- 关注本公众号
- 私信回复关键词「脚本」
回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。
- Author:24th
- URL:https://24th.top/article/399e5b08-46db-8032-a5db-ff2b8fdb50ad
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!









