直接说结论:TLS协议的抓包分析和我们平时抓HTTP完全不是一个难度层级。很多人卡在第一步——明明抓到了几十个TLS握手报文,却不知道每个包在干什么;或者费劲用Wireshark打开了加密流量,发现除了TCP握手全是“Application Data”密文,根本看不进去。这篇文章就是要解决这两件事:先讲透TLS从握手到加密的底层机制,再手把手告诉你如何用Wireshark逐层拆解TLS报文,甚至把HTTPS流量解密成明文来看。无论是做网络运维、协议开发还是安全分析,这套方法都能直接用上。
1. TLS协议原理:从“加密隧道”到“会话密钥”
1.1 TLS究竟在保护什么
很多人对TLS的第一个误解,是以为TLS只保护HTTP流量。这是不对的。TLS工作在网络层之上、应用层之下,它本身是一层独立的记录协议,HTTP/2、SMTP、IMAP、FTP这类应用层协议跑在它上面都可以。你可以把TLS想象成一条加密管道,应用层数据先塞进这条管道,再交给TCP去传输。
那TLS实际保护了三个东西:保密性、完整性、身份真实性。
保密性靠对称加密完成,完整性靠MAC或HMAC完成,身份真实性靠数字证书和非对称签名完成。这三个目标对应着TLS握手过程中三批不同的数据交换。理解这一点非常关键,因为Wireshark分析时你看到的每一个报文,本质上都是在为这三个目标服务。
1.2 一次完整的TLS 1.2握手
以最经典的TLS 1.2握手为例,报文的顺序是这样:
- TCP三次握手,建立可靠连接。
- 客户端发送ClientHello,告诉服务器自己支持的TLS版本、密码套件、支持的扩展,还附带一个客户端随机数。
- 服务器回ServerHello,选定双方要用的TLS版本和密码套件,生成一个服务器随机数。
- 服务器发送Certificate,把服务器证书以及证书链(如果有的话)发给客户端。
- 服务器发送ServerKeyExchange,这一步只在需要密钥交换时出现。使用ECDHE时,这里会带出服务器的ECDHE公钥和签名。
- 服务器发送ServerHelloDone,表示“我说完了,该你了”。
- 客户端回ClientKeyExchange,带上客户端生成的ECDHE公钥。此时双方各自有了对方公钥,可以算出预主密钥。
- 客户端发送ChangeCipherSpec,通知服务器“从下一条开始我这边都加密了”。
- 客户端发送Finished,这是第一条加密消息,内容是之前所有握手消息的摘要,服务器要验证。
- 服务器也回ChangeCipherSpec和Finished,双方完成握手。
- 之后就是正常的HTTPS业务数据。
Wireshark里抓一次HTTPS请求,看到的顺序基本就是这样。你别看这串流程只有十来步,里面每一步都依赖前一步的结果,任何一步的证书校验失败、密钥协商失败,整条连接都会断开。
1.3 密码套件到底是个什么组合
Wireshark的ClientHello里有一长串密码套件列表,每个套件看起来都是类似这样的字符串:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
这一长串实际上是四个独立算法的组合:
- 密钥交换算法:ECDHE
- 证书签名验证算法:RSA
- 对称加密算法:AES_128_GCM
- 消息认证算法:SHA256
我们逐个拆开理解。ECDHE负责密钥协商,RSA负责验证服务器的证书签名,AES_128_GCM负责会话数据加密,SHA256负责生成握手摘要和MAC。服务器在ServerHello里选定一个套件后,后面所有动作都围绕这四个算法展开。
你如果在Wireshark里看一个密码套件还觉得抽象,我可以打个比方:ECDHE是两个人隔着帘子互相递半把钥匙,RSA是服务器先在喇叭里喊一句“我是我,这里是我的签名”,AES是双方拿到完整钥匙后开始用同一个密码本写密信,SHA256是每封信末尾都附一个只有双方能验证的校验码。四件事各干各的,合成一个密码套件,缺一不可。
1.4 TLS 1.3简化了什么
TLS 1.3比1.2最大的区别,是把握手的往返次数从两次降为一次(完整握手)。
在TLS 1.2里,客户端和服务器要来回沟通密码套件、交换密钥参数,至少两个RTT才能开始发数据。TLS 1.3把大部分工作提前了:ClientHello里直接带上客户端的密钥共享参数,服务器看到后立刻能算出会话密钥并发回自己的密钥共享参数,这样一来,第二个RTT的时候双方已经有共识密钥了。
另一个重要变化是,TLS 1.3废弃了RSA密钥交换、RC4、3DES、SHA1等老旧算法,不再支持前向保密性差的密码套件。Wireshark里抓TLS 1.3流量,你还会发现Certificate消息本身也是被加密的,这是TLS 1.3比较特殊的地方——如果你不解密,是无法看到服务器证书内容的。
2. Wireshark抓TLS的首要前提:先拿到密钥
2.1 为什么抓包抓到的全是密文
刚上手Wireshark抓HTTPS包,你会遇到一个非常打击人的现象:抓了半天,列表里确实有大几十个包,TCP三次握手、TLS握手都在,但真正包含业务数据的Application Data包,你点开以后里面是一个个十六进制字节,根本看不出HTTP的请求路径、状态码、请求头。
这不是Wireshark的问题。加密流量本来就是不给你看的。但问题在于,Wireshark本质上是个数据解析器,它只知道TLS记录层的格式,不知道加密之后的具体内容。所以如果我们要看明文HTTP内容,就必须把会话密钥喂给它。
在Wireshark里喂密钥有这么一个机制:你告诉它“我跟某一段TCP连接之间通信时用的密钥是什么”,它就可以用这段密钥去解密对应连接的所有加密记录。这涉及两个要素,一是密钥本身,二是密钥对应哪条连接。
2.2 SSLKEYLOGFILE环境变量
主流浏览器(Chrome、Firefox)都支持一个调试选项:把TLS会话密钥导出到一个外部文件。这个文件就是Wireshark可以读取的密钥日志。
Windows下设置方法最直接,命令行里执行:
set SSLKEYLOGFILE=C:\temp\tlskeys.log然后用命令行启动Chrome或Firefox:
"C:\Program Files\Google\Chrome\Application\chrome.exe"Linux下同样:
export SSLKEYLOGFILE=/tmp/tlskeys.log firefox只要这个环境变量存在,浏览器跑起来之后就会把所有新建立的TLS 1.2/1.3会话密钥追加写到这个文件里。注意是追加,如果你浏览器开了一整天,文件里会有一堆不同会话的密钥,不是只能存一条。
这里有个关键点:SSLKEYLOGFILE必须在浏览器启动之前设置,中途设置是不生效的。而且这个文件里的密钥是会话级的会话密钥,不是服务器的私钥。它只影响当前抓包,不会泄露服务器私钥,所以测试环境用是安全的。
2.3 可选的抓包位置与抓包过滤器
拿到密钥之后,下一步才是抓包。抓包位置理论上讲要离客户端和服务器的通信最近,但如果只是本机调试HTTPS,最简单的办法是直接抓本机网卡的包,或者抓回环接口。
Wireshark里先设置好抓包过滤器,只保留TCP端口443的流量,不然你抓一堆乱七八糟的UDP、其他TCP包,看着头都大:
tcp port 443也可以顺带抓一下80端口方便对照:
tcp port 443 or tcp port 80如果你是想抓某个内网服务器上的HTTPS流量,但自己没有在服务器上的执行权限,那就要考虑交换机镜像端口了。把镜像口流量引到跑Wireshark的电脑上,抓包位置离真实通信越近越好,中间穿过多次NAT或负载均衡的话,SSLKEYLOGFILE对应的会话可能对不上。
提示:如果只是为了解析HTTPS内容,建议直接在目标机器上把浏览器开起来,一步到位,不要折腾镜像。镜像更适合抓无法修改配置的第三方客户端流量。
3. Wireshark逐包拆解TLS握手:从ClientHello到Finished
3.1 用过滤表达式锁定TLS流量
进入Wireshark主界面后,第一个人人都会用的过滤器是这条:
tls它的作用是把当前抓包文件里所有TLS记录层的包一次全部筛出来。但问题在于,这个过滤条件会把所有TLS包都列出来,数量还是不少。更推荐的做法是按握手消息类型来过滤。
TLS握手消息封装在握手协议层里,每种消息类型对应一个整数值。我用得最频繁的是这几个:
- ClientHello:type为1
- ServerHello:type为2
- Certificate:type为11
- ServerKeyExchange:type为12
- ClientKeyExchange:type为16
- Finished:type为20
Wireshark里过滤 ClientHello 可以这样写:
tls.handshake.type == 1如果只看包含某个服务器IP的TLS握手,那就加一个IP条件:
tls.handshake.type == 1 and ip.addr == 192.0.2.10这组过滤表达式建议直接保存成常用过滤器,以后每次打开Wireshark直接点。
3.2 ClientHello报文的核心字段
双击第一个ClientHello包,展开记录层之后,你会看到三层嵌套:TLS记录层、握手协议层、扩展层。每个字段都不是无缘无故出现的。
先看随机数。ClientHello里有一个32字节的随机数(客户端随机数),它不是一个单纯的时间戳,而是服务端和客户端双方各自生成、后续共同参与会话密钥计算的关键材料。客户端随机数 + 服务器随机数 + 预主密钥,三者共同经过伪随机函数才能派生出最终的会话密钥。这么设计是为了保证即使某一方的随机数不够随机,最终密钥仍有一定随机性。
接下来是Cipher Suites列表。Wireshark会直接把客户端支持的所有密码套件一个个列出来,每个套件名后面还有对应的IANA编号。这里建议你只关注强套件,如果你看到列表里有TLS_RSA_WITH_AES_128_CBC_SHA之类的老套件,说明客户端兼容性设置比较旧,生产环境的客户端通常不会带这个。
然后是Extension区域里最重要的三个扩展:
- SNI(Server Name Indication),它解决的是一台服务器上托管多个HTTPS域名时,客户端如何告诉服务器“我要访问哪个域名”的问题。Wireshark里能看到这个扩展,说明客户端发送了目标域名,这对分析CDN多租户流量特别有用。
- ALPN(Application-Layer Protocol Negotiation),它的值是类似
h2、http/1.1这样的协议列表,客户端声明自己支持在TLS之上跑什么应用层协议。服务器回包时会选定一个。 - supported_versions,TLS 1.3里靠它来协商版本,因为TLS 1.3的ClientHello里版本号字段还是显示为TLS 1.0的兼容值,真正的版本信息在supported_versions扩展里。
3.3 ServerHello与服务器证书分析
服务端回包时,ServerHello里首先会选定密码套件。Wireshark会高亮显示一个字段:Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,这个是双方最终协商出的套件。
接下来是证书包。Certificate消息在TLS 1.2里是明文传输的,可以直接展开看证书内容。我会特别关注三个字段:
- 证书版本、序列号、签名算法
- 颁发者(Issuer)
- 主题(Subject)和主题备用名(Subject Alternative Name,即SAN)
- 证书有效期
主题通常包含CN和O字段,SAN则负责列出该证书能覆盖的域名。如果证书里的SAN和客户端访问的域名对不上,浏览器会报错,但在Wireshark里你只能在证书解析层面判断“这个证书确实只覆盖了A域名,但你访问的是B域名”。
注意:在很多内网HTTPS测试场景里,服务器用的是自签名证书。自签名证书在解密分析上不影响——证书只是身份认证,公钥交换、对称加密照样进行。所以Wireshark解密照样能做,不需要非得是CA签发的证书。
3.4 ServerKeyExchange与ClientKeyExchange
在ECDHE密钥交换算法下,ServerKeyExchange包里面有这样一个椭圆曲线参数块:
- EC参数:包括曲线类型和曲线名
- 公钥点:服务器的ECDHE临时公钥
- 签名:上述公钥和参数的签名
这里的“临时公钥”是每次会话新生成的,不是证书里的那个静态公钥。证书里的公钥是用来验证签名的,ECDHE公钥是用来做密钥协商的,两者角色完全不同。我用一句话概括:证书公钥是“证明你是谁”的,ECDHE公钥是“用来生成通信钥匙”的。
客户端收到这些后,会在ClientKeyExchange里同样发回一个椭圆曲线公钥点。服务端和客户端此时各自拿着对方公钥和自己的私钥,通过椭圆曲线迪菲-赫尔曼算法就能算出同一个共享秘密。
这个共享秘密就是预主密钥,它不直接用于加密,而是要经过PRF(伪随机函数)和前面那两个随机数混合,生成最终的主密钥和会话密钥。
Wireshark里如果你已经导入了密钥日志,在ClientKeyExchange之后通常能看到解密出的Finished内容。Finished消息的内容是双方对之前所有握手消息消息摘要的加密签名,目的就是为了防止握手消息被篡改——如果有人在中途改了一个字节,两边的Finished校验值就对不上,握手立刻失败。
3.5 握手完成后的加密通道状态
当Wireshark界面里出现两个“ChangeCipherSpec”包之后,后续所有Application Data包就都是密文状态了。此时左侧列表每一行包名会变成Application Data,点击去也能看到明文内容,前提是你导入了密钥。
这里需要注意:Application Data包的数量和数据量通常远远多于握手包。一个页面加载会触发几十上百个加密记录,每个记录最多16KB,实际可能一个TCP段一个。你要看HTTP内容,不要一个一个点Application Data,正确的做法是看Wireshark重组后的HTTPS流。
4. 密钥导入与HTTPS明文内容还原
4.1 Wireshark偏好设置导入密钥日志
拿到SSLKEYLOGFILE后,把它导入Wireshark的方法有几条,我推荐用偏好设置的界面操作,这样一劳永逸,重启Wireshark也有效。
打开Wireshark后,进入编辑 -> 首选项 -> Protocols -> TLS,找到(Pre)-Master-Secret log filename这一项,把SSLKEYLOGFILE的完整路径填进去,点确定。
之后重新打开抓包文件,或者重新抓包,Wireshark会自动检测TLS握手,并用密钥文件里的会话密钥去匹配每一条TCP连接。匹配成功后,原来显示为Application Data的包,扩展区会出现一个解密的Decrypted TLS层,里面就是明文HTTP内容。
4.2 再抓一次TCP流,比较加密与解密后的差异
自己动手做对比是最直观的。启动浏览器前先设好SSLKEYLOGFILE,Wireshark里应用过滤条件tls and http,然后访问任意一个支持HTTPS的网站主页。
抓完包后,单条TCP流里会同时出现TLS握手包和HTTP明文包。你可以展开其中一个HTTP GET请求:
- 请求行:
GET / HTTP/1.1 - 请求头:Host、User-Agent、Accept、Cookie等
- 响应:HTTP/1.1 200 OK,Content-Type,Content-Length等
如果你之前没有导入密钥,同样的包内容是完全不可读的。这个对比会让你牢牢记住:“加密只是协议层上的封口,解密之后就回到了我们熟悉的HTTP抓包场景”。
4.3 从解密流量里提取文件
既然TLS已经被解密,Wireshark的文件 -> 导出对象 -> HTTP就也能用了。这个功能叫“导出HTTP对象”,它会把解密后的HTTP响应体里的每个资源单独导出成一个文件。
具体操作:选中任意一个解密后的HTTP响应包,打开导出对象对话框,列表里会列出从该TCP流解析出的所有对象,包括HTML页面、JS脚本、CSS样式、图片、接口的JSON响应体等。点击保存就能落下文件。
这个功能用来分析网页引了哪些子资源、确认CDN是否命中、导出接口返回的JSON请求,效率比一个个翻报文高得多。
提示:加密流量解密后的导出对象,本质上是把HTTP/2的多路复用流重组了。Wireshark在这方面做得已经比较完善,但遇到有分片或乱序的TCP流,导出结果可能不完整。遇到这种情况,先把TCP乱序问题解决了再看导出对象。
5. TLS 1.3抓包的特殊之处与过滤技巧
5.1 TLS 1.3里哪些消息被加密了
TLS 1.3和TLS 1.2在Wireshark里的表现有非常明显的差异。最大的差异在于TLS 1.3在ServerHello之后,立刻就开始加密传输,Certificate、ServerKeyExchange这些消息全部被加密了。这意味着你在没有密钥的情况下抓一个TLS 1.3握手包,看到的结构是:
- ClientHello(明文)
- ServerHello(明文)
- ChangeCipherSpec(明文,实际上兼容用的)
- EncryptedExtensions(密文)
- Certificate(密文)
- CertificateVerify(密文)
- Finished(密文)
这种情况下,不导入密钥想分析服务器证书是做不到的。但是如果你用了SSLKEYLOGFILE,TLS 1.3的密钥可以全部解析出来,Wireshark会恢复出所有握手的内部消息。
另外要注意,TLS 1.3里ClientHello扩展中的supported_versions字段值必须仔细看。如果过滤tls.handshake.type == 1然后展开supported_versions,你就能确认服务器和客户端协商出来的版本到底是1.2还是1.3。
5.2 一条过滤表达式解决多类问题
做TLS协议分析,有几个过滤表达式我经常打成一套组合拳,日常排查非常实用。
tls.record.content_type == 23这条只看加密的Application Data记录,不看握手和告警,适合快速确认加密流量占比。
tls.handshake.type == 11只看Certificate消息,用来查证书链内容非常方便。
http2只要TLS流被解密,且应用层协商的是HTTP/2,这条就能把HTTP/2帧全部列出来。Wireshark会把HTTP/2的HEADERS帧、DATA帧、SETTINGS帧解析得很清楚。
tls.alert这个过滤条件用来抓TLS告警报文特别有用。凡是证书校验失败、版本不匹配、MAC校验失败,Wireshark里都能看到Alert包,里面有一个AlertLevel和Description字段,能直接看到像certificate_unknown、handshake_failure这类具体原因。
5.3 判断抓到的TLS流量版本
有时候你会抓到一个TLS连接,但不确定两端协商成的是什么版本。最快的方法是看ServerHello里的Version字段,Wireshark如果显示的是TLS 1.2,那说明版本协商结果就是1.2。TLS 1.3的ServerHello版本字段在supported_versions扩展里,旧的版本号字段会显示为TLS 1.0,但Wireshark会通过扩展自动识别并标记为TLS 1.3。
除此之外,你还可以用tls.version字段来做统计。Wireshark的统计菜单里有协议分级功能,它会统计所有不同TLS版本的包数量,让你一眼看出环境里还有多少老版本TLS在跑。
6. 常见问题与排查技巧实录
6.1 解密后的Application Data还是乱码
这是新手最容易踩的坑之一。明明导入密钥了,Wireshark里依然全是Application Data,展开后依然是密文。问题通常出在这么几个地方:
第一,抓包文件和密钥日志不是同一段时间的。SSLKEYLOGFILE里的每个会话密钥都有对应的TCP四元组(源IP、源端口、目标IP、目标端口),如果你抓的包不是浏览器在这段时间产生的,密钥根本对不上。
第二,TLS 1.3会话恢复场景下,有些密钥用的是外部预共享密钥,SSLKEYLOGFILE不一定记录了这部分。遇到这种情况,能做的办法是重新开一个全新的浏览器页面,强制做一次全新握手,别用会话恢复的旧连接。
第三,你设置SSLKEYLOGFILE的浏览器进程,和Wireshark抓到的那个流量不是同一个进程。比如你系统里有两个浏览器实例,抓包抓的是另一个,密钥自然配不上。解决办法是关闭所有多余浏览器进程,重新开一个干干净净的窗口。
6.2 只看到TCP三次握手,没有后续TLS包
这种情况通常是客户端或服务器主动断开了连接,原因大概率是证书校验失败。打开抓包文件,过滤tcp.connection.state == "TCP Established"看全程状态,或者看看有没有RST包。
还有一种可能是你的抓包过滤器写错了,写了tcp port 80,导致443端口的TLS流量根本没被抓进来。把过滤器改成tcp port 443 or tcp port 80再抓一次。
6.3 服务器证书显示为损坏或无法解析
如果你没有导入密钥,TLS 1.3的Certificate包本身是密文,Wireshark在tls.handshake.type == 11下根本不会看到它,所以没有Certificate包不代表证书没发。但如果你导入了密钥之后仍然看不到证书,可能是TLS 1.3的证书消息被完整解密,但证书链本身包含中间证书,Wireshark解析不完全。这种情况下你需要看的是x509过滤条件能否定位到证书,或者导出证书后自己用openssl校验。
另一个相关问题是时间不对导致的“证书有效期错误”。如果你遇到SSL_ERROR_BAD_CERT_DOMAIN或者证书过期,但那台服务器的系统时间明显不对,及时矫正系统时间即可。这个问题在虚机和测试环境里特别常见。
6.4 如何确认握手是否真的成功
判断TLS握手是否成功,有一个很直接的指标:是否出现了Finished消息双方的ChangeCipherSpec。如果客户端发了ChangeCipherSpec和Finished,但服务器迟迟不回ChangeCipherSpec,并且过了几秒连接就RST掉,那基本可以判定是证书或密钥交换时报错。
更精确的做法是看TLS Alert告警包。在Wireshark里过滤tls.alert,如果看到fatal级别告警,对应Description字段会明确告诉你失败的原因,比如bad_certificate、unsupported_extension、protocol_version等。这些告警名称基本和RFC 5246中定义的Alert描述一一对应,查起来也方便。
6.5 用密钥日志解密服务器端TLS流量
有一种场景,浏览器不在本地,而是在另一台机器上。这种情况下SSLKEYLOGFILE做不到,因为它只能在运行在目标机器上的客户端进程里导出。要抓服务器端的TLS流量并解密,有两条路:
一是直接在有服务器进程的机器上修改应用,让它支持导出TLS密钥。不同编程语言的TLS库基本都支持类似SSLKEYLOGFILE的机制,Java、OpenSSL、Python的ssl库都有对应选项,但需要改代码或加环境变量。
二是用临时接入反向代理的方式,把流量代理给一个你可以控制的进程,让它做TLS终结,再把日志导出。这种方法会改变通信路径,生产环境慎用。
如果只是做一次性能排查,我建议直接看TLS握手耗时统计就够了,不用费劲解密应用层内容。TLS握手的round trip时间可以通过Wireshark的Statistics -> TCP Stream Graph -> Round Trip Time Graph查看,也能帮你找到慢在哪一个环节。
7. 把Wireshark分析TLS变成日常习惯
这篇文章写到这儿,核心内容其实已经讲完了。最后再分享一点我自己的体会。
抓TLS包和解密TLS包,本质上是在“加密世界”和“可读世界”之间架一座桥。这座桥能不能走通,完全取决于你有没有拿到会话密钥。所以任何一次TLS分析,第一件事不是打开Wireshark,而是提前想清楚密钥从哪来。本机浏览器有SSLKEYLOGFILE,自研客户端有对应的导出接口,纯观察别人的加密流量则要借助中间设备。把这个问题解决了,后面所有拆包、看证书、查告警都是顺理成章的事。
其次,不要一上来就看TLS 1.3,它把加密握手封装得太多,信息密度反而低。刚开始学的人先拿TLS 1.2抓包练手,把ClientHello、ServerHello、ClientKeyExchange、Finished这一串流程一个个打开看,突破口比直接啃TLS 1.3快得多。
最后,Wireshark本身就是最好的教材。每点开一个字段,你就能在对应的RFC文档里找到定义,照着抓包文件边看边查,一两周时间基本就能把TLS协议从“听说过”变成“拆过”。这套基本功打牢之后,再去折腾TLS 1.3、QUIC或者更上层的HTTP/3,上手速度会快很多。