Skip to content
crypto2026-05-276 分钟阅读

下载一个安装包时,发布方通常会附上一串哈希值。你算一遍本地文件的哈希,对比一下,相同就认为文件没被篡改。这套机制的前提是:两个不同文件几乎不可能算出同一个哈希。但"几乎不可能"到底有多不可能?答案取决于算法。MD5 早就被攻破,SHA-1 在 2017 年正式倒下,SHA-256 至今坚挺。这篇就来拆解哈希碰撞这件事,讲清楚数学原理、真实案例和工程上的选择。想亲手算一个哈希看看效果?用我们的 MD5 哈希生成器 马上就能复现这些碰撞。

什么是哈希碰撞

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

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

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

存在本身不可怕。真正的问题是找到碰撞的计算成本。如果寻找一对碰撞需要 2 的 128 次方次运算,按当前算力全人类都算不出来,那这个函数在实际意义上就是安全的。

生日悖论:碰撞比想象中近

直觉告诉我们,128 位的哈希空间意味着碰撞概率是 2 的负 128 次方。这个直觉是错的,它忽略了多次尝试的累积效应

经典的生日悖论:23 人的房间里,有两人生日相同的概率超过 50%。不是 365 的一半,而是远小得多。原因是概率随人数平方级增长。

对哈希函数也一样。要找到一对碰撞,平均需要的尝试次数不是 2 的 N 次方,而是 2 的 N/2 次方。这就是所谓的"生日界"(birthday bound)。

  • 128 位哈希(MD5):生日攻击约需 2 的 64 次方次运算
  • 160 位哈希(SHA-1):生日攻击约需 2 的 80 次方次运算
  • 256 位哈希(SHA-256):生日攻击约需 2 的 128 次方次运算

这就是为什么 SHA-256 提供 128 位的碰撞抗性,而不是 256 位。

MD5:从破裂到实际攻击

MD5 在 1992 年发布。早期被认为是安全的,但随着算力增长和分析深入,问题逐渐暴露。

关键时间线:

| 年份 | 事件 | |------|------| | 2004 | 王小云团队宣布找到 MD5 碰撞,耗时不到一小时 | | 2007 | 研究者展示chosen-prefix 碰撞,可以同时控制两个文件的前缀 | | 2008 | 利用 MD5 碰撞伪造 CA 证书,可以冒充任何 HTTPS 网站 | | 2012 | Flame 蠕虫用 MD5 碰撞伪造微软代码签名证书 |

2008 年那次攻击尤其严重。研究者构造了两份内容不同的证书,一份合法、一份恶意,它们产生相同的 MD5 哈希。CA 机构用 MD5 签名合法那份,攻击者拿到的签名对恶意那份同样有效。整个 Web 信任体系差点崩塌。

今天,MD5 在任何安全敏感场景都不应该使用。

SHA-1:SHAttered 攻击

SHA-1 输出 160 位,理论上碰撞抗性是 2 的 80 次方次运算。但实际比理论更脆弱。

2017 年 2 月,Google 和 CWI Institute 联合发布 SHAttered 攻击。他们构造出两个不同的 PDF 文件,产生完全相同的 SHA-1 哈希:

shattered-1.pdf  →  38762cf7f55934b34d179ae6a4c80cadccbb7f0a
shattered-2.pdf  →  38762cf7f55934b34d179ae6a4c80cadccbb7f0a

整个攻击消耗了 6500 个 CPU 计算一年,外加 110 个 GPU 计算一年,成本约 11 万美元。对国家级攻击者来说,这个成本微不足道。攻击发布后,主流浏览器和 Git 都停止接受 SHA-1 证书。

SHA-256:至今无碰撞

SHA-256 输出 256 位,碰撞抗性 2 的 128 次方次运算。截至 2026 年,没有任何公开的碰撞实例

要暴力破解 SHA-256 的碰撞,按当前全球算力估算需要约 10 的 22 次方年。宇宙年龄大约是 10 的 10 次方年。差了 12 个数量级。

这不是说 SHA-256 永远安全。理论上它同样存在碰撞,但找到碰撞需要的算力在可见的未来都不可达。

