3、TLS握手抓包实战:从ClientHello到密钥交换
好,咱们直接进入正题。上一章我们把Wireshark环境搭好了,过滤表达式也练熟了。这一章,我带你亲手抓一个真实的TLS握手包,把每一步拆开来看。
说实话,我当年刚接触TLS时,看着那一堆乱七八糟的报文头都大了。后来跟着抓了几次包,才慢慢摸到门道。你跟着我走一遍,保证比光看书强十倍。
3.1 抓包前的准备
先别急着点开始。我习惯在抓包前做两件事:
- 清空浏览器缓存——避免会话复用导致看不到完整握手
- 设置过滤表达式——直接定位到目标流量
我个人最常用的过滤表达式是:
tls.handshake.type == 1 || tls.handshake.type == 2 || tls.handshake.type == 11 || tls.handshake.type == 16
这个表达式能抓到ClientHello(1)、ServerHello(2)、Certificate(11)和ClientKeyExchange(16)。基本上覆盖了握手核心流程。
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字段会误判。
再看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报文,你会看到一个证书列表。从上到下依次是:
- 服务端证书——网站自己的证书
- 中间CA证书——签发服务端证书的机构
- 根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里看不到——因为密钥不会在网络上传输。
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的完整握手。你数一下,一共7次交互。TLS 1.3把这个压缩到了3次,效率高了不少。
嗯,到这里,TLS握手的核心流程就讲完了。你打开Wireshark,找一个HTTPS网站的抓包,对照着这一章的内容,一条一条报文看过去。看完了,你就能自己分析大部分TLS连接问题了。