MD5 曾经是文件完整性校验和数字签名的首选哈希函数。但自 2004 年以来,研究人员已经展示了能够打破其安全保证的实用碰撞攻击。本文解释 MD5 碰撞的工作原理、为何重要,以及你应该使用什么替代方案。

什么是哈希碰撞?
哈希碰撞是指两个不同的输入产生相同的哈希输出。对于安全的哈希函数,找到碰撞在计算上应该是不可行的。MD5 产生 128 位(16 字节)的哈希,因此生日攻击的界限是 2^64 次运算——但实际攻击要快得多。
MD5 碰撞如何工作
MD5 使用 Merkle-Damgård 结构:消息被填充到 512 位的倍数,分成块,并通过压缩函数迭代处理。压缩函数对 128 位内部状态(四个 32 位寄存器 A、B、C、D)执行 64 轮非线性运算。
王小云 2004 年的差分攻击利用了压缩函数中的弱点。通过精心选择两个消息块之间的差异,攻击者可以使内部状态差异在 64 轮后抵消,产生相同的最终哈希。这比暴力破解高效得多——攻击复杂度约为 2^39 次 MD5 运算,在现代 PC 上不到一小时即可完成。
现实世界的碰撞工具
像 fastcoll 和 HashClash 这样的工具实现了这些攻击。例如,使用 fastcoll:
fastcoll_v1.0.0.5.exe -p original.pdf -o collision1.pdf collision2.pdf
这会生成两个具有相同 MD5 哈希但内容不同的 PDF 文件。
为什么 MD5 在安全上被破解
MD5 未能满足安全所需的抗碰撞性。后果是严重的:
| 性质 | 用于 | MD5 状态 |
|---|---|---|
| 抗碰撞性 | 数字签名、证书 | ❌ 已破解(2^39 次运算) |
| 抗第二原像 | 文件完整性(恶意) | ❌ 已削弱 |
| 抗原像 | 密码哈希 | ⚠️ 仍然较强(~2^123)但不推荐 |
MD5 消亡的时间线
- 2004:王小云在 Crypto 2004 上展示实用的 MD5 碰撞。
- 2008:Sotirov 等人利用 MD5 碰撞伪造 RapidSSL 中间 CA 证书,展示了现实世界的影响。
- 2012:Flame 恶意软件使用 MD5 选择前缀碰撞伪造微软代码签名证书。
- 2017:SHA-1(比 MD5 更强)也被 SHAttered 攻击破解;行业全面迁移到 SHA-2。
MD5 何时仍可接受(何时不可)
| 使用场景 | 安全? | 理由 |
|---|---|---|
| 数字签名/证书 | ❌ 否 | 碰撞允许伪造 |
| 密码存储 | ❌ 否 | 使用 bcrypt、scrypt、Argon2 |
| 文件完整性(防恶意篡改) | ❌ 否 | 攻击者可替换文件和哈希 |
| 文件完整性(意外损坏) | ⚠️ 弱 | 为面向未来使用 SHA-256 |
| 非安全哈希表键 | ✅ 是 | 碰撞导致性能问题而非安全漏洞 |
常见陷阱
- 认为 MD5 对安全“足够好”。 碰撞攻击是实用的——坚定的攻击者可以伪造签名或证书。
- 使用 MD5 进行密码哈希。 即使没有碰撞,MD5 也很快,容易受到暴力破解;始终使用慢速加盐的哈希如 Argon2。
- 忽略生日界限。 对于 128 位哈希,抗碰撞性只有 64 位——现代硬件可行。
- 认为“我只需要抗原像性。” 许多协议依赖抗碰撞性;如果攻击者可以创建两个具有相同哈希的文档,他们就可以用一个替换另一个。
应该使用什么替代方案
- SHA-256(SHA-2 家族):当前标准,没有已知的实际攻击,256 位输出。用于签名、证书和文件完整性。
- SHA-3:替代设计(Keccak),不易受长度扩展攻击。适合新协议。
- BLAKE3:非常快,安全,支持流式处理。适合哈希大文件。
- HMAC-SHA256:对于消息认证,始终使用 HMAC 而不是裸哈希,以避免长度扩展攻击。
工作示例:从 MD5 迁移到 SHA-256
假设你有一个文件上传系统,使用基于 MD5 的文件名和校验和。以下是迁移方法:
之前(不安全)
import hashlib
def store_file(content):
md5 = hashlib.md5(content).hexdigest()
filename = f"{md5}.bin"
with open(filename, 'wb') as f:
f.write(content)
return md5
def verify_file(filename, expected_md5):
with open(filename, 'rb') as f:
content = f.read()
actual_md5 = hashlib.md5(content).hexdigest()
return actual_md5 == expected_md5
之后(安全)
import hashlib
def store_file(content):
sha256 = hashlib.sha256(content).hexdigest()
filename = f"{sha256}.bin"
with open(filename, 'wb') as f:
f.write(content)
return sha256
def verify_file(filename, expected_sha256):
with open(filename, 'rb') as f:
content = f.read()
actual_sha256 = hashlib.sha256(content).hexdigest()
return actual_sha256 == expected_sha256
对于现有的 MD5 哈希文件,重新计算 SHA-256 哈希并更新引用。你也可以在过渡期间同时存储两个哈希。
在我们的哈希文本工具中尝试比较 MD5 和 SHA-256 对同一输入的输出。
常见问题
MD5 是否完全被破解,适用于所有目的?
不。MD5 仍然提供抗原像性(约 2^123 次运算),对于非安全用途如意外损坏的校验和或作为哈希表键是安全的。然而,SHA-256 同样快且消除了任何疑虑。
如果我加盐,可以使用 MD5 吗?
不能。盐不能防止碰撞攻击——它只影响原像攻击。仍然可以找到加盐 MD5 的碰撞。
今天找到 MD5 碰撞需要多长时间?
在现代消费级 GPU(例如 RTX 4090)上,使用 fastcoll 或 HashClash 等工具可以在不到一分钟内找到单个碰撞。云实例使其更便宜。
SHA-1 呢?也被破解了吗?
是的。SHA-1 碰撞在 2017 年被演示(SHAttered 攻击)。SHA-1 不应再用于任何安全目的。Git 仍然使用 SHA-1 作为对象 ID,但威胁模型不同;新项目应使用 SHA-256。
有没有抗量子计算的哈希函数?
SHA-256 和 SHA-3 被认为对 Grover 算法(将安全级别减半)具有量子抗性。对于 256 位哈希,量子攻击仍需要 2^128 次运算——不可行。BLAKE3 也提供 256 位安全性。
结论
MD5 的抗碰撞性已被破解,将其用于安全目的是不负责任的。始终为新应用选择 SHA-256 或更强的哈希。对于现有系统,尽快迁移——碰撞攻击的成本对于有动机的对手来说足够低。使用我们的哈希文本工具验证 MD5 和 SHA-256 输出之间的差异。