跳到主要内容
加密解密2026-09-13李帅9 分钟阅读

什么是哈希碰撞?MD5 和 SHA-256 安全性深度解析

下载一个安装包时,发布方通常会附上一串哈希值。你算一遍本地文件的哈希,对比一下,相同就认为文件没被篡改。这套机制的前提是:两个不同文件几乎不可能算出同一个哈希。但「几乎不可能」取决于算法。MD5 碰撞成本早已落到秒级;SHA-1 在 2017 年被实际攻破,2020 年连 chosen-prefix 也进入可负担区间;SHA-256 至今没有公开碰撞实例。这篇拆清鸽巢原理、生日攻击、真实案例,以及 HMAC 语境下为什么「哈希已破」不等于「带密钥的 MAC 立刻垮掉」。想亲手算哈希?用 MD5 哈希生成器 或 SHA-256 哈希生成器;要和官方公布值比对,用 哈希校验器。

什么是哈希碰撞

哈希函数把任意长度的输入压缩成固定长度的输出。MD5 永远输出 128 位,SHA-256 永远输出 256 位。输入空间无限、输出空间有限,根据鸽巢原理,碰撞必然存在。

碰撞的定义很直接:两个不同的输入,产生相同的哈希值。

hash("文件A") = 9e107d9c...
hash("文件B") = 9e107d9c...   ← 与上面相同,但 文件A ≠ 文件B

存在本身不可怕。真正的问题是找到碰撞的计算成本。若平均需要约 2^128 次运算,按当前公开算力不可达,那函数在工程意义上就可视为抗碰撞。

密码学里还要分清三种「找哈希」问题:

问题攻击者目标理想 N 位哈希的量级
碰撞(collision)任意两个不同输入同哈希~2^(N/2)(生日界)
第二原像(second preimage)给定消息 m,找 m'≠m 且哈希相同~2^N
原像(preimage)给定哈希,反推任一输入~2^N

下载校验、证书签名更怕碰撞与第二原像;「MD5 解密」类需求其实是原像/彩虹表问题——见 MD5「解密」与碰撞。

生日悖论与生日攻击

直觉说:128 位空间里,碰撞概率是 2 的负 128 次方。这忽略了多次尝试的累积效应。

经典生日悖论:23 人的房间里,有两人生日相同的概率超过 50%。不是 365/2,而是远更小。原因是「任意两人配对」的数量随人数平方级增长。

对输出空间大小为 N(例如 MD5 的 N=2^128)的哈希,随机取样约 n 次时,至少出现一对碰撞的概率近似为:

P ≈ 1 − exp( − n² / (2N) )

要让 P≈50%,需要 n ≈ 1.2 √N,也就是常说的生日界:约 2^(N/2) 次哈希,而不是 2^N。

  • 128 位(MD5):生日攻击约 2^64
  • 160 位(SHA-1):约 2^80
  • 256 位(SHA-256):约 2^128

所以 SHA-256 的碰撞抗性按生日界计是 128 位,不是 256 位;256 位指的是原像抗性量级。

生日攻击只是「随机撞库」的下界。真正危险的是结构化碰撞算法:利用压缩函数差分,把成本压到远低于 2^(N/2)。MD5、SHA-1 正是这样倒下的。

identical-prefix 与 chosen-prefix

工程上常见两类碰撞构造:

  1. Identical-prefix:两个文件共享同一前缀,攻击者只在后面插入精心构造的差分块。SHAttered 的两个 PDF 属于这一类。
  2. Chosen-prefix:攻击者可分别为「合法内容」和「恶意内容」选定不同前缀,再补碰撞后缀。这对伪造证书、并立两份合同尤其致命——2008 年 rogue CA、2012 年 Flame,以及 SHA-1 的 Shambles(2020) 都依赖 chosen-prefix 能力。

校验下载时若仍用 MD5:攻击者不必「撞出和官网一模一样的恶意包」,而可以构造「恶意包与某个看似合法的包同哈希」,再诱使你信任那条哈希通道。

MD5:从破裂到日常可利用

MD5 于 1992 年前后进入广泛使用。关键时间线:

年份事件
2004王小云团队公布实用 MD5 碰撞
2007chosen-prefix 碰撞演示,可控制双文件前缀
2008利用 MD5 碰撞伪造 CA 证书
2012Flame 用 MD5 碰撞伪造微软代码签名证书
约 2010 年代中后期至今普通 CPU 上数秒级即可产出 identical-prefix 碰撞对

