【原创】Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密
写在前面 :本文记录了一次完整但走了弯路的 Android App HTTPS 抓包实操。目标是一个
targetSdk=35的国产 App,最终没有用到 frida、没有用到 objection、没有改 APK,仅靠"把 Burp CA 装进系统信任锚"就完成了明文解密。全文所有命令均在本机实测通过,环境为 Windows + MuMu 模拟器 + Burp Suite Pro。
⚠️ 运行环境说明(Windows 用户必读) :本文所有命令行均在 Windows PowerShell (5.1 或 PowerShell 7 均可)中执行,不依赖 Git Bash / cmd。
adb一律使用 MuMu 模拟器自带的绝对路径 ,统一定义为变量$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe',用调用运算符&执行(如& $adb devices)。无需安装 Android SDK,也不依赖系统 PATH (这也是为什么你直接在cmd/PowerShell 里裸敲adb会提示"不是内部或外部命令")。命令中的grep对应 PowerShell 的Select-String、tail -N对应Select-Object -Last N、grep -c对应(... | Select-String).Count。而adb shell '...'的单引号内是安卓设备端 命令(设备端自带 toybox 的grep),保持原样即可,不要改成 PowerShell 语法。
📖 目录
- 一、先分清两个完全不同的报错
- [二、为什么"浏览器能用、App 不能用"](#二、为什么"浏览器能用、App 不能用")
- 三、环境与工具清单
- [四、原理:系统 CA 是怎么被"装进去"的](#四、原理:系统 CA 是怎么被"装进去"的)
- 五、完整实操步骤
- 六、验收:三道硬证据
- 七、踩坑记录
- 八、本次走过的弯路
- 九、自动化脚本
- 十、方法论总结
- 十一、最后的恢复顺序
- 十二、声明
一、写在最前面:先分清两个完全不同的报错
如果你也和当年的我一样,打开抓包工具就条件反射地想着"绕过 SSL Pinning",请先看完这一节,能帮你省下半天时间。
抓包失败时,logcat 里大概率会看到下面两种异常之一:
| logcat 异常原文 | 真实含义 | 正确处置 |
|---|---|---|
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. |
信任锚缺失------App 的信任列表里根本没有 Burp 这张 CA | 装系统 CA(本文主线) |
javax.net.ssl.CertPathValidatorException: Certificate pinning failed! |
证书被 pin 死------App 校验了公钥指纹 | 上 frida hook CertificatePinner |
这两句话长得极像,但属于完全不同的层,处置方式互斥。
本次实操的真根因是第一类,我却在第一类上花了大量时间去试第二类的解法(objection、frida Java hook、frida native hook、iptables 透明代理),四条路线全部作废------不是技术不够,是靶心错了。
判定前必做:先列出设备,再选定设备
MuMu 有时会在 adb 里同时挂多台设备(主实例 127.0.0.1:5555、多开实例 127.0.0.1:7555,以及本机其它安卓模拟器如 emulator-5554)。一旦检测到多台,不带 -s 直接执行 logcat 会立刻报错:
error: more than one device/emulator
所以第一步永远是:列出设备 → 挑出运行目标 App 的那一台 → 把它记成变量 $device,之后每条 & $adb 都带 -s $device:
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
# ① 列出所有设备
& $adb devices -l
List of devices attached
127.0.0.1:5555 device model:PFGM00 ← MuMu 主实例(默认端口,本文用它)
127.0.0.1:7555 device model:PFGM00 ← MuMu 多开/第二实例
emulator-5554 device model:PFGM00 ← 另一台安卓模拟器(无华住会,可忽略)
powershell
# ② 选定设备(改成你列表里实际的那一台)
$device = '127.0.0.1:5555'
# ③ 再执行判定命令(务必带 -s $device)
& $adb -s $device logcat -d | Select-String -Pattern 'ssl|tls|cert|trust|pin|exception' | Select-Object -Last 40
说明:
Select-String默认不区分大小写 (等价于grep -i),正则里的|表示"或";Select-Object -Last 40等价于tail -40。如果只想看某个 App,把 Pattern 换成'com\.htinns|Trust anchor|not found|pinning'即可。
先看这行报错,再决定用什么工具。这应该是你的第一个动作,而不是最后一个。
二、为什么"浏览器能用、App 不能用"
这是本次最有价值的一条差分信号,几乎所有国产 App 都会撞上:
Chrome 显式信任用户安装的 CA → 浏览器能上网;同一个 App 却提示"网络不可用"。
原因是 Android 14(API 34)起的一条硬规则:
targetSdk >= 34的应用默认不再信任"用户安装的 CA",只信任系统级 CA。
而浏览器(Chrome / WebView)为了兼容企业环境代理,显式保留了对用户 CA 的信任------这就造成了"百度能开、App 打不开"的自洽假象,极易被误判成 App 的证书绑定问题。
一条命令确认目标落在哪一边:
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s $device shell dumpsys package com.htinns | Select-String -Pattern 'targetSdk|minSdk|versionName'
versionCode=10094800 minSdk=24 targetSdk=35
targetSdk=35 → 用户 CA 路线作废,必须走系统 CA。
三、环境与工具清单
3.1 实测环境
| 组件 | 版本 / 路径 | 说明 |
|---|---|---|
| 操作系统 | Windows | 宿主机 |
| 模拟器 | MuMu 模拟器(MuMuNxLauncher.exe) |
Android 15 / SDK 35 |
| 模拟器 adb | C:\Program Files\Netease\MuMu\nx_main\adb.exe |
自带,设备序列号 127.0.0.1:5555 |
| 设备代号 | PFGM00 |
|
| 网络模式 | NAT | 宿主地址别名 10.0.2.2 |
| SELinux | Permissive | 标签宽松,但仍建议设对 |
| 抓包工具 | Burp Suite Pro 2026.7.3 | C:\software\BurpSuite V2026.7.3\ |
| Burp 监听 | 0.0.0.0:8080 |
必须 All interfaces |
| 辅助工具 | OpenSSL 3.6.5(Git for Windows 自带,版本随 Git 更新,3.x 均可) | 计算证书 hash / 转格式 / 验签 |
| Python | 3.13.14 | 仅用于跑自动化脚本 |
本次实际用到的工具只有三个 :MuMu(提供 Android 环境)+ Burp(MITM)+ OpenSSL(处理证书)。
frida / objection 全程未用上,原因见第七节。
3.2 工具安装说明
MuMu 模拟器 :网易官网下载安装。装完在 Windows 管理器里确认网络模式为 NAT,并开启 root 权限("设置 → 其他 → root 权限")。
Burp Suite:官网下载安装包(自带 JRE,无需另装 Java)。首次启动会要求生成并保存 CA 证书。
OpenSSL :装了 Git for Windows 就有,无需额外配置(确保 Git 已加入系统 PATH,PowerShell 里才能直接敲 openssl)。验证:
powershell
openssl version
# OpenSSL 3.6.5 29 Sep 2026
adb :MuMu 自带,不用装 Android SDK。本文所有命令统一用变量 $adb 指向其绝对路径,并都带 -s <序列号>,否则会报 more than one device/emulator。
四、原理:系统 CA 是怎么被"装进去"的
Android 的系统证书信任链,最终由 Conscrypt 读取以下两个目录中的 .0 文件:
/system/etc/security/cacerts ← 旧版 rootfs 路径
/apex/com.android.conscrypt/cacerts ← Android 10+ 的 APEX 路径
这两个目录内容完全相同,但 inode 不同 ,是两份独立拷贝------所以必须两个都覆盖,只改一个不会生效。
装好之后还有一个致命细节:
Conscrypt 的
TrustAnchorIndex在 zygote(系统应用孵化器)启动时一次性建立并缓存。杀掉 App 进程重开是无效 的,必须重启 framework(
stop/start)让 zygote 重建信任锚。
所以完整链路是:
Burp CA (cacert.der)
└─ openssl x509 -inform der -subject_hash_old → 9a5ba575
└─ DER → PEM,按 hash 命名 9a5ba575.0
└─ 复制原有 145 张系统 CA + 追加这张 = 146 张
└─ mount --bind 覆盖【两个】CA 目录
权限 644 / 属主 root:root / SELinux 标签 system_security_cacerts_file
└─ & $adb shell stop ; & $adb shell start ← 重建 zygote 信任锚
└─ 模拟器 WLAN 代理 = 10.0.2.2:8080
└─ 启动 App → Burp HTTP history 明文 ✅
五、完整实操步骤
步骤 1 · 确认 root 与设备状态
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb devices -l
# List of devices attached
# 127.0.0.1:5555 device model:PFGM00 ← 本文所用主实例
# 127.0.0.1:7555 device model:PFGM00 ← MuMu 多开实例
# emulator-5554 device model:PFGM00 ← 其它模拟器(无目标 App,可忽略)
# 多设备时,后面每条命令都要带 -s <序列号> 指定,否则报 more than one device/emulator
& $adb -s 127.0.0.1:5555 root
# adbd is already running as root
步骤 2 · Burp 配置监听器
Burp → Proxy → Proxy Settings → 选中 8080 监听器 → Edit:
| 配置项 | 值 | 为什么 |
|---|---|---|
| Bind to address | All interfaces (0.0.0.0) | 绑 127.0.0.1 则模拟器连不上 |
| Port | 8080 |
然后 Proxy → Proxy Settings → Import/Export CA certificate → Export → Certificate in DER format ,存为 C:\Users\<你的用户名>\Downloads\cacert.der。
步骤 3 · 设置模拟器代理(重点中的重点)
MuMu NAT 模式下,宿主机的地址是 10.0.2.2,不是你电脑的局域网 IP。
图形界面路径:
MuMu 模拟器内 → 设置 → WLAN → 长按当前已连接的网络 → 修改网络 → 高级选项 → 代理 → 手动 → 主机名
10.0.2.2、端口8080→ 保存
命令行方式(等效):
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s 127.0.0.1:5555 shell settings put global http_proxy 10.0.2.2:8080
& $adb -s 127.0.0.1:5555 shell settings get global http_proxy
# 10.0.2.2:8080
⚠️ 两条血泪警告:
- 代理只设在安卓设备内的 WLAN 设置里,不要动 MuMu 的 Windows 管理器(那里只管 NAT / 桥接)。
- 绝对不要用
& $adb shell svc wifi disable/enable开关 WiFi ------会触发网络栈重启,adb 直接变成 offline,我在这里卡了整整一小时。
步骤 4 · 验证代理链路通了(先证明管道通,再谈解密)
浏览器访问 http://neverssl.com,然后看 Burp → Proxy → HTTP history(不是"拦截"标签页!)。
出现条目 = 链路通。浏览器显示"不安全"没关系,那只是它读到自签证书的正常提示。
判活唯一标准是 Burp 的 HTTP history。 看错标签页会误判成"没抓到"。
步骤 5 · 读 targetSdk,决定路线
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s 127.0.0.1:5555 shell 'dumpsys package com.htinns | grep -E "targetSdk|minSdk"'
注意:
adb shell '...'的单引号内是设备端 命令(设备端 toybox 自带grep),这里保持grep不变;真正在宿主机侧过滤时才用 PowerShell 的Select-String。
targetSdk < 34→ 装用户 CA 即可收工(Android 7+ 需在"设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书"中选择,且部分 ROM 需先设"仍受信任"凭据)targetSdk >= 34→ 必须走系统 CA,继续往下
⚠️ 先停一下:下面这段"手动 UI 导入用户 CA"只适用于
targetSdk < 34的 App。华住会targetSdk=35,用户 CA 对它无效,看完直接跳步骤 6。
步骤 5 补充 · 手动 UI 导入用户 CA(仅 targetSdk<34;华住会无效)
如果你的目标 App targetSdk < 34,在安卓设置里手动装一张用户 CA 就能让 App 信任 Burp。完整 UI 步骤:
-
Burp CA 已导出 :步骤 2 已把
cacert.der存到C:\Users\<你的用户名>\Downloads\。 -
传到模拟器 :
powershell$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe' & $adb -s 127.0.0.1:5555 push "C:/Users/<你的用户名>/Downloads/cacert.der" /sdcard/Download/或用 MuMu 的共享文件夹直接把文件拖进模拟器。
-
安卓内安装 (路径因 ROM 略有差异,以下为 Android 15 常见路径):设置 → 安全与隐私 → 更多安全设置 → 加密与凭据 → 安装证书 → CA 证书 → 选择
/sdcard/Download/cacert.der→ 命名(如Burp)→ 按提示设置/输入锁屏密码(未设密码会要求先设一个)。 -
验证安装 :设置 → 安全与隐私 → 更多安全设置 → 加密与凭据 → 信任的凭据 → 用户 标签,能看到
PortSwigger CA即成功。
此时浏览器和 targetSdk < 34 的 App 都能被 Burp 解密。
但华住会(targetSdk=35)卡在这一步 :Android 14(API 34)起,targetSdk >= 34 的 App 默认不信任用户安装的 CA,只信任系统级 CA。所以你装完用户 CA 后------Chrome / 浏览器能抓到包 ✅,华住会 App 仍报"网络未连接" ❌。这正是前面"为什么浏览器能用、App 不能用"那一节讲的差分信号。
所以对华住会,不要浪费时间在手动导用户 CA 上,直接看步骤 6 把它装成系统 CA。
步骤 6 · 把 Burp CA 装成系统 CA
先准备好目录和证书:
powershell
# 1) 算 Android 系统 CA 用的旧版 subject hash
openssl x509 -inform der -in "C:/Users/<你的用户名>/Downloads/cacert.der" -subject_hash_old -noout
# 9a5ba575
# 2) DER 转 PEM,并按 hash 命名
openssl x509 -inform der -in "C:/Users/<你的用户名>/Downloads/cacert.der" -out "C:/work/9a5ba575.0"
然后在设备上操作(注意:执行 stop 后 adb 会短暂断开,见 7.3):
powershell
$D = '127.0.0.1:5555' # 即步骤 1「devices」列表里你选定的那一台
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
# 3) 两个目录各准备一份 staging
& $adb -s $D shell 'mkdir -p /data/local/tmp/cacerts_sys /data/local/tmp/cacerts_apex'
# 4) 复制原有系统 CA(先卸载旧 bind,否则源目录会被自己污染)
& $adb -s $D shell 'umount /system/etc/security/cacerts; umount /apex/com.android.conscrypt/cacerts'
& $adb -s $D shell 'cp /system/etc/security/cacerts/*.0 /data/local/tmp/cacerts_sys/'
& $adb -s $D shell 'cp /apex/com.android.conscrypt/cacerts/*.0 /data/local/tmp/cacerts_apex/'
& $adb -s $D shell 'ls /data/local/tmp/cacerts_sys | wc -l' # 应为 145
# 5) 推送 Burp CA
& $adb -s $D push "C:/work/9a5ba575.0" /data/local/tmp/cacerts_sys/9a5ba575.0
& $adb -s $D push "C:/work/9a5ba575.0" /data/local/tmp/cacerts_apex/9a5ba575.0
# 6) 修正权限 / 属主 / SELinux 标签(两个目录都要)
& $adb -s $D shell 'chown root:root /data/local/tmp/cacerts_sys/9a5ba575.0 /data/local/tmp/cacerts_apex/9a5ba575.0'
& $adb -s $D shell 'chmod 644 /data/local/tmp/cacerts_sys/9a5ba575.0 /data/local/tmp/cacerts_apex/9a5ba575.0'
& $adb -s $D shell 'chcon u:object_r:system_security_cacerts_file:s0 /data/local/tmp/cacerts_sys/9a5ba575.0 /data/local/tmp/cacerts_apex/9a5ba575.0'
# 7) bind mount 覆盖两个目录
& $adb -s $D shell 'mount --bind /data/local/tmp/cacerts_sys /system/etc/security/cacerts'
& $adb -s $D shell 'mount --bind /data/local/tmp/cacerts_apex /apex/com.android.conscrypt/cacerts'
# 8) 校验
& $adb -s $D shell 'ls /system/etc/security/cacerts | wc -l' # 应为 146
步骤 7 · 重启 framework(关键,不能省)
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s 127.0.0.1:5555 shell 'am force-stop com.htinns'
& $adb -s 127.0.0.1:5555 shell 'stop' # adb 会断开,属正常现象
Start-Sleep -Seconds 5
& $adb -s 127.0.0.1:5555 shell 'start'
等 framework 起来(getprop init.svc.zygote = running 且 pidof system_server 非空),再重新连 adb。
步骤 8 · 启动 App,检查 Burp
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s 127.0.0.1:5555 shell 'monkey -p com.htinns -c android.intent.category.LAUNCHER 1'
此时 App 应能正常联网(首页、轮播图、底部导航栏都加载出来),Burp HTTP history 里能看到明文。
六、验收:三道硬证据(缺一不可)
证据 1 · SSL 错误归零
powershell
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
(& $adb -s 127.0.0.1:5555 logcat -d | Select-String -Pattern 'Trust anchor for certification path not found').Count
# 0
修复前这个数字是满屏的,修复后是 0。
证据 2 · 界面正常
App 首页能加载出真实内容(本次验证时,轮播图每次重启都换新文案,说明确实在实时拉取),底部导航栏完整,无"网络不可用"提示。
证据 3 · 证书链对比(这是最硬的证据)
很多人判断"有没有抓到"只靠看 Burp 界面,这是软信号 ------可能看错标签页、可能漏判。真正硬的判据是证书链 issuer 对比:
powershell
# 经 Burp 代理
openssl s_client -connect 127.0.0.1:8080 -servername hweb-manager.huazhu.com 2>&1 | Select-String -Pattern 'subject=|issuer='
# subject=CN=hweb-manager.huazhu.com
# issuer= C=PortSwigger, OU=PortSwigger CA, CN=PortSwigger CA ← Burp 签发 = 真 MITM
# 直连真实服务器做对照
openssl s_client -connect hweb-manager.huazhu.com:443 -servername hweb-manager.huazhu.com 2>&1 | Select-String -Pattern 'subject=|issuer='
# issuer= DigiCert GeoTrust G2 TLS CN RSA4096 ← 真实 CA
issuer 从 DigiCert 变成 CN=PortSwigger CA,就是"Burp 确实在中间解密"的不容置疑的证据。
顺便说:只看界面的话,Burp 里的
CONNECT隧道也可能看起来"有流量",但那是透传不解密,看不到明文。证书链对比能一次性区分这两种情况。
七、踩坑记录(这部分可能比正文更有用)
7.1 remount /system 失败------不是权限问题
/system/bin/sh: remount: inaccessible or not found
mount: '/system' not in /proc/mounts
看到 adbd is already running as root 却还是失败?别往权限上想。根因是 Android 15 采用 super 动态分区 + overlayfs,/system 已经不是独立挂载点了 ,任何 remount /system 必然失败。
解法 :mount --bind 不要求源可写 ,直接覆盖目标目录即可。根分区 /dev/block/sda6 是 ro 也不影响。
7.2 只覆盖一个 CA 目录 = 白干
/system/etc/security/cacerts ← 内容与下面相同
/apex/com.android.conscrypt/cacerts ← 但 inode 不同,是两份拷贝
用 stat -c "%i %n" 对比两个目录里同名证书的 inode 就能看出这一点。两个都要 bind mount,否则 App 照样报错。
7.3 装好 CA 后仍然报错------TrustAnchorIndex 缓存
杀掉 App 进程重开,错误依旧。原因是 Conscrypt 的信任锚索引在 zygote 启动时就建立好了,App 进程只是继承了这个快照。
必须重启 framework :& $adb shell stop + & $adb shell start。adb 会断开,属正常现象,等 zygote 回来即可。
7.4 PowerShell 下 adb 的路径与调用注意点
powershell
# 错误示范:直接写带空格的路径、或忘记 & 调用运算符,PowerShell 会解析失败
adb push C:\work\9a5ba575.0 /data/local/tmp/cacerts_sys/
# 正确:用 & 调用运算符;多设备时务必加 -s <序列号>(这里用 $device)
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
$device = '127.0.0.1:5555'
& $adb -s $device push 'C:\work\9a5ba575.0' /data/local/tmp/cacerts_sys/
PowerShell 不会 像 Git Bash 那样把 /data/xxx 自动转成 C:\Program Files\Git\data\xxx,所以本地 Windows 路径(如 C:\work\9a5ba575.0)直接写即可,无需任何路径转换开关。唯一要记住的两点:
- 带空格的可执行文件路径必须用
&调用 (或先赋值给变量再& $adb),本文所有命令都遵循此写法。 - PowerShell 的单引号字符串不展开变量 (与 bash 的
"$VAR"不同),所以'C:\...\adb.exe'就是字面路径;而adb shell '...'里的单引号同样只是字符串定界,整串原样传给设备端 sh。
顺带提醒:如果你之前在 Git Bash 里踩过
MSYS_NO_PATHCONV的坑,在 PowerShell 里完全不存在------那是 Git Bash 独有的路径转换机制,PowerShell 没有。
7.5 bind mount 的幂等性------重跑脚本会丢掉 145 张系统 CA
第二次运行注入脚本时,发现"原系统 CA 只有 1 张已就位"。
原因是目标目录已经被自己上一次的 bind mount 覆盖了 ,此时 cp /system/etc/security/cacerts/*.0 ... 拷出来的其实是自己上次做的 staging 目录(只有新加的那 1 张)。
正确顺序:
先 umount → 再从真实只读源复制 → 断言数量 ≥ 100 → 再 mount --bind
我在自动化脚本里加了这条断言,防止以后重跑时静默损坏系统信任链:
python
adb("umount %s" % target, shell_cmd=True) # 先卸载
tmp = staging + "_new"
adb("rm -rf %s" % tmp, shell_cmd=True)
adb("mkdir -p %s" % tmp, shell_cmd=True)
adb("cp %s/*.0 %s/" % (target, tmp), shell_cmd=True)
cnt = int(adb("ls %s | wc -l" % tmp, shell_cmd=True).strip().split()[0])
if cnt < 100: # 断言没被污染
raise RuntimeError("原系统 CA 只有 %s 张,说明源目录被 bind 覆盖污染" % cnt)
7.6 危险操作:不要 rm -rf 清空 CA 目录
如果用 rm -f /system/etc/security/cacerts/*.0 来"清空重装",一旦中途失败,整个设备的 HTTPS 会全面失效(连系统组件都访问不了网络)。
安全做法是"复制到临时目录 → 校验数量 → mv 原子切换 → 再 bind mount",全程不碰原目录。
八、本次走过的弯路(供后来者避坑)
这次实操一共试了 4 条失败路线,全部因为把问题定义成"绕过 SSL 证书绑定"。它们的失败原因值得记录:
| 尝试的路线 | 失败现象 | 真正原因 |
|---|---|---|
objection android sslpinning disable |
A Frida agent exception has occurred. Error: abort was called ... tryGetEnvJvmti |
objection 1.12.5 与 Android 15 的 ART 不兼容,直接崩 JVMTI |
| frida hook Java 层 TrustManager | ReferenceError: 'Java' is not defined |
该 App 进程是 ARM64 转译进程 (app_process64 映射的是 /system/lib64/arm64/libc.so),frida 17 的 Java 桥无法初始化 |
| frida hook native 层 BoringSSL | 全部 skip(未导出目标符号) |
libssl.so / libcrypto.so / libconscrypt.so 均未导出 X509_verify_cert 等符号 |
| iptables 透明代理 | 方向性错误 | 试图绕过"App 的代理检测",但失败原因压根不是代理检测 |
如果第 1 步就抓 logcat 看报错原文,10 分钟就能到答案。
附:frida 路线并非全无价值
frida 在原生 x86_64 App(没有 ARM 转译)上是可以用的,只是需要做两件事:
- hook
X509ExtendedTrustManager.checkServerTrusted的3 参重载 ------只 hookX509TrustManager的 2 参版本对现代 TLS 栈无效,这是最常见的"hook 了但没效果"原因 - 用 Python API 加载脚本 而非
frida -l:CLI 的-l在部分环境下不回显日志,会让你误判"脚本没加载",白折腾很久
九、自动化脚本
上述步骤我封装成了一个幂等脚本,一次跑完(含 framework 重启):
python
#!/usr/bin/env python3
# install_system_ca.py ------ 把 Burp CA 装成 Android 系统级信任锚
import os, re, subprocess, sys, time
ADB = r"C:\Program Files\Netease\MuMu\nx_main\adb.exe"
SERIAL = "127.0.0.1:5555"
NO_CONV_ENV = {"MSYS_NO_PATHCONV": "1"} # 兼容 Git Bash 的路径转换开关(PowerShell 下此变量无害)
# 两个 CA 目录内容相同但 inode 不同, 必须都覆盖
CA_DIRS = [
("/system/etc/security/cacerts", "/data/local/tmp/cacerts_sys"),
("/apex/com.android.conscrypt/cacerts", "/data/local/tmp/cacerts_apex"),
]
def adb(*args, shell_cmd=False):
env = dict(os.environ); env.update(NO_CONV_ENV)
cmd = [ADB, "-s", SERIAL] + (["shell"] if shell_cmd else []) + list(args)
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
return (p.stdout or "") + (p.stderr or "")
def find_cert():
if len(sys.argv) > 1:
return sys.argv[1]
for cand in [os.path.expanduser(r"~\Downloads\cacert.der"),
os.path.expanduser(r"~\Downloads\cacert.pem")]:
if os.path.isfile(cand):
return cand
raise SystemExit("[!] 找不到 cacert.der, 请显式传入路径")
def subject_hash_old(der_path):
out = subprocess.run(["openssl", "x509", "-inform", "DER", "-in", der_path,
"-subject_hash_old", "-noout"],
capture_output=True, text=True)
h = out.stdout.strip()
if not re.fullmatch(r"[0-9a-fA-F]{8}", h):
raise RuntimeError("openssl 计算 hash 失败: %r" % h)
return h
def main():
der = find_cert()
h = subject_hash_old(der)
pem = os.path.join(os.path.dirname(os.path.abspath(__file__)), "%s.0" % h)
subprocess.run(["openssl", "x509", "-inform", "DER", "-in", der, "-out", pem],
check=True, capture_output=True)
print("[*] subject_hash_old = %s" % h)
adb("root")
for target, staging in CA_DIRS:
# 关键: 先卸载旧 bind, 否则 cp 源目录被自己覆盖, 会丢原有系统 CA
adb("umount %s" % target, shell_cmd=True)
tmp = staging + "_new"
adb("rm -rf %s" % tmp, shell_cmd=True)
adb("mkdir -p %s" % tmp, shell_cmd=True)
adb("cp %s/*.0 %s/" % (target, tmp), shell_cmd=True)
cnt = int(adb("ls %s | wc -l" % tmp, shell_cmd=True).strip().split()[0])
if cnt < 100: # 断言源目录未被污染
raise RuntimeError("原系统 CA 只有 %s 张(应 >=145), 源目录被 bind 污染" % cnt)
adb("rm -rf %s" % staging, shell_cmd=True)
adb("mv %s %s" % (tmp, staging), shell_cmd=True)
subprocess.run([ADB, "-s", SERIAL, "push", pem, "%s/%s.0" % (staging, h)],
check=True, capture_output=True, env={**os.environ, **NO_CONV_ENV})
adb("chown root:root %s/%s.0" % (staging, h), shell_cmd=True)
adb("chmod 644 %s/%s.0" % (staging, h), shell_cmd=True)
adb("chcon u:object_r:system_security_cacerts_file:s0 %s/%s.0" % (staging, h), shell_cmd=True)
adb("mount --bind %s %s" % (staging, target), shell_cmd=True)
print(" %s -> ok (原 CA %s 张)" % (target, cnt))
print("[*] 重启 framework (adb 会短暂断开)...")
adb("stop", shell_cmd=True); time.sleep(5); adb("start", shell_cmd=True)
for _ in range(15):
time.sleep(8)
if adb("getprop init.svc.zygote", shell_cmd=True).strip() == "running" \
and adb("pidof system_server", shell_cmd=True).strip():
print(" framework ready"); break
print("[+] 完成。启动 App 后看 Burp -> Proxy -> HTTP history")
if __name__ == "__main__":
main()
用法:
powershell
python install_system_ca.py
# 或指定证书路径
python install_system_ca.py "C:/Users/<你的用户名>/Downloads/cacert.der"
注意:MuMu 每次重启后,bind mount 会失效,需要重跑一次脚本。
十、方法论总结
这次实操最大的收获不是"装上了系统 CA",而是几条可迁移的判断原则:
1. 报错语义 > 一切假设
先读 logcat 的异常原文,再决定用什么技术。Trust anchor not found 和 CertificatePinningException 是两条互斥的路。
2. 差分信号价值极高
"浏览器能用 / App 不能用"这类现象,直接指向信任链差异,而不是证书绑定。同类差分还有"同设备上 App A 能、B 不能"。
3. 能用系统级手段,就别用注入
系统 CA 注入 > APK 重打包 > frida hook > objection
复杂度低 中 高 高
通用性高 中 低 低
系统 CA 这条路对所有 App 通用,且不依赖 root 之外的高级技巧。
4. 判断"是否真在中间"要找硬证据
- 差:看 Burp 界面
- 中:界面 + 网络计数(ESTABLISHED 数 / iptables pkts)
- 好:证书链 issuer 对比(DigiCert → PortSwigger CA)
5. 错误结论要留痕,也要主动推翻
我最初在项目笔记里写过"APP 抓包路线放弃",理由只是"真机 root 门槛太高"。模拟器路线跑通后这条结论就被推翻了------推翻一个错误结论,比维护它值钱得多,否则以后会直接跳过一条能走的路。
6. 手册里一定要写"已排除路线"
如果当初就写清楚"frida 三条路在 ARM64 转译进程上全部不可用",能省掉数小时的盲试。
十一、最后的恢复顺序(重启后最省事)
powershell
# 1. 启动 MuMu,跑 install_system_ca.py(含 framework 重启)
python install_system_ca.py
# 2. 设置代理
$adb = 'C:\Program Files\Netease\MuMu\nx_main\adb.exe'
& $adb -s 127.0.0.1:5555 shell settings put global http_proxy 10.0.2.2:8080
# 3. 启动目标 App
& $adb -s 127.0.0.1:5555 shell 'monkey -p com.htinns -c android.intent.category.LAUNCHER 1'
# 4. Burp -> Proxy -> HTTP history 看明文
十二、声明
本文所述操作均在本人拥有授权的测试目标上进行,仅用于安全研究学习。所有命令均为只读分析或本地证书信任链调整,未对生产业务造成任何影响。
涉及 App 的 API 抓取结果仅用于漏洞研究验证。请勿用于任何未授权的用途。
如果你也在抓包时卡在"App 说网络不可用、浏览器却好好的",先去看 logcat 那一行报错------大概率答案就在那里。
原创文章,欢迎转载,转载请注明出处。