白盒.org 把每个原理拆到不能再拆
首页计算 › 信息与编码

哈希:为什么改一个字母,会得到完全不同的乱码

L2 · 标准版 信息与编码 2026年9月18日

哈希函数是一台把任意输入搅成固定长度指纹的确定性搅拌机。它刻意让输入的每一比特都影响输出的每一比特——所以改一个字母,不是"变一点点",而是整体重排。

一句话结论

哈希函数是一台确定性的、定长的、不可逆的搅拌机

  • 同样的输入,永远得到同样的输出(确定性);
  • 不管喂进去一个字还是一部电影,都只吐出 256 个 0 和 1(定长);
  • 而且它被刻意设计成:输入改动一位,输出改动一半(雪崩效应)。

第三条不是副作用,它是设计目标。“改一个字母得到完全不同的乱码”,原因全在这里。

直觉铺垫:一台搅拌机

想象一台果汁机:

  • 确定性:同样的苹果、同样的转速,打出来永远是同一个味道。“baihe.org” 这个字符串,在世界上任何一台电脑上算 SHA-256,结果都一样。
  • 定长输出:不管倒进去一颗樱桃还是一整筐,机器只倒出一小杯 —— 256 个比特。这意味着信息必然丢失:输入有无穷多种,输出只有 22562^{256} 种,撞车(碰撞)一定存在。这是鸽巢原理,不是设计缺陷。
  • 雪崩:往果汁里多放一粒盐,整杯味道都不一样。而且不是”多了一点咸味”,是你没法从味道上判断”是不是只放了一粒盐”。
  • 不可逆:给你一杯打好的果汁,你无法倒推出原来放了几个苹果、有没有放盐。

现在问关键的一句:为什么不做得温和一点?值得这么极端吗?

值得,而且必须。

假设哈希是”温和”的:改一个字母,输出只变一两个比特。那我就能这样破译:

  1. 猜一个输入,算出哈希,看它和目标哈希差几个比特;
  2. 改掉一个字母,再看差几个比特;
  3. 哪个改动让差距变小,就往哪个方向继续改。

这是一架可以一步步爬上去的梯子,就像” hotter / colder “那个游戏。只要输出肯告诉攻击者”你更接近了”,密码就会被爬出来。

所以哈希函数必须把梯子抽掉:无论你怎么微调输入,输出都像重新掷了一次硬币。你得不到任何”更接近了”的信号,只能从头瞎猜。

这就是雪崩效应的真实身份 —— 它不是哈希的一个特性,它是哈希之所以能当密码用的前提。

推导过程

形式化地写一句话

H:{0,1}{0,1}256H: \{0,1\}^* \to \{0,1\}^{256}

左边的 * 表示”任意长度”,右边是固定的 256 位。所以 SHA-256 的输出永远是 32 字节,写成十六进制是 64 个字符,无论你哈希的是 "a" 还是整部《红楼梦》。

SHA-256 到底在干什么

  1. 切块与填充:把输入切成 512 比特一块;最后一块不够就补齐,并在末尾附上原始长度。
  2. 初始常量:8 个 32 位工作变量,取自前 8 个质数(2、3、5、7、11、13、17、19)平方根的小数部分前 32 位(0x6a09e667…)。为什么用这种看似随手抓来的数?为了证明没有后门:这些数不是我挑的,没人能事先在里面动手脚。密码学里叫它 nothing-up-my-sleeve number。
  3. 64 轮搅拌:每一轮做三件事 ——
    • 非线性混合Ch(e,f,g)=(ef)(¬eg)\mathrm{Ch}(e,f,g) = (e \land f) \oplus (\neg e \land g)Maj(a,b,c)=(ab)(ac)(bc)\mathrm{Maj}(a,b,c) = (a \land b) \oplus (a \land c) \oplus (b \land c)
    • 循环移位Σ0(a)=(a2)(a13)(a22)\Sigma_0(a) = (a \ggg 2) \oplus (a \ggg 13) \oplus (a \ggg 22)Σ1(e)=(e6)(e11)(e25)\Sigma_1(e) = (e \ggg 6) \oplus (e \ggg 11) \oplus (e \ggg 25)
    • 2322^{32} 加法:进位会自然地把低位的影响推到高位;每轮还要加一个固定常量(取自前 64 个质数立方根的小数部分)。
  4. 反馈:这一轮算出的 8 个变量,直接成为下一轮的输入。所以任何一点差异,会被一轮一轮反复放大。

香农在 1949 年就给这两样武器起了名字:混淆(confusion) 让输入与输出的关系复杂到看不懂,扩散(diffusion) 让一个比特的影响铺满整个输出。SHA-256 的 64 轮,就是把这两件事重复 64 次。

为什么非要非线性不可

假设哈希是线性的:每个输出位都是若干输入位的异或组合。那么 256 个输出位就是 256 个线性方程 —— 你只要收集 256 组输入/输出,用高斯消元就能把整个函数解出来,然后随意构造碰撞。