2008 年攻击尤其严重:两份内容不同的证书同 MD5,CA 签了「合法」那份,签名对「恶意」那份同样有效。

2026 年现状:MD5 在任何安全敏感场景都不应再承担「抗伪造指纹」的角色。非对抗场景(去重、缓存键、自有系统内快速变更检测)仍可使用,但下载完整性、证书、密码、API 防伪不要再用。更完整的用途边界见 MD5 指南。

SHA-1:SHAttered 与 Shambles

SHA-1 输出 160 位,理想生日界约 2^80。实际更弱。

  • 2017 年 SHAttered(Google + CWI):两个不同 PDF 同 SHA-1,消耗约 6500 CPU 年 + 110 GPU 年,成本约十余万美元量级。主流浏览器与证书生态随即淘汰 SHA-1 签名。
  • 2020 年 Shambles:chosen-prefix 碰撞成本降到可被资金充足的攻击者承担的量级;Git 等依赖「内容寻址 = SHA-1」的系统加速向 SHA-256 迁移。
shattered-1.pdf  →  38762cf7f55934b34d179ae6a4c80cadccbb7f0a
shattered-2.pdf  →  38762cf7f55934b34d179ae6a4c80cadccbb7f0a

2026 年现状:新系统不要用裸 SHA-1 做完整性或签名摘要。遗留 git 对象、旧支付接口仍可能要求 SHA-1——那是兼容,不是安全背书。站内 SHA-1 工具 也按「存量对接」定位。

SHA-256:至今无公开碰撞

SHA-256 输出 256 位,生日界碰撞抗性 2^128。截至 2026 年,没有任何公开的 SHA-256 碰撞实例。

暴力搜索碰撞按当前全球算力量级遥不可及;这不表示「数学上不存在碰撞」,只表示「找出来」在可见未来不现实。量子侧主要讨论 Grover 对原像的平方加速,碰撞故事仍与对称原语寿命绑定——延伸阅读:哈希算法怕量子计算吗。

算法对比与选型细节见 SHA-256 指南、SHA-256 vs MD5。

HMAC 语境:碰撞破了,MAC 是否立刻报废?

HMAC(Hash-based Message Authentication Code)是「带密钥的哈希」:

HMAC(K, m) = H( (K' ⊕ opad) ‖ H( (K' ⊕ ipad) ‖ m ) )

它解决的是另一类问题:在共享密钥的前提下,证明消息未被篡改且来自持钥方。裸哈希做下载校验时,任何人都能重算;HMAC 则不能。

和碰撞相关的几条工程判断:

  1. 威胁模型不同:公开的 MD5/SHA-1 碰撞攻击针对的是无密钥的摘要。HMAC 的伪造通常需要未知密钥下的相关密钥攻击或密钥泄露;「能撞 MD5」≠「能伪造 HMAC-MD5」。
  2. 长度扩展 ≠ 碰撞:Merkle–Damgård 结构的裸哈希(MD5/SHA-1/SHA-256)存在长度扩展问题;HMAC 的双层填充正是为挡住这类滥用。需要 MAC 时用 HMAC,不要手写 H(key ‖ message)。
  3. 算法选型仍要前进:HMAC-MD5 / HMAC-SHA1 在公开文献里没有像裸碰撞那样「几秒打穿」的实用破法,但标准与合规已淘汰它们。新项目用 HMAC-SHA256(或更强)。工具与原理见 HMAC 生成器 与 HMAC 指南。
  4. 别把场景弄反:Webhook/API 签名走 HMAC;ISO/安装包完整性走公布的 SHA-256 +(更好)签名。用 哈希校验器 比对下载哈希,不要拿它当「带密钥的 API 验签台」。

一句话:碰撞摧毁的是「摘要可当防伪指纹」的信任;HMAC 额外要求密钥。密钥丢了两者都完;密钥还在时,仍应把底层哈希升级到 SHA-256 一族。

Python 演示:概念与文件校验

下面用 MD5 只演示 API,不表示你能在本机随机撞出碰撞:

import hashlib

def md5_hex(s: str) -> str:
    return hashlib.md5(s.encode()).hexdigest()

a, b = "hello", "world"
print(md5_hex(a))  # 5d41402abc4b2a76b9719d911017c592
print(md5_hex(b))  # 7d793037a0760186574b0282f2f435e7

def verify_collision(x: str, y: str) -> bool:
    if x == y:
        return False
    return md5_hex(x) == md5_hex(y)

