Length Extension Attack on Merkle–Damgård Hash Functions
一个完全不知道密钥的攻击者,仅凭一段哈希值,
就能构造出"看起来合法"的新签名。
这听起来像魔法,但它只是数学。
先来看看,攻击究竟发生在什么场景下。
想象你正在开发一个 Web 后台。你设计了如下的请求签名机制:
SECRET(攻击者不知道)token = SHA256(SECRET || data)看起来很合理对吧?毕竟攻击者不知道 SECRET,应该没法伪造 token……真的吗?
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) # ⚠️ 致命
在不知道 SECRET 的前提下,攻击者照样能算出合法的 token'。这就是长度扩展攻击。
想理解攻击,必须先看清算法的"内部构造"。这一节我们用动画拆解 SHA-256。
SHA-1、SHA-256、MD5 等几乎所有"经典哈希"都基于这个结构。它把消息切成 512 位 的小块,像流水线一样依次"压缩"。
f 的输入都是【上一步输出的 256-bit 状态】+【当前 512-bit 消息块】。
最终输出的哈希值 就是最后一次的状态,没做任何额外处理。
这就是漏洞的根源!
消息长度通常不是 512 的整数倍,所以 SHA-256 要先做"填充"。规则极其规范,可被预测。
在消息末尾追加 1,二进制形式即追加字节 0x80。
追加若干 0,使消息长度模 512 等于 448(即留出 64 位放长度)。
最后 64 位写入原始消息的位长度(大端序)。
SHA-256 的内部状态由 8 个 32 位整数(合计 256 位)组成,分别叫 H₀..H₇。每处理完一个 512-bit 块,这 8 个数就会被更新。
把上一章的 3 个事实串起来,攻击思路就出来了。
SHA-256 输出的 256 位就是处理完最后一个块后的 8 个 32-bit 寄存器的值,没有任何混淆。
给定原始消息长度,任何人都能算出 SHA 在最后一块加了什么 0x80、多少个 0x00、64-bit 长度字段。
只要有"中间状态" + 后续消息块,就能把 SHA-256 从那一刻继续往下算,结果完全等价于一次性计算整段。
假设服务端算的是 H = SHA256(SECRET ‖ data)。
攻击者拿到 H,就拿到了算完 SECRET ‖ data 之后的内部状态。
只要他知道 len(SECRET) + len(data),就能算出 SHA 在那时候补的填充 P,
然后把任意他想加的内容 extension 拼上去:
H' 可以仅凭 H + extension 算出,完全不需要 SECRET!点击播放,看看一次完整的攻击是怎么发生的。每一步都用真实的 SHA-256 计算。
SECRET = "secretkey123" 🙈 攻击者不知道
data = "user=alice&action=read"
(点击播放后计算)
len(SECRET) = 12 字节 🤔 通过尝试 1~30 暴力穷举
extension = "&action=DELETE_ALL"
服务端先把 SECRET 和 data 拼起来(共 34 字节 = 272 bit),按 SHA-256 规则填充到 64 字节,做一次压缩,得到合法的 token。
这个 token 就是 SHA-256 处理完那一块后的 8 个寄存器值!攻击者把 64 个 hex 字符切成 8 段,每段 8 字符 = 32-bit 整数,就还原出了 H₀..H₇。
攻击者知道 SECRET ‖ data 长 34 字节(猜了 SECRET 长度)。他模拟 SHA-256 的填充逻辑,算出服务端当时补的 0x80 + 21 个 0x00 + 64-bit 长度域=272,共 30 字节,正好把 34 字节凑到 64 字节边界。
攻击者构造伪造请求 data' = data ‖ glue_padding ‖ extension。
注意:服务端拿到 data' 后会计算 SHA256(SECRET ‖ data'),前 64 字节正好是【SECRET ‖ data ‖ glue padding】,刚好填满一个 SHA-256 块!
攻击者把第 2 步还原的 H₀..H₇ 当作"伪 IV",喂给 SHA-256 压缩函数,处理 extension(也要做填充,但这次总长度 = SECRET+data+glue+extension)。压缩结果就是 H',即新的合法 token!
攻击者把 (data', H') 发给服务端。服务端老老实实算 SHA256(SECRET ‖ data'),结果等于 H'。它无法分辨 data' 是不是伪造的。💥 攻击成功!
现在轮到你了!修改任意参数,亲眼看看攻击是真的成立。
👈 在左边设置参数,按 ①②③ 顺序点击按钮即可看到完整攻击过程。
很简单——把所有可能的密钥长度都试一遍。每个长度都生成一个候选 token,发给服务端试,能通过验证的那个就对应真实密钥长度。
攻击的根因是 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 (Keccak) 基于 Sponge 结构,天然免疫长度扩展攻击;BLAKE2 / BLAKE3 也设计了内置 MAC 模式。
HMAC,不要自己拼 H(K‖M)。SHA256(SHA256(K‖M)) 也能挡住该攻击(比特币 PoW 设计就用了双 SHA-256)。