所以 SHA-256 里必须塞进 Ch\mathrm{Ch}Maj\mathrm{Maj} 和模加法的进位:它们不是线性的。顺便一提,异或 \oplus 本身是线性运算,光靠它一个是不够的 —— 这也是为什么”把输入全部异或一遍”不能当哈希用。

单向性:请注意,它没有被证明过

必须诚实地说一句:至今没有任何哈希函数被真正”证明”不可逆。 我们对 SHA-256 的信心,来自”全世界的聪明人找了几十年,还没找到比暴力搜索更快的逆运算”。

这是经验,不是定理。所以密码学才会定期让哈希函数退役(MD5 → SHA-1 → SHA-2 → SHA-3),不是因为旧函数被证明有洞,而是因为攻击方法一直在进步。

交互演示

先看真实的 SHA-256。改动下面任意一个字符,数一数有多少个比特跟着翻了:

演示 1 · 雪崩效应(真实 SHA-256)
原文
改一处
输出翻转了 123 / 256 位(48.0% — 理想值是 50%
原文 c6ef89d017945fe39345cc0ff73334e4f8b9b3bd841bdaab03053d068aebd7dd
改后 6c23bc90a113be68ec410f5661a8c28390739f79e1462ba381befa8bb89bcbc8
红格 = 因你改动一个字符而翻转的比特位。无论改的是第一个字母还是最后一个, 翻转数都稳定落在 128 附近——这就是「雪崩」。

无论你改的是第一个字母还是最后一个,翻转数都稳定在 128 附近(256 的一半)。这不是巧合,这是设计指标:每一个输出比特,被翻转的概率都应该是 50%,不多不少。

那么这个”一半”是怎么长出来的?下面用一个教学用的、简化到只剩骨架的混合函数来演示。真实 SHA-256 的每一轮更强、共 64 轮,但道理完全一样:

演示 2 · 扩散是怎么长出来的
点下面任意一位,把它翻转(只翻这一位),然后看差异如何随轮数蔓延:
混合轮数10
32 个输出位中,受影响的位合计
1
4/32
2
4/32
3
4/32
4
4/32
5
16/32
6
4/32
7
8/32
8
4/32
9
14/32
10
13/32
第 1 轮时只有孤零零一个点。跑到第 10 轮,已经有 13 / 32 个输出位被这一个输入位改写。当所有位都被改写、且改写方向接近随机时, 你就再也看不出「原文改了哪一位」——这就是哈希的单向性来源。

第 1 轮时只有孤零零一个点被改写。随着轮数增加,差异向两侧蔓延,很快就铺满整行。当所有输出位都被改写、而且改写方向看起来像随机时,你就再也看不出”原文到底改了哪一位” —— 单向性就是这么来的。

常见误解

  1. “哈希就是加密” —— 不是。加密要能解开(有密钥),哈希故意让你解不开。所以网站应该哈希存储你的密码,而不是加密存储:它根本不需要知道你的明文,只需要在你登录时比对哈希值。遇到”客服能告诉你密码是什么”的网站,它一定是存错了。
  2. “哈希不会撞车” —— 会。输入无限、输出有限,鸽巢原理保证必然撞车,只是 22562^{256} 大到撞不上而已。哈希的安全性不是”绝对不撞”,是”撞不起”。
  3. “SHA-256 提供 256 位安全” —— 抗碰撞只有 128 位。因为生日攻击:找碰撞不需要遍历 NN 次,只需要约 N\sqrt{N} 次。这就是生日悖论的力量:2323 个人里有两人同生日的概率超过一半,靠的完全是同一件事。
  4. “MD5 只是老了一点,还能凑合用” —— 不能。MD5 的碰撞在 2004 年就被实际构造出来,SHA-1 在 2017 年被真实碰撞攻破(SHAttered)。它们现在只能用于检测传输中的意外损坏,不能用于任何安全场景。
  5. “加盐就是给密码加个固定字符串” —— 盐必须每个用户随机生成、各不相同。加盐防的不是”某一个人被破解”,而是”一张预计算的彩虹表打穿整个数据库”。
  6. “哈希值一样就说明文件没被篡改” —— 只能防意外损坏,防不住攻击者:他改完文件重新算一遍哈希贴上去就行了。要真正防篡改,需要带密钥的 HMAC,或者非对称的数字签名

延伸引用

  • 横切原理 · 熵与信息 —— 哈希是一次有损压缩,它到底丢掉了什么?
  • 计算 · 从哈希到数字签名 —— 哈希 + 私钥 = 不可抵赖
  • 数学 · 生日悖论:为什么 23 个人就有一半概率撞生日 —— 以及为什么答案是 N\sqrt{N} 而不是 NN
  • 外部:FIPS PUB 180-4,SHA-2 标准原文

本文是「白盒」的第一篇样板文。发现错误、或者觉得哪一段没讲清楚,欢迎在 GitHub 上提 PR。