4. TLS 1.2 握手过程(上):Client Hello 报文深度解析

好,咱们正式开始抓包分析。这一章,我带你拆解 TLS 握手的第一个报文——Client Hello。

很多人觉得 Client Hello 就是个打招呼的包,没啥好看的。其实不然。这个包承载了客户端所有的“能力声明”,服务端要根据它来决定用什么算法、要不要恢复会话、甚至要不要给你发证书。说白了,握手能不能成,Client Hello 已经定了一半。

核心观点:Client Hello 是客户端向服务端提交的“能力清单”。服务端只能从清单里选,不能自己发明。

4.1 抓包前的准备

我习惯用 Chrome 访问 https://www.example.com,然后在 Wireshark 里过滤 tls.handshake.type == 1。为什么是 1?因为 Handshake Type 的枚举值里,Client Hello 就是 1。

嗯,这里要注意:如果你抓的是 HTTPS 流量,记得先关掉 TLS 1.3 的支持(在 Chrome 的 chrome://flags 里搜 TLS 1.3 并禁用),否则你看到的可能是 TLS 1.3 的 Client Hello,结构略有不同。咱们这一章专注 TLS 1.2。

4.2 Client Hello 报文结构总览

先看一张整体结构图,让你心里有个谱。

Client Hello 报文结构 TLS Record Layer (Content Type: 22, Handshake) Version: TLS 1.2 (0x0303) Random (32 bytes) 4 bytes Unix Time + 28 bytes Random Bytes Session ID (0 ~ 32 bytes) Cipher Suites (2 ~ 2^16-1 bytes) Extensions (SNI, Signature Algorithms, etc.) 长度可变,SNI 是其中最重要的扩展 Record

从上到下,依次是:版本声明、随机数、会话 ID、密码套件列表、扩展字段。咱们一个一个来啃。

4.3 Version:版本协商的起点

Wireshark 里你会看到 Version: TLS 1.2 (0x0303)。注意,这个字段是客户端能支持的最高版本,不是它一定要用这个版本。服务端如果只支持 TLS 1.1,那它会在 Server Hello 里回复 0x0302。

我曾经遇到过一个坑:某客户端的 TLS 版本字段写的是 0x0304(TLS 1.3),但实际它根本不支持 1.3 的握手流程。服务端一看,好,那就按 1.3 来,结果客户端直接挂了。后来查出来是开发人员写死了版本号。所以,版本字段一定要如实填写,别自作聪明。

4.4 Random:防重放的关键

Random 字段 32 字节,前 4 字节是 Unix 时间戳,后 28 字节是随机生成的。它的作用有两个:

  • 参与密钥派生:后续的 Master Secret 计算需要用到客户端和服务端的随机数。
  • 防止重放攻击:每次握手的随机数都不一样,攻击者无法重放旧的握手报文。

你想想看,如果没有这个随机数,攻击者录一次完整的握手,然后重放 Client Hello,服务端就会生成同样的密钥。有了随机数,每次密钥都不同,重放就没意义了。

小技巧:在 Wireshark 里展开 Random 字段,你可以看到具体的 32 字节十六进制值。我习惯把它复制出来,跟服务端的 Random 对比,确保两端没有用同一个随机数(虽然概率极低)。

4.5 Session ID:会话复用的“老办法”

Session ID 字段,长度 0 到 32 字节。如果客户端之前跟这个服务端握过手,并且服务端缓存了会话信息,客户端就会把上次的 Session ID 带过来。服务端如果还记得,就可以直接复用之前的密钥,跳过完整的握手。

但这里有个问题:Session ID 是服务端生成的,客户端只是原样返回。如果客户端第一次连接,这个字段就是空的(长度为 0)。

我记得有一次排查性能问题,发现某个客户端每次都要完整握手,明明之前已经连过。抓包一看,Session ID 每次都是 0。后来发现是服务端的会话缓存配置有问题,Session ID 发出去后服务端自己忘了。嗯,这种问题挺隐蔽的。

场景 Session ID 长度 含义
首次连接 0 客户端没有可复用的会话
会话复用 1 ~ 32 客户端希望复用该 ID 对应的会话
服务端拒绝复用 非 0 但服务端返回新 ID 服务端不认旧 ID,重新生成

4.6 Cipher Suites:密码套件列表

这是 Client Hello 里最核心的部分。客户端会列出它支持的所有密码套件,按优先级排序。服务端从中选一个它也能支持的。

一个典型的密码套件长这样:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)。拆开来看:

  • 密钥交换:ECDHE(椭圆曲线 Diffie-Hellman 临时密钥)
  • 身份认证:RSA(用 RSA 证书签名)
  • 对称加密:AES_128_GCM(128 位 AES,GCM 模式)
  • 哈希校验:SHA256(用于 PRF 和 HMAC)

