Android 反调试检测与绕过(VMP 场景)
场景
对 VMP 加固应用做动态分析(trace 解释器、dump 内存)时,Frida/调试器一挂上应用就闪退或卡死。VMP 壳把反调试与反 Frida 检测内嵌在解释器/守护进程里,必须先过检测才能继续 trace 路线(04-vmp-dump-trace-recovery)。
输入信号
- Frida attach/spawn 后进程立即退出,或界面卡死
- 启动时有第二个进程(守护进程)互相 ptrace
- logcat 出现
tracerpid/frida/detected字样 - 应用在无 hook 时正常,一旦有
/proc/self/maps出现 frida 相关映射就退出 - 反编译/反汇编看到大量
open("/proc/self/status")、ptrace、clock_gettime调用
常见检测与绕过对照表
| 检测手段 | 原理 | 绕过 |
|---|---|---|
| TracerPid | 读 /proc/self/status 的 TracerPid: 字段,非 0 = 被调试 | hook open/read 伪造文件内容;或 spawn 早注入让检测点发生在注入之后 |
| ptrace 双进程守护 | 守护进程 ptrace 主进程,检测自身/对方是否被附加 | 先杀/停守护进程(见下);patch ptrace 返回值 |
/proc/self/maps Frida 检测 | 扫描 maps 找 frida-agent/gadget 字符串 | frida-server 改名/换端口(见下);maps hook 过滤 |
| frida 默认端口扫描 | 连 27042 端口探测 frida-server | frida-server -l 0.0.0.0:xxxxx 换端口 |
| 时间差检测 | clock_gettime(CLOCK_MONOTONIC) 两次采样差过大 | hook clock_gettime 固定返回值;硬断不停检测区 |
| syscall 直读 | 绕过 libc 直接 syscall 读状态 | hook 内核层(需 root + kprobe),或模拟器/定制 ROM 绕开 |
| 反 unidbg/模拟器 | 检测 syscall 指纹、时间单调性 | unidbg 的 setTime/hook 处理;成本较高,优先真机 |
绕过一:frida-server 换端口 + spawn 早注入
bash
# 1. 设备上启动 frida-server 指定端口
adb shell "su -c '/data/local/tmp/frida-server -l 0.0.0.0:28999 &'"
# 2. 桌面端连接指定端口 + spawn 模式(早注入, 先于壳的检测点)
# frida 命令行:
frida -H 127.0.0.1:28999 -f com.target.app --no-pause -l bypass.js
# Python:
# device = frida.get_device_manager().add_remote_device("127.0.0.1:28999")
# session = device.attach("com.target.app", realm="native")spawn 模式下 Frida 在 Application 创建前注入,能覆盖 attachBaseContext 阶段的检测。
绕过二:TracerPid 伪造
javascript
// bypass.js: hook open/read 伪造 /proc/self/status 的 TracerPid
var openPtr = Module.findExportByName("libc.so", "open")
var readPtr = Module.findExportByName("libc.so", "read")
var fake = "TracerPid:\t0".replace("0", "0")
Interceptor.attach(openPtr, {
onEnter: function (args) {
this.path = args[0].readCString()
},
onLeave: function (ret) {
if (this.path && this.path.indexOf("status") !== -1 && this.path.indexOf("self") !== -1) {
this.hit = true
}
}
})
Interceptor.attach(readPtr, {
onEnter: function (args) {
this.hit = this.hit || false
},
onLeave: function (ret) {
// 简化: 直接替换 read 缓冲中的 TracerPid 行为 0
if (this.hit) {
var buf = args[1]
var data = buf.readUtf8String(Math.min(ret.toInt32(), 512))
if (data && data.indexOf("TracerPid:") !== -1) {
data = data.replace(/TracerPid:\s*\d+/, "TracerPid:\t0")
buf.writeUtf8String(data)
}
this.hit = false
}
}
})更稳的做法是只 hook fopen/fgets(应用通常用 ifstream/fgets 读 status),按实际实现调整。
绕过三:ptrace 双进程守护
流程:
1. 启动应用 → 观察进程树: 主进程 + 守护进程(通常低权限/不同 uid)
2. adb shell ps -A | grep <pkg> 确认守护进程 pid
3. 方案 A: su -c "kill -9 <守护pid>" 后立即 attach 主进程
(守护死后主进程的 ptrace 保护消失; 部分壳主进程也会自检守护存活, 需同时 patch)
4. 方案 B: Frida 早注入后 hook ptrace 调用, 伪造失败返回:
Interceptor.attach(Module.findExportByName("libc.so", "ptrace"), {
onEnter: function (args) { this.req = args[0].toInt32() },
onLeave: function (ret) {
if (this.req === 16 /* PTRACE_ATTACH */ || this.req === 0) {
ret.replace(-1) // 让壳以为 ptrace 失败
}
}
})
5. 方案 C: 定制 ROM (Youpk 思路) 在系统层对目标进程豁免 ptrace 检测绕过四:时间检测与时钟固定
javascript
// hook clock_gettime 固定单调时钟
Interceptor.attach(Module.findExportByName("libc.so", "clock_gettime"), {
onEnter: function (args) {
this.clk = args[0].toInt32()
},
onLeave: function (ret) {
if (this.clk === 1 /* CLOCK_MONOTONIC */) {
// 写固定时间戳, 消除单步/断点延迟特征
}
}
})攻击链
spawn 早注入 + frida-server 换端口 → 加载 bypass 脚本
→ TracerPid/时间/maps 检测逐个确认并绕过 (logcat + 是否闪退判断)
→ 双进程守护 → 杀守护或 hook ptrace
→ 稳定挂载后 → 转 04-vmp-dump-trace-recovery 的 trace 路线
→ 全程记录: 哪个检测点被哪个 bypass 覆盖, 剩余未覆盖检测清单MCP 工具映射
AI Agent 可调用以下 MCP 工具自动完成或加速上述攻击链步骤:
| 攻击链步骤 | MCP 工具 | 说明 |
|---|---|---|
| 部署并启动 frida-server | android_frida_ensure_server | 在 MuMu/root 设备部署 frida-server(可指定 arch/version) |
| 运行 bypass 脚本 | android_frida_run_script | 对目标进程跑一次性 Frida JS,收集 send 消息(TracerPid/时间 bypass 脚本) |
| 解密/脱壳 recipe 附带反调试观察 | android_crypto_unpack_recipe | 含反调试相关模板,观察 dlopen/RegisterNatives/解密证据 |
| 强制停止重试 | android_force_stop | 绕过失败后清理进程状态重新开始 |
证据与验证闭环
- 记录包名、进程树(主进程+守护进程 pid/uid)、frida-server 版本与端口、Android 版本。
- 每个检测点绕过绑定:检测代码位置(so 偏移/API 名)、bypass 脚本、绕过前后行为差异(闪退 vs 稳定运行)。
- 未被绕过的检测点必须列为"剩余风险",不得默认全过。
- 保存 logcat、Frida 输出、脚本版本到
exports/android/,可重放复现。