# 真实碰撞对是差分构造的字节序列,不是随机字符串
# 可检索公开的 MD5 collision example(Marc Stevens 等)自行验证

工程里校验文件更常见的是流式 SHA-256:

import hashlib

def file_sha256(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
actual = file_sha256("./download.bin")
print("校验通过" if actual == expected else "文件已损坏或被篡改")

浏览器里不想开终端时:把文件拖进 哈希校验器,算法选 SHA-256,粘贴发布方期望值即可。

真实世界的后果

文件完整性:MD5 校验下载时,攻击者可构造与「某个可信哈希」碰撞的恶意安装包。哈希一致,跑的却是木马。

代码签名与证书:CA、驱动签名、公证体系依赖哈希。SHA-1 被弃用,正是因为碰撞成本已进入攻击者预算。

Git 对象:历史用 SHA-1 标识 commit/tree/blob;SHAttered / Shambles 之后,官方路线转向 SHA-256。

区块链:比特币等重度依赖 SHA-256 的抗碰撞与原像假设;这也是生态迟迟不回到短摘要的原因之一。

按场景选算法

场景算法选择理由
文件完整性校验SHA-256(或更强)MD5/SHA-1 可被构造碰撞
密码存储bcrypt / Argon2快哈希利于爆破,需要慢 KDF + 盐
数字签名摘要SHA-256 或 SHA-3主流 CA / 库默认路径
消息认证(API/Webhook)HMAC-SHA256共享密钥 + 抗长度扩展结构
非安全去重 / 缓存键MD5 或 xxHash无对抗伪造动机时可换速度
内容寻址(类 Git)SHA-256SHA-1 不再适合当唯一信任根

密码存储是常见误区:SHA-256 太快。需要慢哈希时用站内 bcrypt 工具。哈希与加密的分工见 哈希 vs 加密。

用 ToolVault 计算和校验

以下工具均在浏览器本地运行,不上传原文:

操作步骤也可对照 如何校验哈希。

FAQ

SHA-256 碰撞概率有多高?

随机搜索量级约 2^128(生日界)。对当前现实攻击者不可达。理论上碰撞存在;难的是构造出来。

MD5 还能用吗?

非安全场景可以:缓存键、去重、无对抗动机的快速指纹。证书、密码、对抗性下载完整性校验不要再用。

下载校验该用 MD5 还是 SHA-256?

发布并校验 SHA-256(或更强)。MD5 的 chosen-prefix 碰撞可伪造「哈希一致但内容不同」;一致的 MD5 不能证明文件就是你想要的那份。

HMAC-MD5 是不是也彻底不能用了?

与裸 MD5 不同:没有密钥就无法直接套用公开碰撞去伪造 HMAC。但新项目没有理由继续用 HMAC-MD5/SHA1——标准与库都在推向 HMAC-SHA256。遗留验证用 HMAC 生成器 即可,设计新协议时直接上 SHA-256。

怎么快速核对发布方给的哈希?

把文件或文本与期望摘要贴进 哈希校验器,或用 SHA-256 哈希生成器 现算后人工对比。两者都在本地浏览器运行。

总结

哈希碰撞问的不是「会不会发生」,而是「找到它要花多少钱」。MD5 已便宜到可批量利用;SHA-1 在 SHAttered / Shambles 之后离开安全默认;SHA-256 的碰撞成本仍在 2^128 量级。做完整性与签名摘要时用 SHA-256 或更强;做 API 防伪用 HMAC-SHA256;把 MD5 留在去重和缓存这类不依赖抗伪造的场景。


ToolVault 工具匣 出品的开发者指南。相关工具:MD5 哈希、SHA-256 哈希、哈希校验器、HMAC 生成器。相关阅读:MD5 指南、MD5「解密」与碰撞、SHA-256 指南、HMAC 指南、哈希 vs 加密、哈希算法怕量子计算吗。更多工具见首页。

实现注记

把哈希工具搬到浏览器时测过:SubtleCrypto 算 SHA-256 的吞吐大约是纯 JS 的 8–15 倍,100MB 输入从分钟级降到秒级。浏览器不把 MD5 放进 SubtleCrypto,主要是避免给已废弃算法官方快车道——所以 MD5 路径用 SparkMD5,SHA 家族走 crypto.subtle.digest。哈希校验器 一次只跑你选中的一种算法(不是并行打出五份摘要),比对前会做大小写与首尾空白归一化;选 MD5/SHA-1 时界面会提示碰撞风险,避免「匹配 = 安全」的误读。