08:HTTPS 抓包的八类盲区——中间人也有看不透的东西
大家好,我是毛衣哥。上一期我们把 HTTP 扒光了,这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子,七步拆完。
前七篇几乎都在说"中间人什么都能看到"。但这不是事情的全貌。
如果你是开发者,你可能遇到过这种情况:Charles 装好证书配好代理,浏览器里的 HTTPS 全都能看到。但一打开某个 App——全乱了。什么都看不到。
这不是你配置错了。是你的对手比你多走了一步。
盲区一:Certificate Pinning(证书固定)
这是最常见的"抓不到包"的原因。
原理:
普通的 TLS 校验:浏览器检查证书是否由受信任的 CA 签发 + 域名是否匹配 SAN → 通过。
Certificate Pinning:App 代码里写死了"我只接受这个特定的证书(或这个公钥的哈希)"。
OkHttpClient client = new OkHttpClient.Builder() .certificatePinner(new CertificatePinner.Builder() .add("dumpany.cn", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build()) .build();这行代码的意思是:访问 dumpany.cn 时,服务器发来的证书的公钥哈希必须等于AAAA...。即使系统信任了中间人的 CA,App 也不信任——因为中间人发的假证书的公钥哈希不是我代码里写死的那个。
绕过难度:极高。
- iOS 端:需要越狱手机,或者用 Frida/Cycript 做运行时 hook,禁用 pinning 检查
- Android 端:可以用 Xposed + JustTrustMe 模块,或者重打包 App 替换 pinning 的公钥
- 两者的核心问题都是:你需要修改 App 本身,然后重新签名——这个过程在未越狱的 iPhone 上几乎不可能
真实案例:
- 各大银行 App(招行、工行等)几乎全做了 Certificate Pinning
- 微信的部分接口做了 pinning
- 支付宝
- 几乎所有金融类 App
所以你要是想去抓银行 App 的包,只装 Charles 证书是不够的。要么用越狱机 + hook 工具,要么就别想了。
盲区二:双向 TLS(mTLS)
原理:
正常 HTTPS:只有服务器发证书,客户端不用。
双向 TLS:客户端也要发证书给服务器验证。
TLS 握手流程(mTLS): ① ClientHello ② ServerHello + Certificate(服务器证书)+ CertificateRequest(服务器要求客户端也发证书) ③ ClientKeyExchange + Certificate(客户端证书)+ CertificateVerify(客户端签名) ④ 服务器验证客户端证书 → 如果无效,拒绝连接中间人能做的是冒充服务器(发假证书给客户端),但它能不能冒充客户端?不能——因为它没有客户端的私钥。
即使中间人装了自己的 CA 能签发假服务器证书,但服务器要求的客户端证书它拿不出来。
绕过难度:极高。
- 你需要拿到 App 里内置的客户端证书和私钥
- 这些通常在 App 的资源文件里或代码里硬编码,但通常是加密存储的,需要逆向工程提取
真实案例:
- 微服务架构中 Service Mesh(Istio)的默认通信方式
- 部分高安全等级的企业内部系统
- 苹果 App Store 的部分后端
盲区三:应用层再加密
原理:
HTTPS 帮你防的是"传输途中被偷看"——但它不防服务器。TLS 解密之后,服务器看到的是明文的请求内容。
有些应用不信任这个。它们在 TLS 之上再做一层加密——即使中间人解密了 TLS,看到的也是密文。
比如 Signal 协议(WhatsApp、Signal 用的端到端加密):
传输过程中(中间人视角): TLS 层 → 能解密 → 看到的是 Signal 协议的加密 payload,而非明文消息内容 Signal 层 → 没有对方的私钥 → 无法解密绕过难度:不可能。除非你有通信双方的端到端加密密钥。
真实案例:
- WhatsApp(端到端加密)
- Signal
- Telegram 的秘密聊天
- iMessage
盲区四:非代理感知的客户端
原理:
Charles 是通过系统代理来拦截流量的。但很多 App 的实现方式是:
// Android 下绕过系统代理URLurl=newURL("https://dumpany.cn");HttpURLConnectionconn=(HttpURLConnection)url.openConnection(Proxy.NO_PROXY);不读取系统代理设置,或者直接使用Proxy.NO_PROXY。操作系统配的代理对它无效。
绕过方法:
你没办法通过"配代理"来解决这个问题。需要使用更底层的拦截方式:
- 使用 VPN 模式抓包(创建一个本地 VPN 服务,接管所有流量)
- 使用透明代理
- 在路由器层面做重定向
很多移动抓包工具(比如 Packet Capture、HttpCanary)就是用的 VPN 模式来绕过非代理感知的问题。
盲区五:QUIC / HTTP/3
原理:
HTTP/3 跑在UDP上,而不是 TCP。传统的基于 TCP 的抓包工具(Charles、Fiddler 的代理模式)对 UDP 流量无能为力。
QUIC 协议栈:
UDP(传输层) → QUIC(内置 TLS 1.3 加密,比单独的 TLS 层更难插手) → HTTP/3(应用层)QUIC 自带加密,而且是跟 TLS 1.3 深度集成的——没有单独的 TLS 握手层可以给你"插一脚"。
绕过方法:
- 在 QUIC 连接建立阶段(首次连接时),如果中间人拦截了 ClientHello,可以尝试降级到 HTTP/2
- 或者在系统层面禁用 QUIC(Chrome 的
chrome://flags/#enable-quic里可以关掉) - 关掉之后 HTTP/3 的网站会自动降级到 HTTP/2(走 TCP),就可以正常抓了
盲区六:HPACK 压缩(HTTP/2 的头部压缩)
严格来说这不是"抓不到",是"抓到了但看不懂"。
HTTP/2 使用 HPACK 压缩请求头。你在抓包工具的界面上看到的可能是:
:method: GET :path: /api/users :scheme: https :authority: dumpany.cn但实际在 TCP 流里,这些头部经过了 HPACK 压缩——使用了静态 Huffman 编码和动态表。
抓包工具需要完整实现 HPACK 解压缩才能展示 HTTP/2 请求头。如果工具不支持(比如老旧版本的 tcpdump),你看到的就是一堆压缩后的二进制内容。
Wireshark 和 Charles 都已经支持 HPACK 解压缩,所以这不是个大问题。
盲区七:WebSocket 的掩码(Masking)
原理:
WebSocket 协议规定:客户端发给服务器的帧必须加掩码(Mask)。
掩码是一个 4 字节的随机值,对 payload 做 XOR 运算:
客户端要发:hello 掩码(随机生成):0x37 0xfa 0x21 0x5d 加密(XOR):0x37⊕0x68=0x5f, 0xfa⊕0x65=0x9f, … 发送的实际数据:0x5f 0x9f 0x42 0x