🏠 总目录📚 本教程 10 · 哈希构造
📑 本页目录(点开跳转)

10 · 哈希构造与长度扩展攻击

26 分钟 | ⭐ 一个结构缺陷打穿了一批 API


🎯 一句话

哈希函数只能处理固定长度的输入,要处理任意长度就得迭代而最经典的迭代方式(Merkle–Damgård)有一个结构性后果 —— 它让 H(secret ‖ message) 这种看起来合理的认证方式变得完全不安全。

服务端算的:密钥消息填充H = 摘要公开给你了攻击者做的:密钥(不知道)消息填充附加的内容新的合法摘要把 H 当成中间状态,直接接着算知道 H(密钥‖消息) 和消息长度,就能算出 H(密钥‖消息‖任意附加)⭐ 根因:Merkle–Damgård 结构的摘要【就是】它的内部状态拿到摘要 = 拿到了继续计算的起点,完全不需要知道密钥✅ 正确做法:用 HMAC(两层嵌套),或用 SHA-3(海绵结构,天然免疫)
攻击者不知道密钥,却能把公开的摘要当成中间状态接着算下去。⭐ 根因:Merkle–Damgård 的摘要就是它的内部状态。✅ 正确做法是 HMAC(两层嵌套)或 SHA-3(海绵结构,天然免疫)。

🏗️ 一、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 部分 → 跑置换 → 再读,直到够长
M₁ M₂ M₃ r c P P P P 输出(只读 r) c 部分从不直接暴露
Sponge 构造:消息块只 XOR 进 r 轨道,输出也只读 r;⭐ c(capacity)从不直接暴露,所以拿到输出也重建不出完整内部状态

为什么它免疫长度扩展

   ⭐ 输出的只有 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 构造本身没错,错的是你在它外面自己拼了一层认证

✅ 检查点

  1. Merkle–Damgård 构造的流程是什么?它的核心定理说了什么?
  2. 填充为什么必须把消息长度编进去?
  3. 长度扩展攻击的核心观察是什么?攻击者需要知道什么、不需要知道什么?
  4. 举一个 H(secret‖msg) 被打穿的具体场景。
  5. 防长度扩展的三种做法是什么?
  6. Sponge 构造为什么免疫长度扩展?
  7. Sponge 的两个额外好处是什么?
  8. BLAKE3 的树哈希相比 MD 构造有什么优势?
👀 答案
  1. 把消息分块填充,从固定 IV 开始,用压缩函数 f 逐块迭代 Hᵢ = f(Hᵢ₋₁, Mᵢ),输出就是最终的内部状态。核心定理:若压缩函数抗碰撞,则整个构造抗碰撞——把固定长度的抗碰撞自动升级成任意长度的。
  2. 否则不同消息填充后可能变成同样的块序列(如 "abc" 和 "abc\0\0"),直接产生碰撞。
  3. 核心观察:H(M) 就是处理完 M 后的内部状态,所以能把它当起点继续往下算,得到 H(M‖padding‖任意后缀)。攻击者需要知道 H(M) 和 M 的长度(长度可枚举),不需要知道 M 的内容或 secret
  4. API 签名 tag = SHA256(secret‖"user=alice&role=guest"):攻击者用长度扩展算出 SHA256(secret‖原文‖pad‖"&role=admin") 的 tag,提交后服务器验证通过,而解析器取最后一个 role → 提权。Flickr API 等真实栽过。
  5. 用 HMAC(双层嵌套天然免疫)②用 SHA-3/BLAKE2/BLAKE3(Sponge 不输出完整状态)③用 SHA-512/256 这类截断输出的版本。
  6. 因为状态分 r(rate,会输出)和 c(capacity,永不直接输出),攻击者拿到输出也无法重建完整内部状态,就没法接着往下算。
  7. 输出长度任意(挤压阶段想读多久读多久,可当 PRNG 用)②同一个置换支持多种用途(SHA-3/SHAKE/KMAC 共用核心)。
  8. 可并行。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) 做认证,用 HMACSponge 构造(SHA-3):状态分 r(会输出)+ c(capacity,永不直接输出),吸收阶段消息XOR进r再跑置换,挤压阶段读r;⭐免疫长度扩展是因为拿到输出也重建不了完整状态。额外好处:输出长度任意(可当PRNG,SHAKE)、同一置换多种用途、安全参数由 c 直接决定。BLAKE3 的树哈希可并行(MD 是严格串行的),大文件快一个数量级。选型:通用 SHA-256/BLAKE3、认证 HMAC-SHA256 或 AEAD、密码 Argon2id、密钥派生 HKDF。

下一节 👉 11-MAC与认证加密.md

打卡记录保存在你的浏览器里,首页能看到总进度