我个人习惯在 Wireshark 里直接看 Cipher Suites 这一行,它会列出所有套件。如果列表里没有服务端想用的那个,握手就会失败。我曾经见过一个案例:客户端只支持 TLS_RSA_WITH_AES_128_CBC_SHA,但服务端只开了 ECDHE 的套件,结果两边谈不拢,直接握手失败。所以,密码套件列表要尽量丰富,别只放一两个。

避坑指南:我曾经遇到一个客户,他们的客户端只放了 3 个密码套件,而且都是 CBC 模式的。服务端升级后默认禁用了 CBC,结果所有连接都失败。排查了整整一天才发现是套件列表太短。建议至少放 10 个以上,覆盖不同算法族。

4.7 SNI 扩展:虚拟主机的“身份证”

SNI(Server Name Indication)是 TLS 扩展里最重要的一个。它让客户端在握手阶段就告诉服务端:我要访问的是 www.example.com,而不是 www.example.org。

为什么需要这个?因为一台服务器上可能托管了多个域名,每个域名有各自的证书。如果没有 SNI,服务端不知道客户端要访问哪个域名,只能发默认证书。客户端拿到证书后一比对,域名不匹配,就会弹出安全警告。

在 Wireshark 里,展开 Extensions → server_name,就能看到具体的域名。我经常用这个来验证客户端是不是真的在访问预期的域名。有一次排查一个 HTTPS 劫持问题,发现 Client Hello 里的 SNI 是 www.baidu.com,但实际 IP 指向了一个未知服务器。嗯,这就是典型的 DNS 劫持。

注意:SNI 是明文传输的。虽然 TLS 握手后续会加密,但 SNI 本身不加密。这意味着中间人可以看到你访问的域名。TLS 1.3 的 ECH(Encrypted Client Hello)解决了这个问题,但 TLS 1.2 里没有。

4.8 其他常见扩展

除了 SNI,Client Hello 里还有几个扩展值得关注:

  • Signature Algorithms:告诉服务端客户端支持哪些签名算法(如 RSA-SHA256、ECDSA-SHA256)。服务端在 Certificate Verify 阶段会用到。
  • Supported Groups:客户端支持的椭圆曲线(如 secp256r1、x25519)。用于 ECDHE 密钥交换。
  • EC Point Formats:椭圆曲线点的编码格式,通常就是 uncompressed。

这些扩展看似不起眼,但少了任何一个都可能导致握手失败。我建议你在 Wireshark 里把每个扩展都点开看看,熟悉它们的结构。

4.9 实战:用 Wireshark 验证

打开你的 Wireshark,过滤 tls.handshake.type == 1,然后访问一个 HTTPS 网站。选中 Client Hello 报文,在 Packet Details 面板里展开 Handshake Protocol: Client Hello。对照着咱们上面讲的内容,一个一个字段看过去。

你可能会发现,实际报文里的字段比咱们讲的还多。没关系,抓住核心的就行:Version、Random、Session ID、Cipher Suites、SNI。其他的扩展,等你遇到具体问题时再深入。

我的习惯:每次分析 TLS 问题,我第一件事就是看 Client Hello 的 Cipher Suites 列表和 SNI。这两个字段能告诉我 80% 的信息。剩下的 20% 才需要看后面的报文。