2、MD5 算法原理:数据填充、初始化 MD 缓冲区、处理消息块

讲 MD5 算法,很多人一开始就被那些移位、模加弄得头大。我刚开始接触的时候也一样,觉得这玩意儿就是个黑盒子。

其实 MD5 的原理并不复杂,说白了就三个阶段:

  • 数据填充 – 把消息补成 512 位的整数倍
  • 初始化 MD 缓冲区 – 准备好四个 32 位寄存器
  • 处理消息块 – 用四个轮函数来回折腾

下面我一个一个说透。

2.1 数据填充

MD5 以 512 位(64 字节)为一个消息块。如果原始消息长度不是 512 的倍数,就必须填充。填充规则很死板:

  • 先补一个 ‘1’ 位(二进制),后面补 ‘0’。
  • 填充后的总长度(单位:位)必须满足 长度 ≡ 448 mod 512。也就是说,最后留出 64 位放原始消息长度。
  • 最后附加原始消息的长度(以 bit 为单位,用小端序存放),长度用 64 位无符号整数表示。
我踩过的坑: 曾经在处理大文件时,直接用了 32 位长度字段,结果文件超过 512MB 就出错。MD5 规范里长度是 64 位的,必须用 64 位整数。别省这个。

举个例子,原始消息是 “abc”(3 字节 = 24 位)。填充后变成:

原始二进制:           01100001 01100010 01100011
补 '1':               … 01100011 1
补 '0' 到 448 位:     … 后面补 423 个 0
附加原始长度 24 位:  最后 64 位为 0x00000000 00000018 (小端)
最终消息块 512 位:    [填充后的 448 位] + [64 位长度]
小技巧: 填充时可以先算好需要补多少个 0。公式:padding_len = (448 - (len * 8) % 512) % 512。注意取模要小心负数情况,我建议用 (448 - (msg_bits & 511)) & 511 来算。

2.2 初始化 MD 缓冲区

MD5 需要四个 32 位寄存器,记为 A、B、C、D。初始值是固定的:

寄存器 初始值(十六进制) 说明
A 0x67452301 小端序读出来就是 01 23 45 67
B 0xEFCDAB89 小端序 89 AB CD EF
C 0x98BADCFE 小端序 FE DC BA 98
D 0x10325476 小端序 76 54 32 10

这几个值看起来随机,其实是 MD5 作者 Ronald Rivest 特意选的,说白了就是为了让初始状态均匀分布。

我在写自己的 MD5 实现时,曾犯过一个低级的错误:把大小端顺序搞反了。如果你直接拿网络上的十六进制串赋值,而你的平台是大端,一定要做字节序转换。大部分 x86 机器是小端,但 ARM 可以配置,**检查一下你的平台**。

2.3 处理消息块

这是 MD5 的核心,也是最容易晕的地方。我尽量用直白的语言解释。

每个 512 位的消息块会被细分成 16 个 32 位的字(记为 M[0]...M[15])。然后进行四轮运算,每轮 16 步,一共 64 步。

每一步都会更新 A、B、C、D:

// 伪代码,一步操作
FF(a, b, c, d, M[k], s, t)  // 第1轮
GG(a, b, c, d, M[k], s, t)  // 第2轮
HH(a, b, c, d, M[k], s, t)  // 第3轮
II(a, b, c, d, M[k], s, t)  // 第4轮

具体每个轮的逻辑函数:

轮数 函数 逻辑表达式 非线性特点
1 F (b & c) | (~b & d) 位选择,类似IF-THEN-ELSE
2 G (b & d) | (c & ~d) 与 F 对称,但条件位不同
3 H b ^ c ^ d 异或,强雪崩效应
4 I c ^ (b | ~d) 与 H 类似但更复杂

每步还有一个固定的移位量 s 和一个正弦常数 t。常数来自 floor(2^32 * |sin(i)|),i 从 1 到 64。这些常数事先算好,直接查表。

完整的一步操作(以第1轮为例):
// 当前状态:a, b, c, d
// M[k] 是第 k 个消息字(0-15),s 是移位位数,t 是常数
temp = (a + F(b, c, d) + M[k] + t) & 0xFFFFFFFF
a = d
d = c
c = b
b = (b + (temp << s | temp >> (32 - s))) & 0xFFFFFFFF

每 16 步之后,A、B、C、D 会累加到上一轮的输出上。最终,整个消息块处理完后得到的 A || B || C || D 就是 128 位的 MD5 摘要。

你问为什么需要这么多轮?其实就是为了让输入的任何微小变化都能扩散到整个输出。我调试过 MD5 的 collisions,发现两对不同的消息能在某一步产生相同的中间状态,但经过 64 步之后雪崩效应会强行把差异放大。不过,嗯,后来王小云教授找到的碰撞攻击证明这个扩散还不够强……那是后话了。

2.4 整体流程一览

用一张图总结 MD5 的完整处理过程:

MD5 算法核心流程 MD5 算法核心流程 原始消息(可变长度) 数据填充 补1 + 补0 + 64位长度 初始化A/B/C/D 0x67452301… 处理每个512位消息块 四轮×16步 = 64步 输出128位摘要 A || B || C || D 注意 每个块处理完后,A/B/C/D 会累加到前一块的结果上。 如果有多个512位块,循环 直到所有块处理完毕。
给想手写实现的朋友: MD5 的移位量和常数表网上到处都有,但我建议直接复用 RFC 1321 附录里的参考实现,别自己算 sin,可能会因为浮点精度出偏差。另外,每一步的地址索引(k 的顺序)在四轮中是不同的,我当年就是抄错了 GG 轮的 k 顺序,导致摘要全错,检查了一整天。

好,MD5 的基本原理就是这样:填充、初始化、处理块。虽然现在 MD5 已经被证明不安全,但理解它的结构对学习其他哈希算法(比如 SHA-2 / SHA-3)非常有帮助。至少你在看密码学论文时,不会对“压缩函数”和“轮迭代”感到陌生。