📑 本页目录(点开跳转)
10 · 哈希构造与长度扩展攻击
⏱ 26 分钟 | ⭐ 一个结构缺陷打穿了一批 API
🎯 一句话
哈希函数只能处理固定长度的输入,要处理任意长度就得迭代。
而最经典的迭代方式(Merkle–Damgård)有一个结构性后果 ——
它让 H(secret ‖ message) 这种看起来合理的认证方式变得完全不安全。
🏗️ 一、Merkle–Damgård 构造
给定一个【压缩函数】f:(n bit 状态, b bit 数据块) → n bit 状态
M = M₁ ‖ M₂ ‖ ... ‖ Mₜ (填充到分组边界)
H₀ = IV(固定常数)
H₁ = f(H₀, M₁)
H₂ = f(H₁, M₂)
...
Hₜ = f(Hₜ₋₁, Mₜ)
输出 = Hₜ ⭐ 注意:输出就是【最终的内部状态】
IV ──►[f]──►[f]──►[f]──►[f]──► 输出
▲ ▲ ▲ ▲
M₁ M₂ M₃ M₄
它的漂亮之处:
定理:若压缩函数 f 是抗碰撞的,则整个 MD 构造也是抗碰撞的。
💡 人话:只要把一个"固定长度的抗碰撞函数"做对,就自动得到"任意长度的抗碰撞函数"。
用它的哈希:MD5、SHA-1、SHA-2(SHA-256/512)—— 一大批主流哈希。
📏 填充规则(不能随便设计)
标准做法(长度填充 / MD-strengthening):
附加 1 个 '1' bit → 补 '0' 直到差 64 bit 满块 → 最后 64 bit 写【消息长度】⭐
⚠️ 为什么必须把长度编进去:
如果不编长度,"abc" 和 "abc\0\0" 填充后可能变成同样的块序列
→ 直接产生碰撞 💀
💥 二、长度扩展攻击
这是 MD 构造的结构性后果,不是实现 bug。
⭐ 核心观察:H(M) 就是处理完 M 之后的【内部状态】
→ 如果我知道 H(M) 和 M 的【长度】,
我就能把这个状态当作起点,【继续往下算】
→ 我可以算出 H(M ‖ padding ‖ 任意后缀)
【完全不需要知道 M 的内容】⭐⭐
我知道:H(secret ‖ "amount=100") ← 服务器给的认证 tag
我不知道:secret 是什么
但我能算出:H(secret ‖ "amount=100" ‖ padding ‖ "&amount=10000")
↑ 我加的
→ 伪造了一个合法的 tag 💀
🎯 这打穿了什么
❌ 一个看起来很合理的 API 认证设计:
tag = SHA256(secret_key ‖ "user=alice&role=guest")
请求里带上 tag,服务器验证
💥 攻击者:
① 拿到一个合法请求和它的 tag
② 用长度扩展,算出
tag' = SHA256(secret ‖ "user=alice&role=guest" ‖ pad ‖ "&role=admin")
③ 提交 "user=alice&role=guest<pad>&role=admin" + tag'
④ 服务器验证通过 ✅(很多解析器会用最后一个 role)
→ 提权成功 💀
💥 真实事故:Flickr 的 API 签名、多个云服务的早期签名方案、 各种自制的"用哈希做签名"的 web 应用 —— 都栽在这个上面。 攻击者只需要知道 secret 的长度(可以枚举),不需要知道内容。
✅ 三种正确的做法
| 做法 | 为什么安全 |
|---|---|
| HMAC ⭐ | 双层嵌套结构,天然免疫(第 11 章) |
| 用 SHA-3 / BLAKE2 / BLAKE3 | Sponge 结构不输出完整内部状态(见下) |
| SHA-512/256(截断版) | 输出被截断,攻击者拿不到完整状态 ⭐ |
🔑 一句话记住:永远不要用
H(key ‖ message)做认证,用 HMAC。
🧽 三、Sponge 构造(SHA-3 走的路)
- r(rate):会输出的部分
- c(capacity):永不直接输出 ⭐
- 吸收阶段:消息块 XOR 进 r 部分 → 跑一次置换 → 重复
- 挤压阶段:读出 r 部分 → 跑置换 → 再读,直到够长
为什么它免疫长度扩展:
⭐ 输出的只有 r 部分,攻击者永远看不到 c 部分
→ 拿到输出也无法重建完整内部状态
→ 无法"接着往下算"
额外的好处:
| 好处 | 说明 |
|---|---|
| 输出长度任意 | 挤压阶段想读多久读多久 → 可以当伪随机数生成器用 ⭐ |
| 同一个置换多种用途 | SHA-3、SHAKE(可扩展输出)、KMAC 共用一个核心 |
| 安全性参数清晰 | 抗碰撞强度直接由 c 决定 |
🌲 四、其他构造(了解)
| 构造 | 特点 |
|---|---|
| HAIFA | MD 的改进版,每轮混入计数器和盐 → 免疫长度扩展 |
| 树哈希(BLAKE3) | 可并行:把消息切成块,各自哈希再合并 ⭐ |
| Davies–Meyer | 用分组密码造压缩函数:f(H, M) = E_M(H) ⊕ H(SHA-2 内部就是这个) |
💡 BLAKE3 的树结构值得一提:MD 构造是严格串行的(Hᵢ 依赖 Hᵢ₋₁), 而树哈希可以多核并行,对大文件快一个数量级。
📋 五、选型建议
✅ 新项目:
├─ 通用哈希 → SHA-256(最通用)或 BLAKE3(要快)
├─ 需要认证 → HMAC-SHA256 或直接用 AEAD
├─ 密码存储 → Argon2id(第 9 章)
└─ 密钥派生 → HKDF
⚠️ 遗留系统:
├─ 看到 MD5/SHA-1 → 尽快迁移
└─ 看到 H(key‖msg) → 立刻改成 HMAC ⭐
🔗 和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 9 章 三个安全性质 | MD 构造把它们从压缩函数继承上来 |
| 第 9 章 危险用法② | 本章就是它的展开 ⭐ |
| 第 11 章 HMAC | 长度扩展攻击的解法 |
| 第 6 章 分组密码 | Davies–Meyer 用它造压缩函数 |
| 第 2 章 结构缺陷 | 恩尼格玛的教训重演:数学没问题,结构漏了 |
| Claude 资料库 · 触达生产系统 | 「标准化认证」那一节劝你别自造凭据方案——自制 API 签名 H(secret‖参数) 正是被本章这个攻击打穿的东西 ⭐ |
| 智能体教程 · 沙箱与提示注入 | 防御原则「不信任自制组件」的密码学版本:MD 构造本身没错,错的是你在它外面自己拼了一层认证 |
✅ 检查点
- Merkle–Damgård 构造的流程是什么?它的核心定理说了什么?
- 填充为什么必须把消息长度编进去?
- 长度扩展攻击的核心观察是什么?攻击者需要知道什么、不需要知道什么?
- 举一个
H(secret‖msg)被打穿的具体场景。 - 防长度扩展的三种做法是什么?
- Sponge 构造为什么免疫长度扩展?
- Sponge 的两个额外好处是什么?
- BLAKE3 的树哈希相比 MD 构造有什么优势?
👀 答案
- 把消息分块填充,从固定 IV 开始,用压缩函数 f 逐块迭代 Hᵢ = f(Hᵢ₋₁, Mᵢ),输出就是最终的内部状态。核心定理:若压缩函数抗碰撞,则整个构造抗碰撞——把固定长度的抗碰撞自动升级成任意长度的。
- 否则不同消息填充后可能变成同样的块序列(如 "abc" 和 "abc\0\0"),直接产生碰撞。
- 核心观察:H(M) 就是处理完 M 后的内部状态,所以能把它当起点继续往下算,得到 H(M‖padding‖任意后缀)。攻击者需要知道 H(M) 和 M 的长度(长度可枚举),不需要知道 M 的内容或 secret。
- API 签名
tag = SHA256(secret‖"user=alice&role=guest"):攻击者用长度扩展算出SHA256(secret‖原文‖pad‖"&role=admin")的 tag,提交后服务器验证通过,而解析器取最后一个 role → 提权。Flickr API 等真实栽过。 - ①用 HMAC(双层嵌套天然免疫)②用 SHA-3/BLAKE2/BLAKE3(Sponge 不输出完整状态)③用 SHA-512/256 这类截断输出的版本。
- 因为状态分 r(rate,会输出)和 c(capacity,永不直接输出),攻击者拿到输出也无法重建完整内部状态,就没法接着往下算。
- ①输出长度任意(挤压阶段想读多久读多久,可当 PRNG 用)②同一个置换支持多种用途(SHA-3/SHAKE/KMAC 共用核心)。
- 可并行。MD 是严格串行的(Hᵢ 依赖 Hᵢ₋₁),树哈希能把消息切块各自哈希再合并,多核并行,对大文件快一个数量级。
🛑 可以停在这里
⚡ 走神救援
Merkle–Damgård:从固定 IV 开始用压缩函数逐块迭代 Hᵢ=f(Hᵢ₋₁,Mᵢ),⭐输出就是最终内部状态;核心定理:压缩函数抗碰撞 ⟹ 整个构造抗碰撞(把固定长度自动升级成任意长度)。用它的有 MD5/SHA-1/SHA-2。填充必须编入消息长度,否则 "abc" 和 "abc\0\0" 会填成同样的块序列直接碰撞。💥⭐长度扩展攻击(结构性后果不是 bug):核心观察是H(M) 就是内部状态,所以知道 H(M) 和 M 的长度(内容和 secret 都不用知道)就能算出 H(M‖pad‖任意后缀)。打穿的典型场景:API 签名
tag=SHA256(secret‖"user=alice&role=guest"),攻击者扩展出...‖pad‖"&role=admin"的合法 tag,解析器取最后一个 role → 提权(Flickr API 等真实栽过)。✅三种解法:⭐用 HMAC(双层嵌套天然免疫)、用 SHA-3/BLAKE2/BLAKE3、用 SHA-512/256(截断输出)→ ⭐一句话:永远不要用 H(key‖msg) 做认证,用 HMAC。Sponge 构造(SHA-3):状态分 r(会输出)+ c(capacity,永不直接输出),吸收阶段消息XOR进r再跑置换,挤压阶段读r;⭐免疫长度扩展是因为拿到输出也重建不了完整状态。额外好处:输出长度任意(可当PRNG,SHAKE)、同一置换多种用途、安全参数由 c 直接决定。BLAKE3 的树哈希可并行(MD 是严格串行的),大文件快一个数量级。选型:通用 SHA-256/BLAKE3、认证 HMAC-SHA256 或 AEAD、密码 Argon2id、密钥派生 HKDF。
下一节 👉 11-MAC与认证加密.md ⭐