HTTPS 连接流程分析

HTTPS 是 HTTP 协议的安全加密版本,将 HTTP 流量封装在 TLS 加密信道中通信。为了完成这个通信过程,需要在客户端和服务器间完成加密通信协商,下面对这个过程进行分析。

抓取分析样本

为了尽可能观测到整个 HTTPS 交互过程,并尽可能在 Wireshark 中解密所有流量,我们启动一个受控的 HTTPS 服务器,发起一个 HTTPS 请求,并记录这个过程中的所有数据包。

整个实验在一个 Kali Linux 虚拟机中进行,测试流量发送与抓包解析等工作均在这个虚拟机中进行。使用 Kali Linux 虚拟机的好处在于:各种工具齐全,不需要额外安装;没有各种遥测服务的干扰,找到的包非常纯净,可以快速定位整个流程。

HTTPS 服务器使用自签名的证书和私钥,并通过 Python 的 http.server 模块快速启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 生成RSA私钥和自签名证书(无密码)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
# 启动HTTPS服务器
sudo python3 - <<EOF
import http.server, ssl
server = http.server.HTTPServer(('localhost', 443), http.server.SimpleHTTPRequestHandler)
server.socket = ssl.wrap_socket(
server.socket,
keyfile='server.key',
certfile='server.crt',
server_side=True,
ssl_version=ssl.PROTOCOL_TLS
)
print("HTTPS服务器运行中...")
server.serve_forever()
EOF

抓包工作通过 Wireshark 的终端版本开启。由于 HTTPS 服务器在 localhost 上启动,HTTPS 流量将通过 lo 回环端口收发,因此 Wireshark 也监听在回环端口上。完成后将抓包结果保存到文件中用于后续分析。

1
2
# 开始抓包
tshark -i lo -w https_capture.pcap

HTTPS 请求通过 curl 发送,相比使用火狐等浏览器携带的大量无关流量,使用 curl 可以保证流量单一,减少非必要的数据包干扰分析。若握手过程使用前向加密的协议,则持有服务器私钥和全过程流量并不能直接解密。因此需要设置环境变量使 curl 将使用的会话密钥保存为文件,并在后续的分析中使用,保证加密的 HTTPS 流量能成功解密。

1
2
3
4
# 设置SSLKEYLOGFILE环境变量
export SSLKEYLOGFILE=./tls_secrets.log
# 发送HTTPS请求(忽略证书警告)
curl -k https://localhost > /dev/null

完成搭建过程后,能够收到下面几项用于后续分析的文件。服务器私钥文件 server.key、数据包文件 https_capture.pcap、加密通信使用的密钥记录 tls_secrets.log

在新版本的 Wireshark 中,可以将密钥记录嵌入数据包文件中,这样只需要使用数据包文件即可对 HTTPS 流量进行分析。完成后可形成 decrypted.pcapng 文件。

1
editcap --inject-secrets tls,./tls_secrets.log https_capture.pcap decrypted.pcapng

样本分析(TLS 1.3)

打开含有密钥记录的抓包文件,可以得到下面的结果。

a9a780ca08b0722562210e2a6178d499.png

仅通过这一整体图,即可看出这个过程中的整体交互流程,使用时序图可以描述为下面的流程。

这是一次 TLS1.3 的 HTTPS 请求,整体流程可描述为:TCP 三次握手建立连接,TLS 握手建立可信的加密通道,在加密通道中传输 HTTP 请求和响应,TCP 关闭。

在捕获的数据包中,前两条数据包为一次被拒绝的连接。这是因为在 curl 访问 localhost 时,找到了 ::1 的 IPv6 本地地址,而这里建立的服务器没有在 IPv6 接口上监听,因此被操作系统自动拒绝并返回 RST 包。

随后在 127.0.0.1 本地 IPv4 上则成功通过 SYN,SYN-ACK,ACK 的 TCP 三次握手流程建立连接。同时,可以看到,在第三次握手的 ACK 消息中,已经携带了会话层的一部分数据。

客户端发起 TLS 握手

从第三次握手的 ACK 消息携带的数据开始,就开始了 TLS 的握手流程。

这个流程中,客户端首先发送 Client Hello 消息,其中与 TLS 1.0 兼容的固定部分携带了 Client Random、Session ID、支持的加密套件、压缩方法等信息,并在扩展部分携带了一些额外信息。这些扩展信息包括:用于基础连接的 SNI 扩展指示连接的目标域名;用于协议协商的 ALPN、supported_versions,ALPN 指示客户端支持的 TLS 上层应用使用的协议(HTTP/1.1、H2),supported_versions 指示支持的 TLS 版本,这里支持从 TLS 1.0 到 TLS 1.3 的版本;用于密钥生成、交换的 ec_point_formats、supported_groups、extend_master_secrets、psk_key_exchange_modes、key_share,key_share 给出椭圆曲线临时公钥;用于流程控制的 encrypt_then_mac、post_handshake_auth、compress_certificate、signature_algorithms,signature_algorithms 给出支持的签名算法可用于证书验证、加密流程验证,post_handshake_auth 将证书传输放在加密连接后。

