App 抓包与检测对抗:从装证书到过加固的完整链路

为什么 Web 抓包一套流程走天下,App 抓包却要 root、要过检测、要上 Frida? 答案一句话:Web 环境信任由浏览器管理,App 环境信任由开发者代码管理。开发者不想让你抓,你就要跟它的检测机制打一场攻防战。

本文仅面向授权环境下的安全测试与学习,介绍检测机制的构成与绕过思路,不构成对任何未授权系统的利用指导。

一、为什么 App 抓包难:三个检测维度

抓包的本质是让流量经过你、并让你看到明文。Web 场景下这两件事都由系统说了算(代理设置 + 证书信任链),所以 Burp 一套标准流程直接拿下。App 场景下,开发者可以通过代码主动对抗,全部落在这三个维度:

维度 检测什么 对抗什么
信任层 证书是否被替换 SSL Pinning(不认系统 CA,只认硬编码指纹)
网络层 流量是否走了代理 代理检测、TCP 直连绕过系统代理
设备层 环境是否被”动过” root 检测、Frida 检测、调试检测、模拟器检测

三个检测维度与连环套关系

三关全过,才回到 Web 那种”装证书就能抓”的状态。 而最要命的是这三关互相耦合:过 Pinning 通常需要 Frida → 上 Frida 触发 Frida 检测 → 对抗 Frida 检测又往往依赖 root 环境(装系统证书、注入工具)→ root 又触发 root 检测。所以 App 抓包是一个连环套,不是单点问题。

二、基础抓包:Android 证书体系与代理

1. 证书装哪:系统 CA vs 用户 CA

Android 7.0(API 24)起,App 默认只信任系统 CA,不信任用户安装的 CA。这意味着:

  • 你把 Burp 证书装到”用户证书”区 → 大多数 App 照样报证书错误
  • 解决方案有三条路:
    1. root 后装进系统证书区(/system/etc/security/cacerts/)——最通用
    2. 改 APK 的 network_security_config.xml,允许信任用户 CA——免 root 但破坏签名
    3. Frida 层面绕过证书校验(见第五节)

2. 代理检测与透明代理

App 可以检测系统代理设置(Settings.Global.HTTP_PROXY),检测到就直接不走代理或拒绝联网。绕过思路是透明代理:

透明代理链路:App 感知不到代理存在

1
App(不知道有代理)──TCP 直连──▶ iptables 重定向──▶ Burp 透明模式──▶ 服务器

原理:用 iptables 把 App 进程的出站流量重定向到 Burp 的透明代理端口,App 根本感知不到代理存在。工具:Burp 的 Invisible Proxy 模式 + iptables REDIRECT,或 ProxyDroid 这类工具。这条路的本质是在网络层截流,不依赖 App 配合。

3. 小程序为什么等于 Web 抓包

小程序跑在微信容器里,网络栈是 WebView 内核——证书信任和代理都遵循系统规则,不搞特殊。所以 Burp 一套标准流程直接抓,和抓网页没区别。这也是为什么移动端渗透测试里,小程序是最好打的突破口。

三、加固原理:App 为什么”抓不动”

1. 加固是什么

加固(加壳)的核心动作:把原始的 dex/so 加密隐藏起来,运行时再解密还原执行。目的是防逆向、防二次打包、防业务逻辑被脱端重写。京东安全小课堂把 Android 加壳技术按强度分为 4 代:

代际 技术 说明
第 1 代 整体 dex 加壳 把 dex 加密藏进壳,运行时解密
第 2 代 防调试防 Dump 加反调试,防止内存 dump 出 dex
第 3 代 方法体抽离 方法代码抽走,运行时回填
第 4 代 VMP 加壳 / so 加壳 字节码转虚拟机指令,so 层也加固

关键认知:无论哪一代,运行时解密这一步躲不掉——所以即使是 VMP 壳,理论上也能在内存中修复、dump 出完整 dex。这就是”脱壳”的原理:脱壳不是破解加密,而是等它自己解密时把内存快照拿走。

2. 加固厂商格局

类型 厂商
免费 360 加固、百度加固、腾讯乐固、网易易盾、阿里聚安全
商业 爱加密(双 VMP:dex+so)、梆梆安全、顶象、娜迦

其中网易易盾、梆梆、360 属于把检测做到”又早又深”的顶级壳:签名校验早于 Java 桥可用、检测逻辑 native 化 + 内联 + 自实现加密,免 root 的常规绕过手段基本全部堵死(详见第五节)。

四、检测机制全貌:安全 SDK 在检测什么

这是本文的核心。从 Frida 学习笔记两篇实战(招行三套 SDK、网易易盾)和加固原理中,可以提炼出一张完整的检测机制地图:

