#a3f0c2
7d9b4ef
0xCAFE
deadbeef
SHA-256
PADDING
⚠️ 经典密码学漏洞 · 1979年至今依然有效

SHA 长度扩展攻击

Length Extension Attack on Merkle–Damgård Hash Functions

一个完全不知道密钥的攻击者,仅凭一段哈希值,
就能构造出"看起来合法"的新签名。
这听起来像魔法,但它只是数学。

6
章节
100%
浏览器本地计算
可交互实验
向下滚动
CHAPTER 01

故事的开始:一段"看似安全"的代码

先来看看,攻击究竟发生在什么场景下。

📖 场景设定

想象你正在开发一个 Web 后台。你设计了如下的请求签名机制

  • 服务器和客户端共享一个 SECRET(攻击者不知道)
  • 客户端发请求时,对参数计算 token = SHA256(SECRET || data)
  • 服务器收到后,用同样的 SECRET 重新计算 token,比对一致才接受请求

看起来很合理对吧?毕竟攻击者不知道 SECRET,应该没法伪造 token……真的吗?

server.py · 易受攻击的签名验证
SECRET = b"my_super_secret_key"   # 16 字节的密钥

def verify(data, token):
    expected = sha256(SECRET + data).hexdigest()
    return expected == token

# 客户端发来的请求:
# data  = b"user=guest&action=read"
# token = "8e3f...c7a2"

if verify(request.data, request.token):
    execute(request.data)   # ⚠️ 致命

🎯 攻击者的目标

👀
窃听到 一对合法的 (data, token)
data = "user=guest&action=read"
token = "8e3f...c7a2"
😈
构造出 新的 (data', token')
data' = "user=guest&action=read…&action=DELETE_ALL"
token' = ???(不知道 SECRET 也能算出!)

在不知道 SECRET 的前提下,攻击者照样能算出合法的 token'。这就是长度扩展攻击

CHAPTER 02

先搞懂 SHA-256 是怎么工作的

想理解攻击,必须先看清算法的"内部构造"。这一节我们用动画拆解 SHA-256。

2.1Merkle–Damgård 结构:流水线式的压缩

SHA-1、SHA-256、MD5 等几乎所有"经典哈希"都基于这个结构。它把消息切成 512 位 的小块,像流水线一样依次"压缩"。

原始消息被切成 3 个 512-bit 块
M₁
68 65 6c 6c 6f 2c 20 73 65 63 72 65 74 5f 6b 65 79 5f …
512 bit
M₂
66 6f 72 5f 64 65 6d 6f 5f 6f 6e 6c 79 21 21 21 21 21 …
512 bit
M₃
80 00 00 00 00 00 00 00 … 00 00 00 00 00 00 00 c0 ◀ len
512 bit · 含填充+长度
初始向量 IV 256 bit · SHA-256 标准常量
a6a09e667
bbb67ae85
c3c6ef372
da54ff53a
e510e527f
f9b05688c
g1f83d9ab
h5be0cd19
最终哈希 H = H₃ 256 bit · SHA-256 输出
a--------
b--------
c--------
d--------
e--------
f--------
g--------
h--------
点击播放,观察从 IV 经过 3 次压缩函数得到最终哈希
💡
关键洞察:每一步压缩函数 f 的输入都是【上一步输出的 256-bit 状态】+【当前 512-bit 消息块】。 最终输出的哈希值 就是最后一次的状态,没做任何额外处理。 这就是漏洞的根源!

2.2填充规则:把消息凑成 512 位的整数倍

消息长度通常不是 512 的整数倍,所以 SHA-256 要先做"填充"。规则极其规范,可被预测。

追加一个 1 比特

在消息末尾追加 1,二进制形式即追加字节 0x80

追加 0 比特

追加若干 0,使消息长度模 512 等于 448(即留出 64 位放长度)。

追加 64 位长度

最后 64 位写入原始消息位长度(大端序)。

原始消息(hex): 61 62 63
原始位长度: 24 bit
填充后(512 bit / 64 字节):
■ 原始 ■ 0x80 (那个 1 比特) ■ 0x00 填充 ■ 64 位长度

2.3SHA-256 的"中间状态"长什么样?

SHA-256 的内部状态由 8 个 32 位整数(合计 256 位)组成,分别叫 H₀..H₇。每处理完一个 512-bit 块,这 8 个数就会被更新。

H₀
6a09e667
H₁
bb67ae85
H₂
3c6ef372
H₃
a54ff53a
H₄
510e527f
H₅
9b05688c
H₆
1f83d9ab
H₇
5be0cd19
这是 SHA-256 的初始状态(来自前 8 个素数平方根的小数部分前 32 位)。每个块处理完后,它们都会变成新的值。
⬇ 算法结束时
最终哈希 = H₀ ‖ H₁ ‖ H₂ ‖ H₃ ‖ H₄ ‖ H₅ ‖ H₆ ‖ H₇ (直接拼接)
🚨
所以——只要拿到最终哈希值,就等于拿到了"算法结束时的内部状态"。 把这 256 位拆成 8 个 32 位整数,就能从这个状态继续算下去,仿佛我们从未停止过 SHA-256!
CHAPTER 03

漏洞的本质:哈希值就是"中间状态"的明文

把上一章的 3 个事实串起来,攻击思路就出来了。

事实 1

哈希 = 内部状态

SHA-256 输出的 256 位就是处理完最后一个块后的 8 个 32-bit 寄存器的值,没有任何混淆

事实 2

填充规则是公开的

给定原始消息长度,任何人都能算出 SHA 在最后一块加了什么 0x80、多少个 0x00、64-bit 长度字段。

