Android 抓包实战:mitmweb + pproxy(海外出口)+ Frida 解 SSL Pinning
前言
在受限网络里抓一个只对海外开放、且做了证书固定(SSL Pinning)的 Android App,常规「装个 CA + 开个代理」是不够的。这篇把一套跑通的组合记录下来:
- VpnService/TUN 转发:把手机(或指定 App)的流量喂给 mitmproxy;
- mitmweb upstream 模式 + pproxy:mitmproxy 负责解密,pproxy 负责把出口换到干净的海外 IP;
- 系统 CA 信任(root):让 App 信任 mitmproxy 的证书;
- Frida 17 解 SSL Pinning:搞定 App 内置的证书固定。
⚠️ 下文所有
IP:PORT、账号密码、包名都用占位符,替换成你自己的。仅用于授权范围内的调试。
整体链路
1 | |
关键点:mitmproxy 只管解密,出口交给 pproxy 链。这样解密和「换 IP」解耦,出口想换哪国换哪国。
组件与环境
- PC:Windows(miniforge/Python 均可)、
mitmproxy、pproxy、frida-tools、objection、Node(给frida-compile用)。 - 手机:已 root(KernelSU/Magisk),装好
frida-server。 - 转发端:任意基于 VpnService 的转发器(Clash.Meta / 自建的 mihomo 壳都行),把流量转到
http://<PC>:8080。
Step 1 · 手机流量转发到 mitmproxy
用 VpnService 建 TUN,把(全局或指定 App 的)流量转发到上游 http://<PC_IP>:8080(即 mitmweb)。
公司网络连不上 PC? 内网常有防火墙/AP 隔离,PC 的 8080 手机直连不通。用 adb 反向隧道走 adb 通道,绕过网络层:
1 | |
DNS 建议用 redir-host(或 fake-ip + sniffer),保证上游看到的是真实域名而非 fake-ip。
Step 2 · mitmweb(upstream)+ pproxy 换海外出口
2.1 pproxy:把出口换成海外代理
pproxy 是纯 Python 代理,能把本地 HTTP 监听转发到任意上游(http / socks5)。上游代理跑在能连通目标节点的机器上(比如带全局代理/WARP 的 Windows 主机,而不是 WSL——见踩坑)。
1 | |
先单独验证 pproxy 出口是不是想要的地区:
1 | |
2.2 mitmweb:上游模式,负责解密
1 | |
如果上游代理本身就是 HTTP 代理,也可以省掉 pproxy,让 mitmweb 直连:
1 | |
验证整条链:
1 | |
Step 3 · 让 App 信任 mitmproxy 的 CA(root)
Android 7+ App 默认不信任用户证书;Android 14 系统证书库搬进了不可写的 Conscrypt APEX。所以要抓别家 App,得把 mitmproxy 的 CA 装进系统证书库——非 root 做不到。
- Root 设备用 MoveCertificate(Magisk/KernelSU 模块)最省事:把
~/.mitmproxy/mitmproxy-ca-cert.cer以用户证书方式导入,模块会在开机时把它注入 APEX 系统库,重启生效。 - 验证:
1 | |
系统信任搞定后,不做 pinning 的 App 就能被解密了。做了 pinning 的看下一步。
Step 4 · Frida 17 解 SSL Pinning(重点)
App 报 Client TLS handshake failed ... certificate unknown,就是证书固定:CA 装了也没用,App 只认自己钉死的证书。用 Frida 动态 hook 掉校验。
4.1 起 frida-server(版本要和 PC 端 frida 对上)
1 | |
4.2 ⚠️ Frida 17 的坑:脚本里没有全局 Java 了
Frida 17 把 Java/ObjC bridge 从内核里移除了,裸脚本写 Java.perform(...) 会直接报:
1 | |
两种解法:
方案 A:objection(最省事,推荐交互式用)
objection 的 agent 已经把 frida-java-bridge 编译进去了,天然兼容 Frida 17:
1 | |
方案 B:自写脚本 + frida-compile 打包 bridge(可常驻/自动化)
脚本顶部显式 import,再用 frida-compile 把 frida-java-bridge 打进去:
1 | |
打包:
1 | |
用一个常驻的 Python 驱动 spawn 目标 App(在任何 TLS 连接之前就装好 hook,比 attach 更稳):
1 | |
1 | |
验证
- 转发器内核日志:
[TCP] ... host:443 match ... using PROXY[upstream],无 503/超时; - Frida 日志:
OkHttp CertificatePinner.check bypassed持续输出; - mitmweb:能看到该 App 的明文请求,不再
certificate unknown; - 出口:
curl -x http://127.0.0.1:8080 http://ip-api.com/json显示目标地区。
踩坑总结
gost/pproxy 放在 WSL 里连不上上游节点:WSL2 有独立 NAT,公司网络下常常到不了外部节点(
i/o timeout),而 Windows 主机能到(走 WARP 等)。把代理进程放到能连通节点的机器上(一般是 Windows 主机),或改.wslconfig的networkingMode=mirrored也未必覆盖到全部路由。出口 IP 的地理定位不一致:同一个 IP,
ipinfo(用运营商 geofeed)可能判美国,ip-api/MaxMind(偏注册地)可能判别国。选节点时多库都查,挑「都判成目标国」的干净 IP(proxy:false, hosting:false更佳),否则 App 里蹦出的地址会和你以为的出口对不上。**Frida 17 移除全局
Java**:裸脚本报Java is not defined。要么用 objection(agent 已内置 bridge),要么frida-compile把frida-java-bridge打进脚本。objection 是交互式 REPL,需要真终端,不方便脚本化常驻;要自动化/常驻就走「frida-compile + Python 驱动 spawn」。
pproxy 鉴权语法是
scheme://host:port#user:pass(#结尾),不是user:pass@host。mitmproxy 只解密、不负责出口能否连通:上游连不上时 mitmweb 会回
502/503,App 就报错——先用curl -x把 pproxy→上游 这一段单独验通再说。
参考
- mitmproxy upstream 模式:https://docs.mitmproxy.org/stable/concepts-modes/
- pproxy:https://github.com/qwj/python-proxy
- Frida:https://frida.re/ objection:https://github.com/sensepost/objection
- Android 14 系统证书注入 / MoveCertificate:https://github.com/ys1231/MoveCertificate