1. Root 检测(RootBeer 系)

开源 RootBeer 的 7 项子检测,按调用方式分三类:

类型 检测手段 易漏点
包名扫描 枚举已安装应用,找 Superuser.apk / Magisk 管理器 无
文件探测 stat/access/fopen 探测 /system/xbin/su 等 用 fopen 探测的路径,hook stat 系列会漏
命令执行 Runtime.exec("which su") 数组形式 exec(new String[]{"which","su"}),只查 cmds[0] 会漏
系统属性 Build.TAGS 含 test-keys、ro.secure、ro.debuggable 无

2. Frida 检测(四条独立路径)

招行 CmbShield 的反 Frida 由 4 条路径并联,两主动两被动:

路径 性质 原理 对策
#1 anti_thread 主动 开机拉线程扫描进程内所有线程,发现 frida worker 用栈耗尽攻击破坏 静态 patch 调度器
#2 kill_inject 主动 hook dlopen,加载含 frida 特征库时用 mprotect 把 agent 内存页设成不可执行 patch 检测点返 0
#3 maps 扫描 被动 读 /proc/self/maps,匹配 frida / memfd:frida-agent hook fread 行级净化
#4 dl_iterate_phdr 被动 遍历 linker 的 solist 链表(不读文件,绕过 maps 净化) hook 回调包装器