事实 3

压缩可以"接力"

只要有"中间状态" + 后续消息块,就能把 SHA-256 从那一刻继续往下算,结果完全等价于一次性计算整段。

💡

顿悟时刻

假设服务端算的是 H = SHA256(SECRET ‖ data)
攻击者拿到 H就拿到了算完 SECRET ‖ data 之后的内部状态
只要他知道 len(SECRET) + len(data),就能算出 SHA 在那时候补的填充 P, 然后把任意他想加的内容 extension 拼上去:

原签名:
SECRETdata ⟶ SHA256 ⟶ H
攻击者构造:
SECRETdataP (glue padding)extension ⟶ SHA256 ⟶ H'
H' 可以仅凭 H + extension 算出,完全不需要 SECRET
CHAPTER 04

攻击全流程动画演示

点击播放,看看一次完整的攻击是怎么发生的。每一步都用真实的 SHA-256 计算。

🎬 演示场景

服务端密钥(攻击者不可见): SECRET = "secretkey123" 🙈 攻击者不知道
已知合法请求: data = "user=alice&action=read"
攻击者抓到的合法 token: (点击播放后计算)
攻击者推测的密钥长度: len(SECRET) = 12 字节 🤔 通过尝试 1~30 暴力穷举
攻击者想注入的内容: extension = "&action=DELETE_ALL"
速度:
1

步骤 1:服务端正常计算签名

服务端先把 SECRETdata 拼起来(共 34 字节 = 272 bit),按 SHA-256 规则填充到 64 字节,做一次压缩,得到合法的 token。

2

步骤 2:攻击者拿到 token,"反推"内部状态

这个 token 就是 SHA-256 处理完那一块后的 8 个寄存器值!攻击者把 64 个 hex 字符切成 8 段,每段 8 字符 = 32-bit 整数,就还原出了 H₀..H₇

3

步骤 3:构造"胶水填充"(glue padding)

攻击者知道 SECRET ‖ data 长 34 字节(猜了 SECRET 长度)。他模拟 SHA-256 的填充逻辑,算出服务端当时补的 0x80 + 21 个 0x00 + 64-bit 长度域=272,共 30 字节,正好把 34 字节凑到 64 字节边界。

4

步骤 4:把恶意内容接在填充之后

攻击者构造伪造请求 data' = data ‖ glue_padding ‖ extension。 注意:服务端拿到 data' 后会计算 SHA256(SECRET ‖ data'),前 64 字节正好是【SECRET ‖ data ‖ glue padding】,刚好填满一个 SHA-256 块!

5

步骤 5:从中间状态继续计算 extension 部分

攻击者把第 2 步还原的 H₀..H₇ 当作"伪 IV",喂给 SHA-256 压缩函数,处理 extension(也要做填充,但这次总长度 = SECRET+data+glue+extension)。压缩结果就是 H',即新的合法 token!

6

步骤 6:服务端被欺骗,签名校验通过 ✅

攻击者把 (data', H') 发给服务端。服务端老老实实算 SHA256(SECRET ‖ data'),结果等于 H'。它无法分辨 data' 是不是伪造的。💥 攻击成功!

CHAPTER 05

亲手做一次攻击

现在轮到你了!修改任意参数,亲眼看看攻击是真的成立。

🔧 攻击者已知信息

📊 实验结果

伪造的 data':
伪造的 token H':
服务端实际计算的 SHA256(SECRET ‖ data'):
两者是否相等?

👈 在左边设置参数,按 ①②③ 顺序点击按钮即可看到完整攻击过程。

🎯 进阶:当你不知道密钥长度时怎么办?

很简单——把所有可能的密钥长度都试一遍。每个长度都生成一个候选 token,发给服务端试,能通过验证的那个就对应真实密钥长度。

CHAPTER 06

怎么防御?只用一个字:HMAC

攻击的根因是 Merkle–Damgård 结构 + 朴素的 H(K‖M) 用法。换种构造就能彻底解决。

❌ 不要这样写

H(SECRET ‖ data)

受长度扩展攻击影响。所有 Merkle–Damgård 哈希(MD5/SHA-1/SHA-256/SHA-512)都中招。

⚠️ 也不太安全

H(data ‖ SECRET)

能挡住长度扩展攻击,但若 H 存在碰撞(如 MD5/SHA-1),可被利用碰撞绕过认证。

✅ 推荐方案

HMAC-SHA256(K, data)

HMAC 用了双层哈希 + 内外密钥派生:
H((K⊕opad) ‖ H((K⊕ipad) ‖ M))
外层哈希切断了"中间状态 = 输出"的链路,长度扩展攻击立刻失效。

✅ 现代方案

SHA-3 / BLAKE2 / KMAC

SHA-3 (Keccak) 基于 Sponge 结构,天然免疫长度扩展攻击;BLAKE2 / BLAKE3 也设计了内置 MAC 模式。

🛡️ 实战建议

  • 消息认证:永远用 HMAC,不要自己拼 H(K‖M)
  • 新算法选择:偏好 SHA-3、BLAKE2/3 等无长度扩展漏洞的哈希。
  • 已有 SHA-256 也能补救:双重哈希 SHA256(SHA256(K‖M)) 也能挡住该攻击(比特币 PoW 设计就用了双 SHA-256)。
  • 原则:消息认证用专门的 MAC 算法,绝不要把哈希函数当 MAC 直接用。

📚 一图回顾整个攻击

合法签名
[SECRET] data P₁ H
VS
伪造签名
[SECRET] data P₁ extension P₂ H'
攻击者只需要 H + 长度 + extension,不需要 SECRET