3、TLS握手抓包实战:从ClientHello到密钥交换

好,咱们直接进入正题。上一章我们把Wireshark环境搭好了,过滤表达式也练熟了。这一章,我带你亲手抓一个真实的TLS握手包,把每一步拆开来看。

说实话,我当年刚接触TLS时,看着那一堆乱七八糟的报文头都大了。后来跟着抓了几次包,才慢慢摸到门道。你跟着我走一遍,保证比光看书强十倍。

3.1 抓包前的准备

先别急着点开始。我习惯在抓包前做两件事:

  1. 清空浏览器缓存——避免会话复用导致看不到完整握手
  2. 设置过滤表达式——直接定位到目标流量

我个人最常用的过滤表达式是:

tls.handshake.type == 1  ||  tls.handshake.type == 2  ||  tls.handshake.type == 11  ||  tls.handshake.type == 16

这个表达式能抓到ClientHello(1)、ServerHello(2)、Certificate(11)和ClientKeyExchange(16)。基本上覆盖了握手核心流程。

小技巧: 如果你只想看某个特定网站的TLS握手,可以加上 ip.addr == 目标IP。比如 ip.addr == 93.184.216.34 && tls,这样就不会被其他流量干扰。

3.2 ClientHello报文分析

好,开始抓包。访问一个HTTPS网站,停掉抓包。找到第一条TLS报文——就是ClientHello。

展开TLS协议层,你会看到这几个关键字段:

字段 含义 实战要点
Version 客户端支持的最高TLS版本 常见0x0303(TLS 1.2)或0x0304(TLS 1.3)
Random 32字节随机数 用于后续密钥生成,每次握手都不同
Cipher Suites 客户端支持的密码套件列表 按优先级排序,第一个是首选
Extension: SNI 服务器名称指示 里面就是你要访问的域名

这里有个坑,我遇到过好几次。有些客户端会把Version字段写成TLS 1.0(0x0301),但实际上它支持更高的版本。为什么?因为真正的版本协商是在后面通过Supported Versions扩展来完成的。你只看Version字段会误判。

注意: 别被Version字段骗了。TLS 1.3的ClientHello里,Version字段经常显示为0x0303(TLS 1.2),真正的版本信息藏在扩展里。这是为了兼容性做的设计。

再看Cipher Suites。我建议你重点关注第一个套件,那是客户端最想用的。比如:

TLS_AES_128_GCM_SHA256 (0x1301)

这是TLS 1.3的标配套件。如果服务端也支持,握手会快很多。

3.3 ServerHello报文分析

服务端收到ClientHello后,会回复ServerHello。你找到type为2的报文,展开看看。

ServerHello的结构和ClientHello很像,但有几个关键区别:

  • Version:这里才是实际使用的TLS版本
  • Random:服务端生成的32字节随机数
  • Cipher Suite:服务端选定的唯一套件
  • Compression Method:通常为0(不压缩)

有一次我在排查一个连接失败的问题,发现ServerHello里返回的Cipher Suite客户端根本不支持。你想想看,这怎么可能?后来查出来是负载均衡配置错了,把后端服务器的套件列表搞混了。

这里有个小细节:ServerHello里的Random字段,除了用于密钥生成,在TLS 1.3里还承担了降级保护的功能。如果服务端明明支持TLS 1.3,却回复了TLS 1.2的Random格式,客户端会直接终止连接。这是为了防止中间人攻击。

3.4 证书链验证

接下来是Certificate报文。这是整个握手中最重的部分,也是问题最多的地方。

展开Certificate报文,你会看到一个证书列表。从上到下依次是:

  1. 服务端证书——网站自己的证书
  2. 中间CA证书——签发服务端证书的机构
  3. 根CA证书——有时不发送,因为客户端本地已有

我习惯先看三个东西:

  • 证书有效期——是不是过期了?
  • 颁发者与主题——域名对不对得上?
  • 签名算法——是不是太弱了?
实战经验: 我曾经遇到一个案例,证书链里缺少中间CA证书。客户端是Windows没问题,但Android设备全部报错。因为Android不缓存中间CA。解决方案很简单——服务端把完整的证书链发全就行。

验证证书链时,Wireshark会帮你做一部分工作。在Certificate报文的底部,有个Certificate Status字段。如果显示Verified,说明Wireshark本地验证通过了。但注意,这并不代表浏览器也会通过——因为浏览器有自己的信任库。

我建议你手动检查一下证书的Subject Alternative Names (SAN)扩展。现在Chrome已经不看Common Name了,只看SAN。如果SAN里没有你访问的域名,浏览器会报安全警告。

3.5 密钥交换过程

最后一步,也是最核心的一步——密钥交换。这里分两种情况:

协议版本 密钥交换方式 关键报文
TLS 1.2 ECDHE / RSA ClientKeyExchange + ChangeCipherSpec
TLS 1.3 ECDHE(强制) ClientHello中已包含Key Share

咱们先说TLS 1.2的情况。你会看到ClientKeyExchange报文,里面包含客户端生成的公钥(如果是ECDHE)或者预主密钥(如果是RSA)。

展开ClientKeyExchange,找到Pubkey字段。这是一个椭圆曲线上的点,长度通常是65字节(未压缩格式)。服务端收到后,会用自己的私钥计算出相同的预主密钥。

然后双方各自用预主密钥、客户端随机数、服务端随机数,通过PRF函数生成主密钥。这个过程在Wireshark里看不到——因为密钥不会在网络上传输。

调试技巧: 如果你想看到解密后的TLS流量,可以在Wireshark里配置(Pre)-Master-Secret log。Chrome和Firefox都支持通过环境变量SSLKEYLOGFILE导出密钥日志。设置好之后,Wireshark就能解密所有TLS流量,方便你排查应用层问题。

对于TLS 1.3,事情简单多了。密钥交换在ClientHello和ServerHello里就完成了。你会在ClientHello的扩展里看到Key Share字段,里面直接包含了客户端的公钥。服务端回复ServerHello时,也会带上自己的公钥。这样一次往返就搞定了密钥协商。

我刚开始用TLS 1.3时,找了半天没找到ClientKeyExchange报文。后来才反应过来——TLS 1.3把密钥交换前置了,减少了握手轮次。这也是它比TLS 1.2快的原因之一。

3.6 完整握手流程一览

说了这么多,咱们用一张图把整个流程串起来。你对照着抓包结果看,会更清楚。

TLS 1.2 完整握手流程 客户端 服务端 ① ClientHello ② ServerHello + Certificate ③ ServerHelloDone ④ ClientKeyExchange ⑤ ChangeCipherSpec ⑥ Finished ⑦ ChangeCipherSpec + Finished 加密应用数据传输开始

这张图展示的是TLS 1.2的完整握手。你数一下,一共7次交互。TLS 1.3把这个压缩到了3次,效率高了不少。

嗯,到这里,TLS握手的核心流程就讲完了。你打开Wireshark,找一个HTTPS网站的抓包,对照着这一章的内容,一条一条报文看过去。看完了,你就能自己分析大部分TLS连接问题了。