Android 抓包实战全流程:FlowTrans + mitmweb + pproxy + Frida,从解密到重放
本文最后更新于:2026年7月20日 下午
前言
在受限网络里抓一个只对海外开放、做了证书固定(SSL Pinning)、还带反调试的 Android App,「装个 CA + 开个代理」远远不够。这篇把一整套跑通的方案合并记录:设备端转发、PC 端解密+换海外出口、Frida 解 pinning、spawn/attach 取舍、native TLS 的边界,以及抓到之后脱离 app 重放 API 的思路。最后用 Gopuff 做实战。
⚠️ 所有
IP:PORT、账密、包名、client-id、hash 均为占位/脱敏,仅用于授权范围内的调试与学习。
一、整体架构:两半
抓包要解决两件事——①把手机流量导到 PC 的 mitmproxy,②解密 + 换干净的海外出口。分工如下:
1 | |
- FlowTrans(设备端 VpnService/TUN 转发)负责 ①,把(指定 app 的)流量转到上游
127.0.0.1:8080。走adb reverse tcp:8080隧道,公司网络的 AP 隔离/防火墙全绕开。 - mitmproxy 只管解密,出口交给 pproxy,两者解耦,想换哪国 IP 换哪国。
DNS 用 redir-host(或 fake-ip + sniffer),保证上游看到真实域名。
二、组件与环境
- PC:
mitmproxy、pproxy、frida-tools、Node(给frida-compile),Python(miniforge 即可)。 - 手机:已 root(Magisk/KernelSU),装
frida-server(版本与 PC 端 frida 对齐)。 - 转发:FlowTrans / Clash.Meta 等任意 VpnService 转发器,上游填
127.0.0.1:8080。
三、一键 CLI:capture.py
手动一层层敲太累,封装成一个 click CLI(全文见文末附录)。三个子命令:
1 | |
设计要点:
- 自动探测设备:
--device auto用adb devices抓当前连着的,连不上就 try 已知 IP(笔记本换网络/办公室↔家不用改)。 --no-set-proxy:用 FlowTrans 转发时不要再设设备全局代理(settings put global http_proxy)——两者叠加打架,而且抓包进程一停、全局代理指向死端口会让 app 直接断网。- mitmweb 固定密码:mitmproxy 12 的 web UI 默认要 token(裸访问 403),脚本里用
--set web_password=flowtrans设固定密码,开http://127.0.0.1:8081输flowtrans即可(或?token=flowtrans)。 - 出口校验:启动后自动
curl -x pproxy ip-api.com打印出口国家/城市。
四、Frida 解 SSL Pinning(Java 层)
App 报 Client TLS handshake failed ... certificate unknown,就是证书固定:CA 装了也没用。用 Frida hook 掉校验。
Frida 17 的坑:没有全局 Java 了
Frida 17 把 Java bridge 从内核移除,裸脚本 Java.perform(...) 直接 ReferenceError: 'Java' is not defined。两条路:
- objection(交互式最省事):
objection -n <pkg> start→ REPL 里android sslpinning disable。agent 内置了 bridge。 - 自写脚本 + frida-compile(可常驻/自动化):脚本顶部
import Java from "frida-java-bridge",再frida-compile打包。
覆盖常见 pinning 机制的核心脚本(unpin.js):
1 | |
1 | |
spawn vs attach
- spawn(frida 拉起 app):hook 早于任何 TLS 连接,最稳——但对有反调试的 app(Instagram/Gopuff)会超时
timed out while waiting for app to launch。 - attach(先启动再附加):晚一点,但 Java 层 pinning 是每次连接都校验,attach 照样来得及。反调试 app 用它。
- spawn-gating 这台设备/这版 frida 抓不到 zygote fork,实测不通。
五、native TLS 的边界
Java hook 只对「走 Java TLS(Conscrypt/OkHttp)」的 app 有效。有的 app 走 native:
- 导出了符号的 native BoringSSL(部分 Flutter/gRPC):hook
SSL_CTX_set_custom_verify(对每个导出该符号的 module 都挂,别只挂第一个)。注意 Frida 17 用Module.findGlobalExportByName。 - Instagram / Meta 系:主流量走 cronet + 静态链接且符号 strip 的 BoringSSL,
SSL_read/SSL_write根本不被调用,按名字 hook / eCapture(缺SSL_set_fd、SSL_do_handshake)都挂不上,只能特征码扫描——成本高,本文不展开。
六、让 App 信任 mitmproxy 的 CA(root)
Android 7+ 默认不信任用户证书,Android 14 系统证书库搬进不可写的 Conscrypt APEX。抓别家 app 得把 mitmproxy 的 CA 装进系统证书库(非 root 做不到)。Root 设备用 MoveCertificate(Magisk/KernelSU 模块)最省事,重启生效。系统信任 + Frida 解 pinning 双保险。
七、踩坑总结
- pproxy/gost 放 WSL 里连不上上游:WSL2 独立 NAT,公司网络下常到不了外部节点;放到能连通的 Windows 主机上。
- 出口 IP 地理定位不一致:同一 IP
ipinfo判美国、ip-api/MaxMind 判别国。多库都查,挑都判目标国、proxy:false hosting:false的干净 IP。 - pproxy 鉴权语法是
scheme://host:port#user:pass(#结尾),不是user:pass@host。 - mitmproxy 只解密、不管出口连通:上游连不上会回 502/503,先
curl -x把 pproxy→上游单独验通。 - 美国专属 app(如 Gopuff):出口必须干净美区 IP,否则 API 域名可能公网都解析不出来。
八、抓到之后:重放的思路(Gopuff 实战)
抓到明文只是开始,真正有用的是脱离 app 用 curl 重放。以 Gopuff 的 guest token 为例:
排查:token 是个 JWT,iss=identity.gopuff.com,但全程抓不到这个域名。用 mitmweb 的 HTTP API + 脚本遍历所有响应体找「下发 JWT 的那条」,定位到:
1 | |
原来 token 走 GraphQL persisted-query mutation 下发,identity.gopuff.com 只是签发方。
能否重放取决于三件事:
- 认证要不要 secret —— 这里
authorization: Token token=是空的,只要静态x-gopuff-client-id(app 常量)就能换 token; - 参数是否可控 —— persisted query 只放开 variables(改搜索词只改
variables.route的q=,hash 不能动); - 环境约束 —— Gopuff 只认美区 IP,重放要走美国出口。
完整链路:
1 | |
按这个思路纯 curl 走一遍,两步都 200、返回真实搜索结果,重放成立。方法可迁移到任何「抓到包想脱离 app 重放」的场景——核心就那句话:JWT 的 iss 不等于它的获取入口,靠抓包工具的 API 脚本化定位下发点。
附录:capture.py 全文
1 | |
依赖
spawn_unpin.py(spawn/attach 模式驱动)和unpin.compiled.js(Frida 17 打包产物)。
参考
- 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