简介:面向移动端安全测试与逆向分析学习者,这份资源聚焦支付宝小程序 SSL 验证的破解与代理抓包场景,提供一套可复现的实践材料。压缩包共 3 个文件,涵盖 Python、JavaScript 与 Markdown 三种类型:Python 脚本承担注入与校验绕过的主流程,JavaScript 脚本用于小程序侧的 Hook 与请求拦截,Markdown 文档则说明使用方法、环境准备及常见问题排查思路,整体包体仅约 2KB,轻量易读、便于快速对照操作。目前已有 3034 人学习下载,说明该方向关注度较高。读者可借助脚本与说明理解 Frida 注入流程、SSL Pinning 校验的突破点以及代理抓包链路如何打通,并据此搭建本地调试环境、验证请求拦截效果、积累排错经验。适合具备一定抓包工具与逆向基础、希望进一步研究小程序通信安全的读者参考。
1. 从一次“接口看不到”说起:代理抓包卡在证书信任这一环
移动端调试最常见的情况,是接口在浏览器里能看、在 App 里抓不到。代理抓包的链路明明通了,请求也出现在代理列表里,返回却全是CONNECT隧道或 TLS 握手失败,这说明加密流量没有被成功解密。很多人把这种现象归咎于代理工具“不够强”,实际上卡点通常在证书信任链:系统不认代理根证书,或者 App 自己做了 SSL Pinning(证书锁定),在 TLS 层就把中间人挡了回去。这篇按“先看清链路、再搭环境、最后定位锁定类型”的顺序拆一遍,读者照着能判断自己卡在哪一层,也清楚哪些操作只能在自有 Demo 或获得授权的测试环境里做。
2. 代理抓包的链路拆解:从 TCP 握手到 HTTPS 解密
2.1 明文与密文的分界点
要理解抓包为什么失败,先得把 HTTPS 的信任模型摆清楚。客户端发起请求时,先和服务端完成 TCP 握手,再进入 TLS 握手:服务端出示证书链,客户端用本地信任库逐级校验,校验通过后协商出会话密钥,之后所有 HTTP 内容都在密文里。代理工具想看到明文,就必须充当“中间人”——对客户端伪装成服务端,对服务端伪装成客户端,分别建立两条 TLS 连接。这个模型成立的前提只有一个:客户端愿意信任代理签发的那张证书。一旦客户端不信任,或者 App 在代码层额外校验了服务端证书的指纹、公钥或颁发者,中间人链条就会在握手阶段断掉。
所以抓包失败可以粗分成两段:第一段是“系统不信任代理证书”,表现为浏览器能抓、App 不能抓,或者 App 内提示证书错误;第二段是“App 不信任任何非预期证书”,也就是 SSL Pinning,表现为即使系统信任了根证书,App 仍然断开连接。这两段的排查手段完全不同,混在一起看会浪费很多时间。我的习惯是先在一个普通浏览器或系统 WebView 里访问 HTTPS 站点,确认代理证书在系统层已经生效,再去碰目标 App。如果系统层都没通,谈 Pinning 没有意义。
还有一点容易被忽略:Android 7.0 之后,应用默认只信任系统证书库,用户手动安装的 CA 证书对targetSdkVersion >= 24的应用不再生效。这意味着同一台设备上,浏览器可能正常,目标 App 却直接拒绝握手。这不是 Pinning,而是信任库范围变了。区分办法是看 App 的 targetSdkVersion,以及抓包时是否出现Trust anchor for certification path not found一类的异常。把这一层搞清楚,后面配证书才不会做无用功。
2.2 中间人代理的工作模型
代理工具(mitmproxy、Charles、Fiddler 一类)的核心动作可以拆成三步。第一步,客户端把请求发给代理而不是直连服务端,这一步靠设备 Wi-Fi 里配置 HTTP 代理完成,或者用透明代理把流量重定向过来。第二步,代理收到CONNECT host:443后,返回200 Connection Established,随后用自己动态签发的证书和客户端完成 TLS 握手。第三步,代理以客户端身份去连真实服务端,拿到响应后再解密、展示、转发。整个过程中,代理的根证书必须被客户端信任,否则第二步的握手就会失败。
这个模型解释了两个常见现象。其一,如果代理只配置了 HTTP 代理而没处理 HTTPS 的CONNECT,抓到的只有域名和端口,看不到路径和 body。其二,如果 App 走了自定义的网络栈(比如某些跨平台框架自带的 HTTP 客户端),它可能根本不读取系统代理设置,流量绕过了代理,此时需要在更底层做流量重定向。很多“抓不到”的案例其实是流量压根没进代理,而不是证书问题。判断方法很简单:看代理列表里有没有目标域名出现,没有就是流量没进来,有但显示CONNECT或握手失败才是证书或 Pinning 的问题。
再往深一层,代理要解密就必须对每个域名动态签证书。签发的证书链通常是“代理根证书 → 中间证书 → 站点证书”。有些 App 会校验证书链的深度、颁发者名称或有效期,这些信息一旦和真实证书对不上,就会被判定为异常。所以选代理工具时,我一般会看它能不能自定义根证书的字段、能不能固定中间证书,以及是否支持导出 PEM 格式方便塞进测试设备的信任库。这些细节决定了后面排查时有没有可调节的旋钮。
2.3 一份可复现的最小环境清单
做移动端抓包,环境不需要很复杂,但每一件都要能说清用途。下面这份清单是我常用的最小组合,适用于自有 Demo 或书面授权的测试环境:
| 组件 | 作用 | 注意点 |
|---|---|---|
| 抓包代理(mitmproxy 或 Charles) | 生成根证书、解密 HTTPS、展示请求 | 根证书要能导出 PEM |
| 测试设备或模拟器 | 运行被测 App | 系统版本影响信任库行为 |
| 代理配置通道 | 把设备流量指向代理 | 部分 App 不读系统代理 |
| 证书安装通道 | 把代理根证书装进信任库 | Android 7+ 需区分系统/用户证书 |
| 日志工具(adb logcat 等) | 观察握手异常、证书错误 | 关注 TLS 相关关键字 |
这张表里最容易被低估的是“代理配置通道”。系统 Wi-Fi 代理只对遵循系统代理设置的应用生效,很多跨平台框架、自研网络库会忽略它。遇到这种情况,常见做法是在测试设备上做透明代理重定向,或者用支持 TUN 模式的工具把流量接管过来。但这类操作对设备有要求,也比较容易影响其他应用,建议只在隔离的测试设备上做,不要在日常主力机上折腾。
证书安装通道同样有讲究。mitmproxy 默认把根证书放在~/.mitmproxy/下,Charles 则通过菜单导出。导出后要确认格式:Android 用户证书一般要 DER 或 PEM,iOS 走描述文件安装。装完不等于生效,还要在系统设置里确认信任开关打开。我见过不少“证书装了但没信任”的情况,表现和没装一模一样,排查时先看一眼信任状态能省半小时。
3. 把环境搭起来:代理、证书与模拟器的三件套配置
3.1 代理端启动与根证书生成
先让代理跑起来,并确认它监听的地址和端口。以 mitmproxy 为例,启动命令和证书文件位置如下:
# 启动 mitmproxy,监听 8080 端口,允许局域网设备接入 mitmproxy --listen-host 0.0.0.0 --listen-port 8080 # 首次启动后会在用户目录生成根证书,确认文件存在 ls -l ~/.mitmproxy/ # 典型输出包含 mitmproxy-ca-cert.pem、mitmproxy-ca-cert.cer 等--listen-host 0.0.0.0是为了让同一局域网内的手机或模拟器能连上,只在本机调试可以省略。--listen-port选一个没被占用的端口即可。首次启动生成的mitmproxy-ca-cert.pem就是后面要装进设备的根证书。逻辑上,代理启动时会用这张根证书为每个访问的域名动态签发站点证书,所以设备信任这张根证书,就等于信任了代理签出的所有证书。
如果换成 Charles,操作路径是菜单里导出根证书,保存为.pem或.cer。导出后建议用openssl看一眼证书的颁发者和有效期,确认没导错文件:
# 查看证书基本信息,确认是代理根证书 openssl x509 -in mitmproxy-ca-cert.pem -noout -subject -issuer -dates输出里subject和issuer应该一致(自签名根证书的特征),有效期覆盖当前时间。这一步看起来多余,但能提前排除“导出了中间证书”这种低级错误。参数上没什么可调的,重点是确认文件本身可用。
3.2 设备侧安装与信任链确认
证书装到设备上有两条路径:图形界面手动安装,或者用命令行推送到系统证书目录。前者适合真机和 iOS,后者适合有 root 权限的模拟器或测试机。先看通用的手动路径。Android 上把.cer或.pem文件传到设备,进入“设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书”,选择文件并确认。iOS 上把证书文件通过 AirDrop 或本地服务传到设备,安装描述文件后,还要去“设置 → 通用 → 关于本机 → 证书信任设置”里手动打开完全信任开关,否则装了也不生效。
对于模拟器,用 adb 推送并安装更可控:
# 把根证书推到设备可访问目录 adb push mitmproxy-ca-cert.cer /sdcard/Download/ # 部分系统版本可用该命令触发证书安装界面 adb shell am start -a android.settings.SECURITY_SETTINGSadb push只是把文件放进去,真正安装仍要在设置界面完成,或者在有 root 权限时把证书写入系统证书目录。后者属于系统级改动,只在隔离的测试设备上做,并且要记得测试结束后还原,否则设备会长期信任一张代理根证书。
装完后的验证方法是:在设备浏览器里访问一个 HTTPS 站点,同时看代理列表里是否出现该域名的明文请求。如果浏览器能正常打开、代理里能看到路径和响应头,说明系统层信任已经打通。这一步是后续所有排查的基准线,基准线不过,别急着怀疑 Pinning。
3.3 Android 7+ 与 iOS 的信任范围差异
Android 7.0 起,应用默认只信任系统 CA,用户安装的证书对targetSdkVersion >= 24的应用不生效。这个变化直接导致“浏览器能抓、App 不能抓”。判断方法是在设备上查看目标 App 的 targetSdkVersion,或者直接在adb logcat里过滤 TLS 相关日志,看是否出现证书链不受信任的错误。如果确实是这个原因,处理方向有两个:在自有 Demo 的AndroidManifest.xml里通过networkSecurityConfig显式信任用户证书,或者在有 root 的测试设备上把代理证书装进系统证书目录。前者只对自有应用有效,后者是测试环境的通用手段。
iOS 的差异在于信任是分层的。安装描述文件只是把证书放进设备,完全信任需要在“证书信任设置”里单独开启。另外,iOS 对证书链的校验比较严格,代理签发的证书如果缺少必要的扩展字段,可能被直接拒绝。遇到这种情况,优先检查代理工具是否开启了完整的证书链签发,以及设备时间是否准确——时间偏差会导致证书有效期校验失败,这个坑很隐蔽,表现却和证书本身有问题一模一样。
在自有 Demo 上,我会优先用networkSecurityConfig的方式,把调试证书的信任范围限定在 debug 构建里,避免把测试配置带到发布包。这个做法既能复现抓包,又不会在正式环境留下信任后门。具体配置放到最后一章展开,先记住原则:调试信任要可开关、可回收。
4. SSL Pinning 为什么能挡住抓包:三类锁定与识别方法
4.1 证书锁定与公钥锁定
SSL Pinning 的本质是客户端不再单纯依赖系统信任库,而是在代码或配置里“记住”服务端证书的某个特征,握手时做比对。常见的有两种粒度。证书锁定是直接比对整张证书的哈希,服务端一换证书客户端就失败,维护成本高,实际项目里用得少。公钥锁定是比对证书里公钥的哈希(比如 SPKI 哈希),服务端换证书但保留同一对密钥时仍然有效,所以更常见。两者在抓包时的表现是一样的:系统信任已经打通,App 仍然在 TLS 握手阶段断开。
识别方法上,先看异常信息。Android 上常见的是SSLPeerUnverifiedException、CertificateException,iOS 上可能是NSURLErrorServerCertificateUntrusted一类的错误码。这些信息只能说明“证书校验失败”,不能区分是系统不信任还是 Pinning。区分办法是做一个对照实验:用同一个代理证书去访问一个普通 HTTPS 站点,如果正常,说明系统信任没问题,问题出在目标 App 的额外校验上。这个对照实验能快速把范围缩小到 Pinning。
再进一步,可以观察握手是否完成。如果代理侧显示 TLS 握手在 ClientHello 之后就没有下文,通常是客户端在校验证书链时主动断开;如果握手完成了但应用层立刻报错,可能是应用层还做了一次证书比对。两者的排查方向不同:前者往证书链和信任库看,后者往代码里的校验逻辑看。在没有源码的情况下,只能靠日志和异常类型反推,这也是为什么调试构建里保留详细日志很重要。
4.2 双向认证与应用层校验
除了证书锁定,还有两类机制会让抓包失败。一类是双向认证(mTLS),服务端要求客户端也出示证书,代理如果没有对应的客户端证书,握手就完不成。这种机制在金融类、企业内网类应用里比较常见。表现是代理侧能看到 ClientHello,但服务端返回握手失败,客户端报错信息里往往提到客户端证书缺失。排查时要确认服务端是否启用了 mTLS,以及测试环境有没有配发测试用客户端证书。没有授权的情况下,这类机制不应尝试绕过。
另一类是应用层校验,比如请求签名、时间戳、设备指纹。这类机制不影响 TLS 解密,代理能看到明文,但请求会因为没有正确签名而被服务端拒绝。很多人把这种情况误判成“抓包失败”,其实是抓到了但用不了。区分方法很直接:代理里能看到完整的请求和响应体,只是响应是错误码。这时候要解决的是请求构造问题,而不是证书问题。把这两类混为一谈,会在错误的方向上耗很久。
还有一种容易被忽略的情况:部分跨平台框架会把网络请求交给原生层处理,或者在启动时做一次环境检测,发现代理设置就拒绝发请求。表现是代理里完全没有目标域名,或者只有零星请求。这类检测不属于证书校验,排查时要看请求是否压根没发出,而不是看握手是否失败。判断依据是代理列表里有没有该域名的记录,没有就是流量没进来。
4.3 从日志与异常反推锁定类型
在没有源码的情况下,日志是最直接的线索。Android 上用adb logcat过滤 TLS、SSL、Certificate 相关关键字,能看到握手失败的详细原因。iOS 上可以通过设备日志或代理侧的错误信息判断。下面是一段典型的排查命令:
# 清空日志缓冲,复现一次请求,再抓取证书相关日志 adb logcat -c # 在设备上触发一次目标请求后执行 adb logcat -d | grep -iE "ssl|certificate|trust|handshake" | head -50-c清空旧日志避免干扰,-d输出当前缓冲后退出,grep用来缩小范围。日志里如果出现Trust anchor for certification path not found,偏向系统信任库问题;出现Certificate pinning failure或类似字样,偏向 Pinning;出现SSLHandshakeException但没有更细信息,需要结合代理侧握手状态判断。参数上没什么可调的,关键是复现时机要准,先清日志再触发请求。
需要说明的是,这些手段的目的是定位问题类型,而不是提供针对第三方应用的绕过方案。对自有 Demo 或获得书面授权的测试目标,可以在调试构建里关闭 Pinning 或加入测试证书白名单;对没有授权的第三方应用,正确做法是联系对方获取测试接口或沙箱环境,而不是在客户端上做对抗。这个边界要在一开始就划清楚,否则技术手段越多,越容易走偏。
5. 排查与避坑:抓不到包的六种典型现场
5.1 现象、原因与处理对照
下面这几条是我在实际调试里反复遇到的现场,按“现象 → 原因 → 解决”整理,覆盖从流量入口到证书信任再到应用层校验的各个环节。
现象一:代理列表里完全没有目标域名。原因通常是 App 没有读取系统代理设置,流量直连了服务端。跨平台框架、自研网络库、部分系统组件都可能这样。解决办法是先确认设备 Wi-Fi 代理配置正确,再用透明代理或 TUN 模式接管流量;如果只是验证接口,也可以在自有 Demo 里显式设置代理。判断是否属于这一类,看代理列表有没有任何来自该 App 的请求即可。
现象二:能看到CONNECT,但看不到路径和响应体。原因是 HTTPS 没有被解密,通常是代理根证书没被信任,或者信任范围不覆盖该应用。Android 7+ 上用户证书对高 targetSdkVersion 应用不生效是常见原因。解决办法是先确认系统层信任(浏览器访问 HTTPS 正常),再检查目标应用的 targetSdkVersion 和信任配置;自有 Demo 可以通过networkSecurityConfig放行调试证书。
现象三:系统信任已打通,App 仍在握手阶段断开。原因是应用做了 SSL Pinning,比对了证书或公钥。表现是浏览器正常、目标 App 报证书错误。解决办法是先用对照实验确认是 Pinning,再判断是证书锁定还是公钥锁定;对自有 Demo 在调试构建里关闭校验或加入测试证书,对第三方应用则走授权测试环境,不在客户端做对抗。
现象四:证书装了、开关也开了,重启后又失效。原因是部分系统在重启后会重置用户证书的信任状态,或者证书被装到了错误的存储位置。表现是刚装完能抓,重启后又不通。解决办法是确认证书安装在“用户凭据”还是“系统凭据”,测试设备上必要时重新安装并验证;同时检查设备时间是否准确,时间偏差也会导致证书校验失败。
现象五:能看到明文,但服务端返回签名错误。原因不是证书问题,而是应用层做了请求签名、时间戳或设备指纹校验。表现是代理里请求响应完整,但业务返回鉴权失败。解决办法是回到接口文档确认签名规则,用测试环境的密钥构造请求;不要把这误判成抓包失败,否则会在证书方向上白费功夫。
现象六:模拟器上正常,真机上失败。原因是模拟器和真机的系统版本、信任库策略、应用构建变体不同。表现是同一份配置在两台设备上结果不一致。解决办法是记录两台设备的系统版本、应用版本和构建类型,优先在接近目标用户的设备上复现;如果是构建变体差异,确认 debug 和 release 的信任配置是否一致,很多调试开关只在 debug 构建里生效。
把这六种现场按“流量是否进入代理 → 是否完成 TLS 解密 → 是否被应用层拒绝”三个阶段归类,排查时就不会东一榔头西一棒子。我的习惯是每次只改一个变量:先确认流量入口,再确认证书信任,最后才怀疑 Pinning 和应用层校验。变量改多了,出了问题也说不清是哪一步导致的。
6. 在自有 Demo 上做一次闭环验证:从抓包到信任配置
前面讲的多是“怎么定位”,这一章落到“怎么验证自己的配置是对的”。最稳妥的方式是在自有 Demo 上做一次完整闭环:让 App 信任调试证书、抓到明文、再关掉信任确认抓不到,以此证明配置生效且可回收。Android 侧推荐用networkSecurityConfig把调试证书的信任范围限定在 debug 构建,配置示例如下:
<!-- res/xml/network_security_config.xml --> <network-security-config> <!-- 默认不信任用户证书,保持与生产一致 --> <base-config cleartextTrafficPermitted="false"> <trust-anchors> <certificates src="system" /> </trust-anchors> </base-config> <!-- 仅 debug 构建额外信任调试证书 --> <debug-overrides> <trust-anchors> <certificates src="user" /> </trust-anchors> </debug-overrides> </network-security-config>这段配置的关键在debug-overrides:它只在android:debuggable="true"的构建里生效,release 包不会带上对用户证书的信任。base-config里保留system是为了不改变生产环境的信任基线,cleartextTrafficPermitted="false"则是顺手把明文流量关掉,避免调试配置顺带放松了其他限制。把这个文件在AndroidManifest.xml的<application>上通过android:networkSecurityConfig引用即可。参数上,src="user"表示信任用户安装的证书,正好对应我们把代理根证书装进用户凭据的操作。
配置完成后按下面的顺序验证一遍:
- 打 debug 包安装到测试设备,设备上已安装代理根证书并确认信任。
- 设备 Wi-Fi 配置代理指向抓包工具,启动 App 触发一次 HTTPS 请求。
- 在代理列表确认能看到该请求的路径、请求头和响应体。
- 换 release 包重复同样操作,确认抓不到明文,证明调试信任没有泄漏到正式构建。
- 移除设备上的代理根证书,再跑一次 debug 包,确认抓包失败,证明信任来源确实是那张证书。
这五步跑完,基本能确认“证书信任链路是通的、范围是可控的”。如果第 3 步失败,回到第 5 章的排查表逐项对照;如果第 4 步失败,说明调试配置被带进了 release,要检查构建变体和 manifest 合并结果。我一般会把这几步写进项目的调试说明里,换人接手时不用重新摸一遍。
还有一个常被忽略的点:验证时尽量用独立的测试设备,不要在日常主力机上装代理根证书。测试结束后把证书移除、代理配置清掉,避免设备长期处于“信任一张可签发任意站点证书的根证书”的状态。这不是小题大做,而是把调试手段的副作用控制在最小范围。从那以后我每次做完抓包调试,都会强制走一遍“移除证书、清代理、换回 release 包”的收尾流程,吃过一次忘记清理的亏,就不想再吃第二次。希望这些步骤能帮到你,少在证书信任这一层反复打转。
本文还有配套的精品资源,点击获取