Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密

【原创】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 语法。


📖 目录


一、写在最前面:先分清两个完全不同的报错

如果你也和当年的我一样,打开抓包工具就条件反射地想着"绕过 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

⚠️ 两条血泪警告:

  1. 代理只设在安卓设备内的 WLAN 设置里,不要动 MuMu 的 Windows 管理器(那里只管 NAT / 桥接)。
  2. 绝对不要用 & $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 步骤:

  1. Burp CA 已导出 :步骤 2 已把 cacert.der 存到 C:\Users\<你的用户名>\Downloads\。

  2. 传到模拟器 :

    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 的共享文件夹直接把文件拖进模拟器。

  3. 安卓内安装 (路径因 ROM 略有差异,以下为 Android 15 常见路径):设置 → 安全与隐私 → 更多安全设置 → 加密与凭据 → 安装证书 → CA 证书 → 选择 /sdcard/Download/cacert.der → 命名(如 Burp)→ 按提示设置/输入锁屏密码(未设密码会要求先设一个)。

  4. 验证安装 :设置 → 安全与隐私 → 更多安全设置 → 加密与凭据 → 信任的凭据 → 用户 标签,能看到 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)直接写即可,无需任何路径转换开关。唯一要记住的两点:

  1. 带空格的可执行文件路径必须用 & 调用 (或先赋值给变量再 & $adb),本文所有命令都遵循此写法。
  2. 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 转译)上是可以用的,只是需要做两件事:

  1. hook X509ExtendedTrustManager.checkServerTrusted 的3 参重载 ------只 hook X509TrustManager 的 2 参版本对现代 TLS 栈无效,这是最常见的"hook 了但没效果"原因
  2. 用 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 那一行报错------大概率答案就在那里。

原创文章,欢迎转载,转载请注明出处。

相关推荐
booboosui4 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果4 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.6 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen6 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件
booboosui1 天前
Android 上来电怎么判:7 个条件加 5 个中文词
android
MardaWang1 天前
App 内嵌 H5 的信任边界设计(ArkWeb/HarmonyOS 实战)
android
石头慢慢说1 天前
不写 XPath,让 AI 在真机上跑测试:Appium MCP 全流程实战
android
90后的晨仔1 天前
解决 Android 构建报错:Unable to delete file .../file-map.txt
android·前端
mg6681 天前
安卓玩机工具----安卓手机变成桌面电脑!多窗口镜像+音频投射+游戏操控,无需 Root
android·智能手机·电脑