HTTPS 中间人攻击:TLS 1.3 流量解密 CTF 实战

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_0SERVER_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 分别是 keyiv
  • context 为空
  • 哈希用 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}

六、解题思路复盘

  1. 分析 pcap:14 个包,127.0.0.1 ↔ :443,TLS 1.3,套件 TLS_AES_256_GCM_SHA384
  2. 从 keylog 提取密钥:拿到 Client/Server Traffic Secret
  3. HKDF-Expand-Label 派生 key + iv:标签用 tls13 key / tls13 iv,空 context,SHA-384
  4. AAD 里的 length 必须用记录头长度(含 GCM tag)——最大的坑
  5. GCM nonce = IV XOR (4字节0 + 8字节大端序列号)
  6. 按序解密所有 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 计算——这两处最容易错。

发表评论