另加常见检测面:

  • 线程名扫描:/proc/<pid>/task/*/comm 找 gum-js-loop、gmain
  • D-Bus 协议握手:连接 27042 端口发 AUTH 字符串
  • 内存指纹:扫描进程内存找 LIBFRIDA 字节序列

3. 调试检测

  • ptrace 反调试:检测 /proc/self/status 的 TracerPid,非 0 即被调试;或自己 ptrace 自己(anti-ptrace)
  • SIGTRAP 自检:给自己发信号,看 handler 是否被 Frida 拦截
  • 硬件断点检测:检查调试寄存器

4. 签名校验(免 Root 的生死线)

1
取签名证书(getPackageInfo)→ 算 MD5 → 与写死的期望值比对 → 不一致直接 exit 自杀

关键特性:又早(+0.2s 跑完,早于 Frida 的 Java 桥可用)、又 native(不经标准库)、又 inline(比较内联在自实现加密代码里)——窗口期内没有干净拦截点。这就是网易易盾这类顶级壳”免 Root 实际绕不过”的原因。

5. 主动反击型检测

比被动检测更狠:不止”发现你”,而是直接破坏你的工具:

  • anti_thread:发信号让 frida worker 栈炸裂
  • kill_inject:mprotect 把 agent 代码页设为不可执行 → 下次执行直接 SIGSEGV
  • 检测与弹窗解耦:检测命中写信号文件,等主线程跑到某处再读文件弹窗——hook 弹窗调用栈看到的是一条干净 Java 栈,完全追不到 native 检测点
  • 周期轮询:30~150 秒再扫一遍,绕过了过一会儿又弹

6. 检测架构:多 SDK 并联

招行是典型:CmbShield(自研 native)+ RootBeer(开源改装)+ DEC(动态加载 dex 的第三方)三套独立并联,各自检测、各自弹窗、互不共享结果——拆一套,另两套照响。这是大厂 App 的标准姿势。

7. Pinning 与代理检测:流量层的两道闸

上面六类检测的是”环境”,还有两道闸直接卡流量本身:

  • SSL Pinning(证书固定):App 不信任系统 CA 列表,而是硬编码服务器证书的指纹/公钥,甚至把校验下沉到 native(conscrypt 回调、自实现校验)。Burp 的假证书即使被系统信任,App 也不认——这是”抓不到包”最常见的直接原因
  • 代理检测:App 读系统代理设置(Settings.Global.HTTP_PROXY)判断流量是否被转发;更硬核的会检测代理特征(如 HTTP 代理的响应头/握手行为)。检测到就直接不走代理,或干脆拒绝联网

这两道闸的绕过方法见第五节的第 1、6 小节,与上面的环境类检测形成完整攻防闭环。

检测机制与绕过方法对照总图

五、对抗手段:逐检测绕过手册

拿到一个陌生 App,先按下面的层级从便宜到贵试,再对着检测类型逐个拆:

总览:五级分层

级别 方案 成本 覆盖率 适用
L1 改名 + 改端口 + spawn 模式 1 分钟 60% 中小 App、无加固
L2 改名 frida-server + signalProbe 过 SIGTRAP/线程名自检 即时 75% 信号/线程名级检测
L3 抢”干净窗口”attach + 晚挂分离检测面 30 分钟 85% 受隐私门控、同意后才全量检测
L4 r2 反查 exit + 12-NOP + 静态补丁检测函数返 0 1 小时 92% 网易易盾级(静态补丁)
L5 静态 patch 调度器 + 环境伪装 + Stalker/指令级 trace + strongR 半天~一天 98% 招行三 SDK、主动反击型

1. 绕过 SSL Pinning

Pinning 的本质是”App 只认自己写死的证书/公钥”。从通用到硬核:

① 通用 Frida 脚本(Java 层)——覆盖绝大多数 App:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function () {
// 1. 遍历所有 TrustManager 实现,把 checkServerTrusted 置空
Java.enumerateLoadedClasses({
onMatch: function (cls) {
if (cls.indexOf('TrustManager') !== -1) {
var Clazz = Java.use(cls);
if (Clazz.checkServerTrusted) {
Clazz.checkServerTrusted.implementation = function () { return; };
}
}
},
onComplete: function () {}
});
// 2. OkHttp CertificatePinner(市面上最常用)
try {
var pinner = Java.use('okhttp3.CertificatePinner');
pinner.check.overload('java.lang.String', 'java.util.List').implementation = function () { return; };
} catch (e) {}
});

② 一行命令(objection):

1
2
objection -g com.target.app explore
android sslpinning disable

③ WebView 场景:WebViewClient.onReceivedSslError 里调用 handler.proceed()。

④ native 层 Pinning:Java hook 不生效时,App 可能在 native 校验(conscrypt/自实现)。hook SSL_CTX_set_custom_verify / SSL_set_verify,把回调返回值改成 X509_V_OK。注意边界:conscrypt 这类系统库可以 hook;App 自研加固库的 .text 段别碰(改字节会触发 inline hook 完整性检测)。

2. 绕过 Root 检测

检测手段 绕过方法
包名扫描(Superuser/Magisk 管理器) Magisk Zygisk DenyList + Shamiko:对目标 App 隐藏 root 存在
文件探测(fopen su 路径) Shamiko 统一隐藏;或 Frida hook stat/fopen,对 su 相关路径返回不存在
exec("which su")(数组形式) Frida hook Runtime.exec,检查参数数组,命中 which/su 返回空流
系统属性(test-keys / ro.secure) Magisk 自带隐藏;或 hook System.getProperty 清洗

注意:root 检测优先用 Magisk 生态(DenyList/Shamiko)解决,而不是 Frida hook——hook 本身会触发 Frida 检测,等于拆东墙补西墙。环境干净永远是第一优先级。

3. 绕过 Frida 检测(重点)

针对招行/易盾两条实战路线的逐项打法:

检测手段 绕过方法
maps 扫描(/proc/self/maps 找 frida/memfd) Frida hook fread:逐行读时把含 frida 特征的行替换成无害行
dl_iterate_phdr(遍历 solist) hook 回调包装器,跳过含 frida 的 so
线程名(gum-js-loop/gmain) 改用 frida-gadget 注入(无这些线程);或自编译改线程名
D-Bus 握手(27042 端口) 用 strongR-frida-android(去 D-Bus 关键字的 frida-server 构建)
内存指纹(LIBFRIDA 字节序列) 改 frida 源码字符串后自编译
SIGTRAP 自检 signalProbe 脚本模拟信号处理,让自检认为”handler 未被拦截”
静态检测函数 黄金窗口 patch(见下)
anti_thread / kill_inject(主动攻击) 静态 patch 调度器:定位起检测线程/包装 dlopen 的调度函数,mov w0,#0; ret 直接废掉

黄金窗口 patch 具体操作:

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onLeave: function () {
var lib = Process.findModuleByName("防护库.so");
if (!lib) return;
// 静态分析定位到的检测函数地址,窗口内写入 mov w0,#0; ret (arm64)
Memory.patchCode(lib.base.add(0xXXXX), 8, function (p) {
p.writeByteArray([0x00, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6]);
});
}
});

为什么这个窗口最干净:android_dlopen_ext 的 onLeave 时机,.init_array 静态构造已跑完(无害)、JNI_OnLoad 还没跑(反检测逻辑在它里面)——patch 完,防护库从加载起就全失活。

4. 绕过调试检测

  • TracerPid 检测:frida-server 的 ptrace attach 会留下 TracerPid → 改用 ZygiskFrida(Zygote fork 注入,全程无 ptrace),或 spawn 模式配合处理
  • anti-ptrace:patch 掉 ptrace(PTRACE_TRACEME) 调用
  • SIGTRAP 自检:同上面的 signalProbe

5. 绕过签名校验:最好不去碰它

  • 免 Root 注入 = 改 APK = 重签 = 签名变了。对易盾/梆梆/360 这种”又早(+0.2s)、又 native、又 inline”的校验,时间竞赛必输——窗口期内没有干净拦截点
  • 正确姿势:Root 下注原版 APK,不重签。签名还是原值,签名墙根本不出现。ZygiskFrida 在 Zygote fork 极早期注入,签名校验还没机会跑
  • 只有软目标(无签名校验或校验晚)才考虑:hook getPackageInfo 返回原签名值

6. 绕过代理检测

场景 方案
App 检测系统代理设置 透明代理:不设系统代理,用 iptables 在网络层截流
App 无视代理、TCP 直连 同上,网络层强制接管,App 感知不到

命令骨架(root 环境):

1
2
3
# 把目标 App UID 的出站 443 流量重定向到本机 Burp 透明代理端口
iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner <app_uid> --dport 443 -j REDIRECT --to-port 8080
# Burp 开启 Invisible Proxy(监听 8080)

7. 环境伪装:在数据源处做手脚

每个检测最终都读某个”数据源”:文件、系统属性、进程列表、maps。环境伪装 = 在数据源处返回干净数据,让 App 跑完所有检测但全部判定通过:

  • 文件层:Magisk DenyList / Shamiko 隐藏 root 相关路径
  • 属性层:hook System.getProperty 清洗 test-keys
  • 进程层:隐藏 Magisk / Frida 进程
  • 设备层:伪造代理状态、MAC、IMEI 等设备指纹

为什么环境伪装是首选而非 hook 弹窗(UI 兜底):伪装让 App 真实判定 clean,服务器侧风控事件不会上报;UI 兜底只挡住弹窗,risk 照常上报,服务器可能直接风控封号。方法论一句话:在数据源处伪装 > 逐层硬刚 > UI 兜底。

8. 工具链兜底

  • strongR-frida-android:去指纹 frida-server(应对 D-Bus/内存指纹检测)
  • ZygiskFrida:Zygote 极早期注入,无 ptrace、无 memfd、无独立进程(应对进程/TracerPid 检测)
  • frida-gadget 嵌入原版 + DenyList:无 server 进程(应对进程扫描)
  • unidbg:把 SO 搬进模拟器离线跑签名算法——彻底脱离真机反制(终局方案)

六、进阶技巧:硬仗怎么打

1. 真凶追踪法(检测点与弹窗解耦时)

大厂 App 检测命中后不是立刻弹窗,而是写信号文件、等主线程跑到某处再读文件弹窗——hook 弹窗看到的是一条干净 Java 栈,追不到 native 检测点。正确流程:

1
2
3
4
5
6
hook UI 显示点(Instrumentation.execStartActivity)
→ 抓弹窗 Activity 的启动栈
→ 反查触发链
→ 定位检测类
→ 找它的 IO/数据源
→ 在数据源处伪装

2. 跨 ClassLoader hook(动态加载的检测类)

加固 App 的检测类常是运行时动态加载的 dex(如招行 DEC 的 com.a.b.c.*),Java.use 默认找不到。解法:

1
2
3
4
5
6
7
8
Java.enumerateClassLoadersSync().forEach(function (loader) {
try {
loader.findClass('com.a.b.c.Detect');
var factory = Java.ClassFactory.get(loader); // 找到后建专用 factory
var Detect = factory.use('com.a.b.c.Detect');
// 用 factory.use 的对象做 hook
} catch (e) {}
});

3. 快层反调试:只 hook 非安全库

网易易盾的”快层”认 Java/ART Hook(蹦床/入口篡改),检测到就 inline-svc 静默杀线程。对策:不碰 Java Hook、不碰安全库 .text 段,只 hook 非安全库的明文读写点(如 conscrypt/libssl 的 SSL_write / SSL_read)——直接拿到 API 明文,全程不触发检测。

4. 分层覆盖

native(syscall/libc)→ JNI → Java framework → 业务类。任一层漏拦都可能命中检测,检查时要逐层确认。

5. IO 关联 trace

Java 方法 onEnter 设全局标记,让 native hook 把 IO 事件关联到具体调用者——快速定位”这个加密 I/O 是哪个方法发的”。

七、实战决策:三类需求三条路

需求 方案 关键限制
我只有 App,没 root 免 Root Gadget 重打包 只对软目标有效;易盾/梆梆/360 级签名校验此路不通
有 root,长期抓流量 ZygiskFrida 或 frida-server 注原版 + 防护组件补丁 + native SSL hook 需破运行时反调试;不要重签
还原签名算法、批量出参 真机抓一次 I/O → 转 unidbg 离线复现 终局方案:没真机、没 frida、无 on-device 反制可检测
上一篇
下一篇