服务器完成 TLS 握手

服务器收到 Client Hello 后,就可以从 Client Hello 中提供的协议版本、支持的算法、加密套件等中选择双方均支持的选项用于后续通信。选择的这些选项大部分会在返回的 Server Hello 及紧跟的 Certificate 等消息中得到体现。

此次分析的会话中,Server Hello 消息在固定部分携带了 Server Random、Session ID、选中的加密套件、压缩算法,并在扩展中携带额外信息。这些扩展包括:选中的 TLS 版本 TLS 1.3;椭圆曲线临时公钥。

利用 Client Hello、Server Hello 中的信息和客户端、服务端各自持有的私密信息,双方可以衍生出中间链路无法得到的一个共享密钥 shared_secret,用作后续过程中对称加密的基础。这个共享密钥通过椭圆曲线和非对称加密得出,其过程可理解为:双方分别产生了一对密钥对(记作 $d_C,Q_C;d_S,Q_S$),并将公钥部分在 Client/Server Hello 消息的 key_share 扩展中发送。这两个密钥对拥有 $d_C \times Q_S = d_S \times Q_C$ 的性质,由于双方持有对方的公钥和自己的私钥,因此可以得出相同的结果作为共享密钥。这些密钥对都是临时的,且私钥在协商完成后丢弃,从未离开本地,因此即使捕获所有流量也无法从中还原共享密钥。基于 shared_secret、client random、server random、握手过程哈希,可以确定性地衍生密钥进行对称加密通信。

服务端收到 Client Hello,生成 Server Hello 后,就可以得到用作对称加密密钥,且可以保证客户端收到 Server Hello 消息后可以得到相同的对称加密密钥。因此可以在 Server Hello 消息后马上进入对称加密模式通信,无需等待客户端确认。这个进入对称加密模式的决定通过 Change Cipher Spec 消息通知客户端。

Encrypted Extensions 是 TLS 1.3 要求的固定消息,即便其中没有内容也需要发送。这个消息紧跟在 Change Cipher Spec 消息后。随后发送 Certificate 消息将服务器证书链发送给客户端。再跟一个 Certificate Verify 消息,使用证书私钥对前面的握手流程进行签名。使用这个消息,客户端可以验证当前连接的服务器是否为证书声明的身份,防止攻击者伪装服务器发起攻击。

证书消息发送后,服务端就将 TLS 握手所需的全部信息发送完成,因此随后发送 Finished 消息通知客户端,服务器握手流程结束,不会再发送其他握手消息。Finished 消息中携带对握手过程的哈希,可以用于发现握手过程被篡改,并确认双方实现一致,成功完成密钥交换。

分析 Server Hello 到 Finished 中间的所有消息,可以发现它们均不需要客户端提供除 Client Hello 外的其他数据,因此可以合并在一起发送,无需等待,从而减少降低握手时间。

客户端完成握手

客户端收到 Server Hello 消息,即可得到与服务器协商一致的 TLS 版本、使用的加密套件,使用这些套件进行后续的握手流程。使用 Server Hello 中携带的椭圆曲线临时公钥和自己的临时私钥,可以衍生出与服务器一致的加密密钥。

使用这个加密密钥,可以在 Change Cipher Spec 消息对服务器发送的对称加密消息进行解密。从而在 Certificate、Certificate Verify 消息中提取服务器提供的证书链和签名认证,结合本地 CA 列表建立信任链,确认连接的服务器身份与预期的身份一致,没有被第三方监听、篡改。

确认连接可信后,客户端便可发送 Change Cipher Spec 消息通知服务器,客户端发送的消息将由衍生的对称加密密钥保护。并发送 Finished 消息完成握手过程,携带握手过程哈希以发现被篡改的握手过程并确保密钥交换有效完成。

至此双方的 TLS 握手过程结束,后续的通信将使用对称加密技术保护。

外延分析

在主体的概要流程之外,还存在一些细节值得分析。

对称加密密钥

观察抓包生成的密钥记录文件,可以发现其中记录了 5 条密钥,分别记录为:SERVER_HANDSHAKE_TRAFFIC_SECRET、EXPORTER_SECRET、SERVER_TRAFFIC_SECRET_0、CLIENT_HANDSHAKE_TRAFFIC_SECRET、CLIENT_TRAFFIC_SECRET_0。