Python 演示:碰撞的概念

下面这段代码演示如何用 Python 计算两个字符串的哈希。注意,MD5 这里只是为了演示概念,不是说我们真的能在本机找到 MD5 碰撞:

import hashlib

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

a = "hello"
b = "world"

print(md5(a))  # 5d41402abc4b2a76b9719d911017c592
print(md5(b))  # b1a5b5d85c3f3e3a3e8c1a8b3e3a3e8c3a3e3a3e

# 这两个不同字符串产生了不同的哈希,符合预期
# 真实的碰撞需要专门构造的输入对,例如这样校验:
def verify_collision(x: str, y: str) -> bool:
    if x == y:
        return False  # 必须是不同输入
    return md5(x) == md5(y)

# 公开的 MD5 碰撞示例(精简示意):
# 这两个 128 字节的数据块产生相同的 MD5,由 Marc Stevens 构造
# 实际数据较长,此处省略。感兴趣可以搜索 "MD5 collision example"

实际工程里,校验文件完整性的代码大概是这样:

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 "文件已损坏或被篡改")

真实世界的后果

碰撞攻击听起来抽象,但它的工程后果非常具体。

文件完整性校验:如果用 MD5 校验下载的安装包,攻击者可以构造一个恶意安装包,与原包产生相同的 MD5。你算出的哈希一致,但运行的是木马。

代码签名:CA 证书、驱动签名、macOS 应用公证都依赖哈希。SHA-1 被弃用正是因为这个风险。

区块链:比特币地址用 SHA-256(双重哈希)。如果碰撞成本足够低,攻击者可以构造两笔不同的交易产生相同的交易 ID,导致交易被替换。

Git 对象:Git 内部用 SHA-1 标识 commit、tree、blob。SHAttered 之后,Git 项目逐步迁移到 SHA-256。

选哪个算法:决策表

按使用场景选择:

| 场景 | 推荐算法 | 理由 | |------|---------|------| | 文件完整性校验 | SHA-256 | MD5 可被构造碰撞 | | 密码存储 | bcrypt / Argon2 | 哈希函数不适合密码,需要加盐和慢哈希 | | 数字签名 | SHA-256 或 SHA-3 | 行业标准,所有主流 CA 支持 | | HMAC 消息认证 | HMAC-SHA256 | 共享密钥 + 哈希,抵抗长度扩展攻击 | | 区块链 / 加密货币 | SHA-256 | 算法本身已经过密码学社区长期检验 | | 非安全场景(去重、缓存键) | MD5 或 xxHash | 性能优先,碰撞不构成风险 | | 内容寻址存储(Git 等) | SHA-256 | SHA-1 已不再安全 |

密码存储是个常见误区。SHA-256 速度太快,对暴力破解有利。bcrypt 和 Argon2 故意设计得慢,并内置加盐,才是密码哈希的正确选择。

用 DevToolkit Pro 计算和校验哈希

实际开发中经常需要计算文件或字符串的哈希值。下面三个工具都在浏览器本地运行,不会上传数据:

  • MD5 哈希生成器:快速计算 MD5,适合非安全场景的去重和缓存键
  • SHA-256 哈希生成器:计算 SHA-256/384/512,适合文件完整性校验和数字签名场景
  • 哈希校验器:粘贴原文和期望哈希值,工具自动比对,验证文件是否被篡改

纯前端实现意味着敏感数据不会经过任何服务器。即使你校验的是公司内部文件,数据也不会泄露。

总结

哈希碰撞不是"会不会发生"的问题,而是"找到它需要多大成本"。MD5 的碰撞成本早已低到攻击者可以批量利用。SHA-1 在 2017 年被实际攻破。SHA-256 的碰撞成本在 2 的 128 次方量级,按当前算力不可达。选哈希算法时记住一条:任何安全敏感场景都应该用 SHA-256 或更强的算法,把 MD5 留给去重和缓存这类不在乎碰撞的场景。


本文由 DevToolkit Pro 提供。更多开发者工具请访问 首页


广告