HTTPS 中间人攻击:TLS 1.3 流量解密 CTF 实战
前言
最近遇到一道很有意思的 CTF 题:题目只给了两个文件——sslkey.log(SSL 密钥日志)和 target.pcap(抓包文件),要求解出 flag。
这是典型的 HTTPS 中间人攻击(MITM) 场景:攻击者抓到了加密流量,但如果同时拿到了 SSL key log,就能把 TLS 流量还原成明文。
本文完整记录解题过程,重点讲 TLS 1.3 的密钥派生 和 AES-GCM 解密,中间踩的坑也一并分享。
一、分析流量
14 个数据包,本地回环 127.0.0.1:48908 ↔ :443,协议为 TLS 1.3,加密套件是 TLS_AES_256_GCM_SHA384。
先用 dpkt 解析 pcap,把 TLS 记录按顺序 dump 出来:
import dpkt
with open('target.pcap', 'rb') as f:
pcap = dpkt.pcap.Reader(f)
for ts, buf in pcap:
eth = dpkt.ethernet.Ethernet(buf)
ip = eth.data
tcp = ip.data
if len(tcp.data) > 0:
# TLS record: type(1) + version(2) + length(2)
rec_type = tcp.data[0]
rec_len = int.from_bytes(tcp.data[3:5], 'big')
print(f"type={rec_type:#x} len={rec_len}")
TLS 1.3 的 Application Data 记录 type=0x17,就是我们要解的对象。
二、从 keylog 提取 Traffic Secret
sslkey.log 里给了 5 个 Traffic Secret,格式是 NSS key log 标准:
CLIENT_HANDSHAKE_TRAFFIC_SECRET ...
SERVER_HANDSHAKE_TRAFFIC_SECRET ...
CLIENT_TRAFFIC_SECRET_0 ...
SERVER_TRAFFIC_SECRET_0 ...
EXPORTER_SECRET ...
我们需要的是 CLIENT_TRAFFIC_SECRET_0 和 SERVER_TRAFFIC_SECRET_0——这两个才是应用数据的密钥来源。
三、HKDF-Expand-Label 派生 key 和 IV
TLS 1.3 用 HKDF 从 Traffic Secret 派生出真正的 AES key(32 字节)和 IV(12 字节)。关键在于标签前缀 tls13 :
import hmac, hashlib
def hkdf_expand(prk, info, length, h=hashlib.sha384):
hl = h().digest_size
n = (length + hl - 1) // hl
t, okm = b'', b''
for i in range(1, n + 1):
t = hmac.new(prk, t + info + bytes([i]), h).digest()
okm += t
return okm[:length]
def hkdf_expand_label(secret, label, context, length, h=hashlib.sha384):
full_label = b'tls13 ' + label
info = (length.to_bytes(2, 'big')
+ bytes([len(full_label)]) + full_label
+ bytes([len(context)]) + context)
return hkdf_expand(secret, info, length, h)
# 应用数据密钥
ca_sec = bytes.fromhex('')
ca_key = hkdf_expand_label(ca_sec, b'key', b'', 32)
ca_iv = hkdf_expand_label(ca_sec, b'iv', b'', 12)
sa_sec = bytes.fromhex('')
sa_key = hkdf_expand_label(sa_sec, b'key', b'', 32)
sa_iv = hkdf_expand_label(sa_sec, b'iv', b'', 12)
label分别是key和ivcontext为空- 哈希用 SHA-384(因为套件是
..._SHA384)
四、AES-256-GCM 解密(重点:这个坑)
TLS 1.3 的记录结构:
+--------+--------+--------+--------+-----------------------+
| type | ver | length | encrypted_record |
| 0x17 | 0x0303 | (2 bytes) | (密文 + 16字节 GCM tag)|
+--------+--------+--------+--------+-----------------------+
GCM 参数:
- nonce =
IV XOR (4字节0 + 8字节大端序列号) - AAD =
type(1) + version(2) + length(2)
⚠️ 最容易踩的坑
AAD 里的 length 必须是 TLS 记录头的 length(包含 16 字节 GCM tag),不是纯密文长度!
我先用了密文长度,一直解密失败、tag 校验不过。改成记录头长度后,一次通过:
import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def decrypt_record(key, iv, seq, encrypted_record, rec_len):
ct = encrypted_record[:-16] # 密文
tag = encrypted_record[-16:] # GCM tag
nonce = bytes(a ^ b for a, b in
zip(iv, b'x00x00x00x00' + seq.to_bytes(8, 'big')))
# 关键:rec_len 是记录头里的长度(含 tag)
aad = struct.pack('!BHH', 0x17, 0x0303, rec_len)
d = Cipher(algorithms.AES(key), modes.GCM(nonce, tag),
backend=default_backend()).decryptor()
d.authenticate_additional_data(aad)
return d.update(ct) + d.finalize()
序列号(seq)每条记录自增,Client 和 Server 各算各的。
五、解出明文
解密成功后,客户端 HTTP 请求直接现形——flag 就在 POST 请求体里:
客户端请求:
POST / HTTP/1.1
Host: ctfshow.com
User-Agent: curl/7.81.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
flag=CTF{https_secret_data}
服务端响应:
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.10.12
Date: Mon, 07 Jul 2025 16:46:54 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 5
OK...
Flag:CTF{https_secret_data}
六、解题思路复盘
- 分析 pcap:14 个包,
127.0.0.1 ↔ :443,TLS 1.3,套件TLS_AES_256_GCM_SHA384 - 从 keylog 提取密钥:拿到 Client/Server Traffic Secret
- HKDF-Expand-Label 派生 key + iv:标签用
tls13 key/tls13 iv,空 context,SHA-384 - AAD 里的 length 必须用记录头长度(含 GCM tag)——最大的坑
- GCM nonce =
IV XOR (4字节0 + 8字节大端序列号) - 按序解密所有 AppData 记录,从 POST 体拿到 flag
七、总结
HTTPS MITM 的核心逻辑:中间人抓包拿到的是密文,但一旦 key log 泄露,加密形同虚设。
这题最大的收获是实操了一遍 TLS 1.3 密钥派生链:
Traffic Secret
└─ HKDF-Expand-Label("tls13 key") ──> AES-256 key
└─ HKDF-Expand-Label("tls13 iv") ──> 12 字节 IV
↓
nonce = IV XOR seq (计数器)
AAD = 记录头 (type + version + length)
↓
AES-256-GCM 解密
比起直接开 Wireshark(配置 SSLKEYLOGFILE 一步到位),手写解密更能理解 TLS 1.3 的细节,尤其是 AAD 构造 和 nonce 计算——这两处最容易错。