简介:针对支付宝小程序抓包时常见的证书验证拦截问题,这份资源提供了一套基于动态插桩技术的破解方案,适合移动端安全测试人员、逆向工程师及对小程序协议分析感兴趣的开发者。压缩包共包含三个文件:一个脚本负责调用插桩框架并自动化执行注入任务,一份说明文档梳理了原理与使用步骤,另一个脚本实现核心的钩子逻辑,整体体积约两千字节,轻量且便于阅读。内容结合真实调试场景,讲解了如何定位支付宝小程序内部的证书校验逻辑,并通过动态注入绕过固定机制,让代理抓包工具顺利捕获加密流量。资源已有三千零一人学习下载,运行思路清晰,文档配合脚本即可快速搭建抓包环境。读者既能理解小程序端证书固定的常见实现,也能掌握动态插桩的实操手法,为之后分析小程序接口、排查数据安全隐患提供一套可复用的脚本基础和排查思路。
1. 一开代理,小程序 HTTPS 请求就全线失败:这不是代理不行
联调小程序时经常遇到同一个现象:代理工具打开,浏览器网页一切正常,换到某支付 App 里的小程序环境,接口突然全部报 SSL 验证失败、请求超时,白屏一片。有人第一反应是换抓包工具,换完发现照样失败;有人关掉代理直接裸请求,接口又恢复正常。这个来回折腾的时间,往往比真正定位问题的时间还长。
这篇文章要解决的就是这一件事:在代理抓包环境下,小程序 SSL 验证失败到底发生在 TLS 握手的哪一步,代理 CA 证书为什么没有被客户端信任,以及用什么顺序排查、用什么脚本把抓包流量变成可复现的调试证据。内容只针对开发者自有工程、测试环境或已获授权的联调场景,不涉及对线上应用验证机制的逆向和改动。
适合谁看:被小程序联调逼疯的前端、做客户端接口调试的测试开发、以及刚把代理抓包环境搭起来却什么都抓不到的新手。下面从头讲原理、给命令、列坑。
2. 小程序抓包为什么卡在 SSL 验证:握手阶段谁把代理拒了
HTTPS 抓包的原理是中间人代理:客户端连接的是本地代理,代理再去连接真实服务器,两边各自完成 TLS 握手。要让客户端接受代理的解密行为,客户端必须信任代理生成的 CA 证书。听起来简单,但小程序类宿主环境有个特点:它不像普通浏览器那样把所有已安装证书都当成可信根,而是有自己的信任策略。所以常见现象是:手机上证书列表里明明能看到代理 CA,请求还是报验证失败。
先说结论:绝大多数“SSL 验证失败”不是代理工具不行,而是证书被安装到了错误的信任区,或者目标进程根本不读那个信任区。下面把三种常见校验方式说清楚,再给一个用 openssl 快速定位握手断点的命令。
2.1 系统证书信任区与用户证书信任区:代理证书装错地方是根因
许多抓包教程只写了“下载 CA 证书并安装”,但没交代装到哪个信任区。iOS 上证书下载后默认进入“用户”信任区,如果不在“设置 > 通用 > 关于本机 > 证书信任设置”里手动打开代理 CA 的信任开关,TLS 校验时这个 CA 等于不存在。Android 7.0 之后,“设置 > 安全 > 加密与凭据 > 安装证书”安装的是用户证书,默认情况下所有 targetSdk 24 及以上的应用都不会信任它,除非应用自己的网络安全配置里显式列出了用户信任区。
这就是小程序类应用和浏览器行为差异最大的地方。浏览器通常把用户证书也纳入可信集合,但小程序宿主 App 基本按系统证书策略校验,用户证书装了也白装。另一个容易踩的入口是 Android 的“WLAN 证书”和“CA 证书”是两个选项,选错虽然也能装进去,但 TLS 校验根本不会用它。这个区别会让代理看起来“装了证书”,实际完全不生效。
要确认自己装进了哪个信任区,可以在 Android 的“信任的凭据”页面分别看“系统”和“用户”两个页签;iOS 则在“证书信任设置”里看有没有代理 CA 的开关。代理 CA 出现在用户信任区只是第一步,被目标进程信任才是最终目的。多数模拟器/开发机把 CA 塞进系统信任区,就是为了让宿主 App 默认信任它。
2.2 三种常见校验形态:系统证书校验、证书绑定、双向认证
我在项目里总结过一张对照表,排查时可以按表里的特征先归一下类,避免从头到尾瞎试。
| 校验形态 | 客户端表现 | 代理侧看到的失败特征 |
|---|---|---|
| 系统证书校验 | 代理 CA 未被系统信任,握手直接被中止 | openssl/curl 报 verify error 或 unable to get local issuer certificate |
| 证书绑定 SSL Pinning | 客户端内置服务端证书指纹或公钥,校验代理临时证书失败 | ClientHello 之后连接立即 reset,偶见 handshake_failure |
| 双向认证 mTLS | 服务端要求客户端证书,代理没有证书可出示 | 握手在 CertificateRequest 之后中断,服务端主动关闭连接 |
系统证书校验是最好处理的,把代理 CA 装进系统信任区,或者让应用信任用户证书即可。证书绑定则要分场景:如果是自己维护的工程,可以在 debug 构建里关闭 pinning 检查;如果是线上生产包,那不属于调试环境的合法范畴,不应该花力气去绕过。双向认证需要后端提供用于测试的客户端证书,拿到 p12 后在代理工具里导入,握手才能往下走。
大部分小程序联调碰到的是第一种。原因也很简单:小程序宿主 App 通常不自己做额外指纹校验,它依赖的是系统 TLS 栈,只是系统栈从 Android 7.0 开始默认不信任用户证书。所以“小程序 SSL 验证失败”很多时候不是 App 做了手脚,而是系统的默认策略变了。
2.3 用 openssl 判断代理是在哪一步被拒的
与其靠眼睛看界面,不如直接用一个命令观察 TLS 握手的每一段。我一般在本机先用 curl 测试代理是否解密成功,再用 openssl 看证书链。下面这条命令适合在开发机执行,目标域名用测试环境的地址。
printf '' | openssl s_client \ -connect api.dev.example.com:443 \ -proxy 127.0.0.1:8080 \ -servername api.dev.example.com \ -showcerts 2>&1 | grep -E "verify|subject|issuer|alert|error"参数说明:-connect 指定目标服务器地址和端口;-proxy 让 openssl 走本地代理端口 8080,这是关键,不加它测的是直连,和抓包场景无关;-servername 用于触发 SNI,很多后端会根据 SNI 返回不同证书,缺了它可能看到错误证书;-showcerts 会把服务器发来的整条证书链打出来。
输出里重点看两处。一是 verify 路径,如果返回 verify return code: 0,说明本机信任链没问题;如果报 unable to get local issuer certificate,说明代理 CA 没有被当前设备信任。二是 issuer 字段,正常情况下应该显示代理 CA 的名称,如果显示的是服务端真实证书的签发者,说明代理根本没有介入 TLS,请求仍然是直连或被代理工具当成普通隧道放了。
如果在 CONNECT 之后立刻出现 handshake failure,且 issuer 字段正常但连接仍然断开,那基本可以判断是证书绑定或双向认证这一类应用层校验,而不是证书信任区的问题。这个命令能帮你把排查范围从“代理工具坏了”缩小到“证书信任区”或“应用校验策略”二选一。
3. 本地跑通代理抓包的最小方案:证书安装与流量解密确认
原理清楚了,实操才能不跑偏。这一章给出一套最小可用的代理抓包方案:用 mitmproxy 起本地代理,把 CA 证书装进目标设备,再用一条 curl 确认流量真的被解密。整个过程只涉及开发环境,不碰线上生产。
为什么选 mitmproxy 而不是其它图形化工具?因为它脚本化方便、跨平台、命令行输出干净。图形工具在界面里点一点也能完成同样的事,但遇到批量过滤、离线分析、自动化回放时会比较吃力。mitmproxy 学习成本略高,换来的是可重复的抓包流程,这叫性价比。
3.1 用 mitmproxy 起一个本地代理:最小启动命令
启动代理是最简单的一步,但参数里有几个坑值得说。先装包:
pip install mitmproxy然后启动:
mitmproxy --listen-host 127.0.0.1 --listen-port 8080 --set block_global=false参数说明:--listen-host 指定代理监听地址,默认是 127.0.0.1,只允许本机连接;--listen-port 指定端口,默认 8080;--set block_global=false 表示允许非本机 IP 的设备连接,真机调试必须加这个参数,不加的话手机连不上代理。安全上要注意:block_global=false 会让同一局域网的其它设备也能访问代理端口,调试完及时关掉。
mitmproxy 和 mitmdump 的区别也在这里统一说:mitmproxy 是交互式界面,适合看流量;mitmdump 是非交互方式,专跑脚本,适合自动化。日常调试我习惯先用 mitmproxy 观察,稳定后再切 mitmdump 落盘保存。
3.2 把代理 CA 装进安卓与 iOS:两种信任区路径
第一次启动 mitmproxy 后会在用户目录生成 ~/.mitmproxy/mitmproxy-ca.pem。这台代理 CA 就是后续要安装进设备的东西。Android 直接用 pem 文件,iOS 需要转换成 der 格式再安装描述文件,命令如下。
# 导出 iOS 可识别的 der 格式 openssl x509 -in ~/.mitmproxy/mitmproxy-ca.pem -outform der -out mitmproxy-ca.der # Android 直接复制 pem 文件 cp ~/.mitmproxy/mitmproxy-ca.pem android-ca.pem导出后,Android 手机把 android-ca.pem 传到设备上,在“设置 > 安全 > 加密与凭据 > 安装证书”里选“CA 证书”,按提示完成安装。这里必须强调:不要选“WLAN 证书”入口,选错之后代理照样能连接,但 HTTPS 请求证书校验仍然失败。
iOS 的路径多一步:先把 mitmproxy-ca.der 传到手机,点击安装描述文件,然后必须去“设置 > 通用 > 关于本机 > 证书信任设置”里找到对应 CA 并打开开关。很多人在 iOS 上抓包失败,不是因为描述文件没装,而是漏了最后这个信任开关。两个系统装完证书后,把手机 Wi-Fi 代理设置成电脑的局域网 IP,端口填 8080,代理环境就绪。
3.3 验证流量已解密:curl 看证书链与 CONNECT 行为
设备可能没抓包成功,也可能抓了但没解密。区分这两者很简单,在电脑本机跑一条 curl 就能验证代理本身是否工作,再把同样的标准搬到手机上。
curl -sS -v --proxy http://127.0.0.1:8080 https://api.dev.example.com/health这条命令里没有加 -k 跳过证书校验,如果 curl 能成功返回,说明本机已经信任代理 CA。重点看 -v 输出里的 Server certificate 段落:正常情况下 subject 显示的是代理 CA 的名称,说明流量经过了代理解包再重新加密。如果 curl 报证书错误,说明代理 CA 没被本机信任,这时候先去修本机信任区,不要急着手机上排查。
这个最小闭环的逻辑是:本机 curl 通过,代表代理和解密链路是通的;手机浏览器通过,代表代理网络路线是通的;只有某个小程序失败,问题才真正来到应用证书信任策略这一层。按这个顺序排查,能省掉很多盲目的重启和重装。我在多个项目里都用这个方法定位,效率明显比一开始就拿着手机看日志高。
4. 代理抓包时 SSL 验证失败的 5 个常见原因与排查顺序
这章直接给排查清单,每条按现象到原因到解决的方式写。多数情况下,SSL 验证失败跑不出这五类原因。遇到问题时按顺序过一遍,比随机换工具和重装证书靠谱得多。
4.1 手机时间不对,证书有效期判定直接失败
现象:手机连上代理后所有 HTTPS 请求都失败,而且不只是小程序,浏览器打开页面也会提示证书无效。最迷惑的是电脑上一切正常,同一代理、同一目标域名,手机就是握手失败。
原因:TLS 证书校验依赖设备当前时间。证书里有 notBefore 和 notAfter 两个字段,客户端在握手时用设备时间判断当前是否在有效期内。如果手机时间比实际时间慢了几分钟甚至更多,代理 CA 会被判定为“尚未生效”,直接拒绝握手。模拟器尤其常见,宿主机休眠后模拟器时间经常落后。
解决:把手机或模拟器的自动时间和自动时区打开,然后重启代理工具让连接重新建立。校准后别忘了清掉代理里的旧连接状态。时间偏差这个原因太不起眼,但它能让你把所有依赖证书信任的方案全部排查一遍后才发现。
4.2 Android 7+ 对用户证书的信任策略坑
现象:手机浏览器访问 HTTPS 正常,代理面板里也能看到连接记录,但某支付 App 小程序里的请求全部失败,日志显示 TLS 握手阶段客户端主动拒绝。
原因:Android 7.0 开始,应用默认只信任系统证书区。代理 CA 装在用户证书区,普通浏览器会信任,但 targetSdk 24 以上的应用默认不认。这个策略变化直接影响小程序宿主这类大体积 App,因为它们通常严格按系统安全策略实现。
解决:如果抓的是自己开发的调试包,在应用的 networkSecurityConfig 里给 debug-overrides 加上信任用户证书的配置,只影响 debug 构建。如果是模拟器环境,可以在系统证书目录里放入代理 CA,让应用按系统证书路径找到它。对线上生产包,不做绕过。要区分“自己的调试环境”和“别人的生产环境”,这是调试伦理的边界。
4.3 证书绑定把代理 CA 当成“不可信对象”
现象:代理能正常连接,只有某一个或某几个域名失败,其它域名全正常。代理日志里可以看到 ClientHello 发出去了,但紧接着连接被重置,没有完整的握手过程。
原因:客户端在 TLS 握手之外还做了一层校验,检查服务端证书的公钥或指纹是否与内置值一致。代理每次动态生成临时证书,指纹必然和内置值不匹配,连接在验证阶段就被客户端强制断开。
解决:如果目标域名是你自己负责的后端,让开发同事在测试环境提供一个关闭 pinning 的调试入口,或者把测试环境真实证书及其私钥交给代理工具,用固定证书代替动态生成证书。这里需要提醒:证书绑定是为了防止中间人攻击,生产环境不该被弱化,调试阶段的特殊配置也要限定在测试环境内。
4.4 双向认证在 CertificateRequest 这一步中断
现象:代理日志显示 TLS 握手已经进行到服务端请求客户端证书的阶段,随后服务端直接关闭连接,客户端也报错。普通单向认证的接口不会出现这个情况。
原因:服务端配置了 mTLS,要求客户端必须出示受信任的客户端证书。代理默认只有代理 CA,没有客户端身份,服务端在 CertificateRequest 之后等不到有效证书,就主动断了握手。
解决:联系后端申请测试用的客户端证书,通常是 client.crt 和 client.key 两个文件,转成 p12 再导入代理工具。
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12导入时填对 p12 密码,然后在代理工具里启用客户端证书选项。如果密码不对,握手过程会在同一个位置反复失败,日志里一般会提示 certificate unknown。
4.5 代理工具只代理了 HTTP,HTTPS 全是 CONNECT 隧道
现象:代理面板里能看到很多 CONNECT 行,但点进去看不到请求头、请求体,只能看到“建立隧道”这种信息。浏览器能出 HTTPS 页面,但抓包内容为空。
原因:代理工具没有开启“HTTPS 解密”,或者 CA 证书没有被当前系统信任。CONNECT 隧道模式下代理只是转发加密数据,看不到明文。它不是 SSL 验证失败,而是压根没有走到证书信任那一步。
解决:检查代理工具的 HTTPS 解密开关是否打开,再确认安装的 CA 和代理当前使用的 CA 是同一个。很多人的代理是从旧电脑迁移过来的,证书导出过一次,后来代理重置又生成新 CA,设备上装的还是老证书,自然解密不了。遇到这种情况,重新导出一次 CA,删掉设备旧证书后重装,再跑一遍第 3.3 小节的 curl 验证。
5. 进阶:按域名过滤小程序流量,只留自己关心的接口
代理环境跑通之后,新的困扰来了:信息太多。小程序一启动,统计、日志、推送、广告各种域名并发请求几十上百条,自己想要的接口被淹没在瀑布流里。图形界面里手点 filter 只能临时过滤,换个设备或者重新抓一次又得重新配。更好的办法是用 mitmdump 加脚本,把过滤规则写死在代码里。
5.1 全量抓包信息过载,脚本过滤更实用
为什么推荐脚本而不是 GUI 过滤器?三点理由。第一,可重复,脚本随项目仓库走,同事拉下来就能用;第二,可编程,能在保存时同时做请求体提取、字段清洗、落盘格式转换;第三,可控,脚本里可以对非目标域名直接返回响应,减少后续数据体积。图形界面的过滤器适合临时看一眼,不适合做为持续调试基础设施。
mitmproxy 的 Python 脚本接口支持 request、response、error 等事件回调。最常用的是 request:客户端请求刚发出时触发,此时还没拿到响应。如果只是想记录目标请求,在 request 里保存就够了;如果想连响应一起记录,再补一个 response 回调。
5.2 一个过滤并记录请求体的 mitmproxy 脚本
这是一个我在联调里实际用着的脚本模板,按需改域名即可。
from mitmproxy import http, ctx TARGET_DOMAINS = ("api.dev.example.com", "gateway.dev.example.com") def request(flow: http.HTTPFlow) -> None: host = flow.request.pretty_host if not host.endswith(TARGET_DOMAINS): return ctx.log.info(f"[captured] {flow.request.method} {flow.request.pretty_url}") with open("miniapp_traffic.log", "a", encoding="utf-8") as f: f.write(f"== {flow.request.method} {flow.request.pretty_url}\n") body = flow.request.raw_content or b"" f.write(body.decode("utf-8", errors="replace") + "\n")逻辑说明:host.endswith 判断域名是否匹配,用元组传给 endswith 会自动逐个比对;raw_content 是请求体的原始字节,没有请求体时返回空;decode 后面加 errors="replace" 是防止请求体里有二进制内容导致写入崩溃。ctx.log.info 会把日志打到 mitmdump 的标准输出。
运行时用下面这条命令,-s 指定脚本路径,-w 同时落盘原始流量。
mitmdump --listen-host 127.0.0.1 --listen-port 8080 -s capture.py -w flows.bin注意:脚本里我没有对非目标域名做阻断,只是 return 跳过记录。如果想让非目标请求直接断掉,可以给 flow.response 赋值一个最小响应,但那样会改变服务端行为,不建议在真实联调里用。先记录、后过滤是更安全的方式。
5.3 把过滤结果转成可回放的 requests 请求
抓包最终要落到复现和验证。保存下来的 log 文件可以通过简单解析生成回放脚本。下面是一个典型的手工整理后的回放模板。
import requests session = requests.Session() session.headers.update({"Content-Type": "application/json"}) resp = session.post( "https://api.dev.example.com/login", json={"username": "test", "password": "******"}, timeout=10, ) print(resp.status_code, resp.text)说明:把 log 里看到的目标 URL、请求头和请求体填进对应字段;密码等敏感数据从环境变量读取,不要写死在脚本里。回放前确认目标接口允许测试写入,避免把线上数据打脏。这一步的价值在于:同一个请求可以反复回放,改参数、看返回、对日志,联调效率明显提升。
6. 离线分析抓包记录:用一条 Python 脚本统计接口失败率
最后一个技巧适合抓包数据量大的时候用:不盯着 mitmproxy 界面看,而是把流量落盘成二进制 flow 文件,再写脚本离线分析。这样既能统计接口状态码分布,也能快速筛出 5xx 接口和耗时异常请求。
6.1 保存 .flow 二进制日志而不是终端输出
前文已经提到 -w flows.bin,这里强调它的价值。终端输出是即时消费,关了窗口就没了;.flow 文件保存了完整请求响应对象,可以反复读取、重新分析。很多人在现场抓了一堆包,当场不复盘,等回到工位发现窗口已经关了。落盘是给自己留后悔药。
mitmdump --listen-host 127.0.0.1 --listen-port 8080 -s capture.py -w flows.bin6.2 读回 .flow 文件做接口状态码与耗时统计
离线分析脚本不长,但很实用,放在调试工具目录里随取随用。
from mitmproxy import io, http from collections import Counter with open("flows.bin", "rb") as fp: reader = io.FlowReader(fp) status_counter = Counter() error_flows = [] for flow in reader.stream(): if not isinstance(flow, http.HTTPFlow) or not flow.response: continue req, resp = flow.request, flow.response status_counter[resp.status_code] += 1 if resp.status_code >= 500: cost = resp.timestamp - req.timestamp error_flows.append((req.pretty_url, resp.status_code, cost)) print(status_counter) for url, status, cost in error_flows: print(f"[error] {status} {cost:.3f}s {url}")说明:FlowReader 负责读取 .flow 文件;通过 isinstance 过滤非 HTTP 流量;resp.timestamp 减 req.timestamp 得到的是请求从发出到响应完成的大致耗时,虽然不精确,但用来筛选明显慢的接口足够了。
这个脚本跑一遍,就能看出哪类接口大量报 500、哪个接口响应耗时异常。带着这份统计再去翻具体请求详情,比盯着滚动日志抓瞎要精准很多。
我自己有一次在这里栽过跟头:换了两三个抓包工具都没搞定小程序抓包,最后发现只是测试机时钟慢了四分钟。那一刻我意识到,抓包这件事,90% 的坑不在工具,而在证书信任链和时间同步这种基础项。后来每到一个新环境,我第一件事永远是校准时间、验证本机 curl 是否能通过代理正常访问,再谈抓包细节。这个习惯帮我省掉了大量无用功。希望这些经验和命令能帮到你少走一段弯路。
本文还有配套的精品资源,点击获取