SERVER_HANDSHAKE_TRAFFIC_SECRET 在服务器的 Change Cipher Spec 后使用,Encrypted Extensions、Certificate、Certificate Verify、Finished 都使用这条密钥加密。

EXPORTER_SECRET 是 TLS 为上层应用生成的一条密钥,在 TLS 通信本身并未使用。

SERVER_TRAFFIC_SECRET_0 在服务器发送的 Finished 后使用,此后由服务器发送的应用数据都使用这条密钥加密。

CLIENT_HANDSHAKE_TRAFFIC_SECRET 在客户端发送的 Change Cipher Spec 后使用,Finished 消息使用这条密钥加密。

CLIENT_TRAFFIC_SECRET_0 在客户端发送 Finished 后使用,此后由客户端发送的应用数据都使用这条密钥加密。

会话恢复机制

从抓包结果中可以看到一条 New Session Ticket 消息。New Session Ticket 消息可用于 TLS 会话恢复和 TLS1.3 0-RTT 优化。其中携带经过服务器加密、签名的预共享密钥信息,进行会话恢复或 0-RTT 通信时,可以将其放入 Client Hello 消息中,服务器可以从中恢复通信密钥。在 TLS 1.2 场景下,TLS 握手需要 2-RTT,使用会话恢复机制下,由于省去了密钥协商的过程,只需 1-RTT 即可恢复会话。在 TLS 1.3 场景下,默认就只需要 1-RTT 完成 TLS 握手,使用会话恢复机制,则可以实现 0-RTT 握手,在首次发送的 Client Hello 中就可以携带应用数据。

不完整的 TCP 断开流程

在标准的 TCP 断开流程中,需要经过 FIN->ACK->FIN->ACK 的四次握手流程。而此次分析样本中,只出现了客户端发起连接断开的 FIN 和服务器返回的 ACK 响应,而服务器端应发出的 FIN 则没有收到。

经过多轮反复测试,可确认为 http.server 模块使用前面的创建 HTTPS 服务器的方法都不会正常发出 FIN 包完成 TCP 连接断开。通过 curl 参数可以排除 TLS 1.3 版本、HTTP keep-alive 的影响。使用 python -m http.server 启动的 HTTP 则正常发出 FIN 包。直接请求其他在线 HTTPS 站点也有正常的 FIN 包。

百度首页样本分析 (TLS 1.2)

前面构建的本地服务器使用较新的 TLS 1.3 版本进行加密通信,下面通过对百度首页的分析来分析 TLS 1.2 的握手流程。

抓包过程

为了完成抓包,首先启动 Wireshark 监听使用的网卡端口并将抓包结果保存为文件。

1
tshark -i eth0 -w https_capture.pcap

通过 curl 发起请求即可抓到整个交互流程的数据包。同样需要设置 SSLKEYLOGFILE 环境变量来记录加密密钥用于后续解密。

1
2
export SSLKEYLOGFILE=./tls_secrets.log
curl https://baidu.com > /dev/null

取得抓包文件和密钥后同样整合为单一文件以便分析。

1
editcap --inject-secrets tls,./tls_secrets.log https_capture.pcap decrypted.pcapng

得到抓包文件 decrypted.pcapng 用于分析。

整体握手流程

打开抓包结果,得到下面的结果。

基于这个整体图,可以得到下面的 TLS 1.2 时序图。

与 TLS 1.3 不同,TLS 1.2 握手过程多一次 RTT 才完成握手过程。

客户端发起握手时,没有服务器支持的 TLS 版本相关的信息,因此发送的 Client Hello 信息与 TLS 1.3 完全一致。

服务器第一轮 TLS 握手

收到 Client Hello 后,服务器批量返回 Server Hello、Certificate、Server Key Exchange、Server Hello Done 消息。

Server Hello 消息携带选择的 TLS 版本、Server Random、Session ID 信息、选择的圆锥曲线格式信息。Certificate 信息携带服务器证书链以便客户端检查连接的主机是否为预期连接的主机,而不是第三方仿冒的其他主机。Server Key Exchange 使用 Server Hello 中选择的圆锥曲线格式信息生成一对圆锥曲线临时非对称密钥,并将其公钥放在 Server Key Exchange 中发送。此外,Server Key Exchange 还携带使用证书的私钥对握手过程进行签名,以证明服务器拥有发送的证书私钥,即服务器与证书中声明的身份一致。最后发送 Server Hello Done 消息通告 Server Hello 消息结束。

客户端 TLS 握手完成

