跳转到内容

Lucky 13 — 用毫秒级时间差把 TLS 加密看穿

待复核

Lucky 13 是一个TLS 协议的攻击:攻击者不破密钥、不挖算法漏洞,只反复发包,从服务器响应快了几微秒还是慢了几微秒里,一个字节一个字节把别人 HTTPS 流量里的密码、cookie 还原出来。

日常类比:你不知道保险柜密码,但每按一个数字,柜门会”咔哒”一下。错的数字咔哒慢,对的数字咔哒快——你只要安静地按几千次、记下时间,密码就出来了。

这个攻击的”幸运 13”指的是:TLS 在算消息认证码(MAC)时会先吞掉 13 字节的协议头。这个 13 字节是攻击者下手的支点。

不理解 Lucky 13,下面这些事都没法解释:

  • 为什么 TLS 1.3(2018)直接删掉了用了 20 年的 CBC 加密模式
  • 为什么现在浏览器里 HTTPS 几乎都是 AES-GCM 或 ChaCha20-Poly1305(这类叫 AEAD
  • 为什么”理论上安全”的密码协议会被一个时间测量打穿——工程实现的小细节能毁掉数学证明
  • 为什么一系列以”动物”命名的攻击(BEAST / CRIME / Lucky 13 / POODLE / Heartbleed)能在 2011-2014 把 TLS 整个推倒重来

Lucky 13 攻击的关键是 三个事实拼到一起

  1. TLS 1.0–1.2 的 CBC 套件用 MAC-then-Encrypt:先算消息认证码、把 MAC 拼到明文后面、再整体用 CBC 加密。解密时反过来:先 CBC 解密、再按末尾字节剥 padding、再对「剩下的明文」跑 HMAC。

  2. 剥完 padding 后,送进 HMAC 的字节数会变:攻击者改密文末块,服务器会解出不同的「padding 长度」,于是 HMAC 吃到的数据长短不同。即便实现已经把「报错路径」做成一样长,HMAC 本身算多久仍会漏馅——这才是 Lucky 13,不是早期那种「padding 错就立刻返回」的简单 padding oracle。

  3. HMAC(如 SHA-1)按 64 字节分块:数据长度刚跨过块边界时,多跑一次压缩函数,多出几微秒。攻击者把长度卡在边界两侧,反复测响应时间,反推末字节是几;逐字节推进——大约两小时可还原一个 cookie。

13 这个数字来自 TLS 协议头:5 字节记录头 + 8 字节序列号 = 13 字节。HMAC 一开始就要把这 13 字节吞进去,吞完正好让攻击的时间差对齐到容易测的位置——攻击者很”幸运”

案例 1:浏览器里看不见的”被淘汰”

Section titled “案例 1:浏览器里看不见的”被淘汰””

打开 Chrome,访问任何 HTTPS 网站,按 F12 → Security 面板。你会看到类似:

Connection - secure connection settings
TLS 1.3, X25519, AES_256_GCM

注意 AES_256_GCM——这是 AEAD 模式,没有 MAC-then-Encrypt 这一步,加密和认证一起做。 2013 年之前你看到的多半是 AES_128_CBC_SHA——CBC 模式 + 单独 MAC。Lucky 13 直接打这种组合。

可以用 OpenSSL 看支持的 cipher:

Terminal window
openssl ciphers -v 'HIGH' | grep CBC

如果输出里还有 CBC 套件,那台服务器还有”被攻击的能力”。现代 Nginx / Caddy 默认配置已经把 CBC 移到列表末尾或彻底关掉。

案例 2:Lucky 13 怎么”试”出一个字节

Section titled “案例 2:Lucky 13 怎么”试”出一个字节”

攻击者想知道某个 CBC 块 C 解密后的最后一字节 b。流程:

  1. 构造一条伪造记录,把 C 放在末块,发给服务器
  2. 服务器 CBC 解密 → 按末字节解释 padding 长度 → 对剩余字节跑 HMAC → 报错(MAC 几乎总会错)
  3. 测整段响应时间 t(差在 HMAC 多跑了几次压缩)
  4. 改密文里影响「解密后末字节」的那一字节,让服务器解出另一种 padding 长度
  5. 对 256 种候选各测很多次,看哪一种让 HMAC 输入长度跨过 64 字节边界、时间最「特别」——那就是 b

每个字节要发几千到几百万次包,才能把几微秒的差从网络抖动里捞出来。

TLS 1.3(RFC 8446,2018)的设计直接砍掉 CBC、RC4、3DES,只留 AEAD

  • AES_128_GCM_SHA256
  • AES_256_GCM_SHA384
  • CHACHA20_POLY1305_SHA256

AEAD 把”加密 + 认证”绑成一个原子操作。Lucky 13 类的”先解密看 padding,再验 MAC”漏洞结构上不存在了。

  1. “理论安全”不等于”实现安全”:CBC + HMAC 在密码学论文里有可证安全证明,但前提是”两步操作时间不可观测”。Lucky 13 证明这前提在网络上做不到

  2. 修补不是真修:OpenSSL 当时打了补丁,让 padding 错误和 MAC 错误的代码路径走一样长的时间。但补丁本身又被发现有侧信道——常时间编程极难写对。

  3. MAC-then-Encrypt 的设计天生危险:先认证再加密(Encrypt-then-MAC)顺序就没这问题,因为收方先验 MAC、错就直接拒,根本不会进解密路径。TLS 选错了顺序,付了 20 年代价。

  4. DTLS 比 TLS 还危险:DTLS(基于 UDP 的 TLS)允许重传,攻击者可以无限次试同一个密文,效率比 TLS 高一个数量级。

适用攻击的目标(2013 年时):

  • TLS 1.0 / 1.1 / 1.2 用 CBC 模式的连接
  • DTLS 1.0 / 1.2
  • OpenSSL / NSS / GnuTLS / PolarSSL / BouncyCastle 等所有主流实现

不适用

  • TLS 1.3(结构上无 CBC,已免疫)
  • AEAD 模式(AES-GCM / ChaCha20-Poly1305)
  • 启用 Encrypt-then-MAC 扩展(RFC 7366)的连接
  • 网络抖动 > 时间差的场景(攻击需要稳定测量微秒级)

OpenSSL 给 Lucky 13 打的补丁,思路是:不管 padding 对不对,都跑同样多的 HMAC 压缩。看起来简单,实现却踩了一堆坑:

// 简化示意:错的写法
if (padding_ok) {
hmac(data, real_len);
} else {
hmac(data, fake_len); // 时间不一样
}
// 对的写法(伪代码)
hmac_blocks = compute_blocks(max_possible_len);
for i in 0..hmac_blocks {
sha1_compress(...); // 永远跑 max 次
}

但这还不够:memcpy 的长度、CPU 分支预测、内存访问模式都可能泄露。真正常时间的 HMAC 一直到 2014 年才被 OpenSSL 写对。

  • 2011 BEAST(Duong-Rizzo):CBC 的 IV 可预测漏洞,把 CBC 第一刀
  • 2012 CRIME:TLS 压缩侧信道
  • 2013 Lucky 13(本文):CBC padding 时间侧信道
  • 2014 POODLE(Möller 等):SSL 3.0 的 padding oracle,逼业界彻底淘汰 SSL 3.0
  • 2014 Heartbleed(OpenSSL 内存泄露):和 Lucky 13 不同类,但同年放大了”TLS 不安全”的恐慌
  • 2018 TLS 1.3 发布:清理积累 20 年的设计债

Lucky 13 是这条链子的中间一环——单独看不致命,叠加起来推倒了整个旧 TLS

  1. 侧信道是真威胁,不是论文玩具——几微秒的时间差,跨网络也能稳定测出来
  2. 协议设计的顺序至关重要:MAC-then-Encrypt vs Encrypt-then-MAC,一字之差差 20 年
  3. 常时间编程极其难:编译器、CPU 缓存、分支预测都会让”看起来对称的代码”实际不对称
  4. AEAD 是工程上的胜利:把”加密 + 认证”绑成一个原子操作,少一个组合就少一类漏洞
  5. 协议演化靠攻击驱动:BEAST → CRIME → Lucky 13 → POODLE 这条链推动了 TLS 1.3 的彻底重构
  • aes —— AES 在 CBC 模式里被使用,Lucky 13 不破 AES,破”AES + HMAC + 顺序”的组合
  • tls-1.3 —— TLS 1.3 删掉所有 CBC 模式,结构上免疫 Lucky 13
  • heartbleed-2014 —— 同期 TLS 攻击,但走的是内存读越界路线,不同类
  • libsignal —— 现代端到端加密直接用 AEAD,吸取了 Lucky 13 的教训
  • amplification-hell-2014 —— Amplification Hell 2014 — 把家用宽带放大成几百 Gbps 的反射攻击
  • logjam-2015 —— Logjam 2015 — 全世界共用一把锁,国家级窃听者一次撬完