客户端收到 Server Hello 到 Server Hello Done 的一系列消息后,得到了服务器证书链、圆锥曲线加密所需的对方公钥、由服务器证书签名的握手过程哈希。通过服务器证书链、服务器证书签名认证的握手过程哈希,客户端可以连接的对端为预期连接的真正服务器。通过圆锥曲线加密所需的对方公钥,自身再生成一对圆锥曲线非对称加密密钥,即可使用与 TLS 1.3 中相同的方式,生成一个未在握手过程中传输的主密钥。且通过服务器持有的临时私钥和客户端发送的临时公钥能生成一致的主密钥作为后续对称加密的基础。

基于这些特征,客户端在收到 Server Hello Done 消息后,发送 Client Key Exchange、Change Cipher Spec、Finished 消息,单向完成 TLS 握手。Client Key Exchange 消息中,将客户端临时生成的一对密钥中的公钥发送到服务器端。Chnage Cipher Spec 消息通告服务器端,后续消息将通过生成的主密钥衍生的密钥进行加密。Finished 消息会基于主密钥和整个握手流程生成加密、签名的验证数据,用于服务器端检查握手过程是否被篡改,握手是否成功。

服务端 TLS 握手完成

服务端收到 Client Key Exchange 消息后,通过其中携带的客户端临时公钥,结合自己发送 Server Key Exchange 消息时生成的临时私钥,生成一致的主密钥。发送 Change Cipher Spec 通告后续发送的消息基于主密钥的衍生密钥加密通信。紧跟着发送 Finished 消息通告客户端握手过程完成,并发送验证数据。客户端收到该 Finished 消息后,双方就在在 TLS 层达成了一致,可以安全地进行通信了。

外延分析

基于 RSA 系列算法的 TLS 1.2 握手

在 TLS 1.3 中,废弃了一大批较弱的加密套件。其中有部分属于非前向保密的加密套件,在过去的许多文章中都基于此类套件进行分析。

这类加密套件由客户端生成预主密钥并使用服务器证书公钥进行加密后在 Client Key Exchange 中发送到服务器。只有持有证书私钥的服务器能解密获得该预主密钥,并完成后续的握手,依靠这个过程校验服务器持有该证书。由于证书私钥长期有效,预主密钥也以加密形式在网络上出现了,一旦证书私钥泄漏,保存了过去的加密流量的攻击者就可以解密这些流量。

而前面分析的具有前向保密特性的加密套件,由于共享密钥/主密钥从未在网络上出现,且产生该密钥所用的密钥对在完成握手后就丢弃,因此任何一方都无法在会话结束、共享密钥/主密钥丢弃后从传输的网络包中进行解密。因此,即便证书的私钥泄漏,攻击者也只能利用该证书开始进行中间人攻击,解密被攻击的流量,而无法对过去的流量进行解密。

现代 HTTPS 站点配置中,一般已将非前向保密的加密套件禁用。支持此类加密套件的站点会在 SSL Labs 评级中取得 B 及以下的评分。通过 SSL Labs 检查百度站点,可以发现其评级为 C,可能基于兼容性考虑,仍支持这类过时的加密套件。下面通过限制 curl 的加密套件支持,可以得到基于 RSA 的 TLS 1.2 握手流程抓包。

1
curl --tlsv1.2 --ciphers AES128-SHA -v https://baidu.com

可以发现其握手过程相对简单,Server Hello、Certificate 后没有发送 Server Key Exchange,而是在 Client Key Exchange 中携带了 RSA Encrypted PreMaster Secret,其余流程基本一致。

TLS 1.2 的 False Start 优化

在 TLS 1.2 的握手流程中,TCP + TLS 握手,需要在第 4 个 RTT 客户端才能开始发送。其过程为:

1
2
3
4
SYN->SYN-ACK->
ACK+ClientHello->ServerHello+...+ServerHelloDone->
ClientKeyExchange+...+Finished->ChangeCipherSpec+Finished->
ApplicationData

激进地分析,可以发现第 3 个 RTT ClientKeyExchange+…+Finished 发送时,客户端已经持有了加密主密钥,理论上讲可以将应用数据直接携带在 Finished 消息后,无需等待服务器返回的 Finished,这样可以节省 1 个 RTT 的等待时间。

这种优化就是 False Start。使用前向保密加密套件情况下,相比不使用 False Start 优化,维持了相同的安全属性。若在非前向保密加密套件情况下使用 False Start 优化,则增加了私钥泄漏时失败握手的泄漏数据这种情况。因此,使用 False Start 优化要求使用前向保密加密套件。

参考资料

https://www.ssllabs.com/ssltest/analyze.html?d=baidu.com