MD5
1. MD5 基础理论与特性
-
基本概念:MD5 是一种单向散列(Hash)函数,它的作用是将明文(plaintext)转换为密文(ciphertext)。具体来说,它将报文(message)转换为报文摘要(digest),这个过程相当于有损压缩。
-
无需密钥:从报文生成摘要的过程中不需要使用密钥。
-
长度特性:MD5 算法的输入 message 长度可以是 \([0, n]\) 字节(其中 \(n\) 可以无限大),但输出的 digest 长度固定为 128 位,即 16 个字节。
-
MD5 碰撞(Collision):假定可以找到两段不同的报文 \(m1\) 及 \(m2\)(\(m1 \neq m2\)),使得 \(md5(m1) == md5(m2)\),则称发生了 MD5 碰撞。
2. MD5 计算过程中的分块与填充 (Padding)
设文件大小为 \(n\) 字节,MD5 在计算时是分块进行的,每块大小为 64 字节。
当最后一块大小刚好为 64 字节时,该块只能算作倒数第 2 块,必须在其后补充一个大小为 0 字节的块作为真正的最后一块。
最后一块的原始大小为 \([0, 63]\) 字节,需要按以下步骤进行填充,使其凑成 64 字节的一个块,或扩充至 128 字节的两个块:
-
数据填充:
-
情况 A (长度 < 56 字节):假定该块大小 \(n < 56\) 字节,则在末尾补上
0x80,其后补0x00,共补 \(56-n\) 个字节。例如 \(n=55\) 时,只需补上0x80一个字节。 -
情况 B (长度在
[56, 63]字节):假定该块大小 \(n \in [56, 63]\),此时空间不足以追加长度信息,必须补至下一个块的末尾,总共需要补上 \(64 - n + 56\) 个字节。例如 \(n=56\) 时,应补上 \(64\) 个字节(即 \(8+56\) 个字节)。
-
-
长度追加:在填充完 0 后,最后补上 8 个字节。这 8 个字节相当于一个 64 位的整数,记录的是 message 原本的总位数(不含填充内容)。
3. MD5 源代码与核心数据结构剖析
MD5 的实现依赖于特定的数据结构和轮函数操作。
3.1 核心数据结构 MD5_CTX
typedef struct _MD5_CTX
{
ulong32 state[4]; /* 128位摘要 (a, b, c, d) */
ulong32 count[2]; /* 已处理的报文的二进制位数,最大值为 2^64-1 */
unsigned char data[64]; /* 64字节的message块缓存 */
} MD5_CTX;
此处 ulong32 被强制定义为 32 位无符号整数。
3.2 算法核心流程
-
初始化 (
Init_MD5):初始化
count为 0。赋予 4 个链接变量(state)初始值(Magic Numbers):
-
state[0] = 0x67452301 -
state[1] = 0xEFCDAB89 -
state[2] = 0x98BADCFE -
state[3] = 0x10325476
-
-
位运算宏定义:算法包含 F, G, H, I 四个基本的按位逻辑函数,以及循环左移宏
ROL(x, n)。利用这些基本函数构成了FF,GG,HH,II四种宏操作。 -
数据更新 (
Update_MD5):-
利用位运算
(count[0] >> 3) & 0x3F计算当前内部缓冲data中剩余未处理的字节数bytes_left。 -
如果输入数据填满了 64 字节的缓存块,就调用
Process_One_Block_MD5对这 64 字节进行压缩计算,并将剩余数据循环存入缓冲。
-
-
单块处理 (
Process_One_Block_MD5):-
取出当前的
state存入局部变量a, b, c, d中。 -
经历 64 步(共 4 轮,每轮 16 步)的复杂变换,分别调用
FF、GG、HH、II宏,并混入从明文块中取出的数据位和特定的常量const_sin。 -
最后将变换后的
a, b, c, d累加回原state[0..3]中。
-
-
收尾填充 (
Final_MD5):-
保存最初始的已处理二进制位数
total_bits。 -
计算出需要填充的长度
pad_len(补全至不足 8 字节满 64 字节的位置),并使用预设好的pad_stuff(即0x80后面跟着一堆0x00)调用Update_MD5进行填充。 -
最后将
total_bits调用Update_MD5补入末尾。
-
3.3 正弦常量 (Sine Constants)
64 步操作中每次加入的常量值并非随机,而是通过正弦函数计算得来。计算公式为:
y = floor(fabs(sin(i)) * pow(2,32)) (其中 i 从 1 到 64,单位是弧度)。
4. MD5 的应用与碰撞攻击利用
-
数字签名结合:MD5 经常与 RSA 等算法结合用于数字签名。
-
发送方 A 计算摘要 \(m = \operatorname{md5}(\text{letter})\),随后用自己的私钥对摘要加密得到签名 \(m' = \operatorname{rsa}(m, \text{A 的私钥})\)。
-
接收方 B 收到后验证:\(\operatorname{rsa}(m', \text{A 的公钥}) = \operatorname{md5}(\text{letter})\)。
-
-
碰撞的实际影响:假定构造出两个会发生碰撞的文件 \(m1\) 和 \(m2\)(即 \(md5(m1) == md5(m2)\)),由于此时计算所用的 4 个 state 种子值完全相等,那么在此基础上追加相同内容,其结果依然相等,即:\(md5(letter + m1) == md5(letter + m2)\)。这意味着 \(letter + m1\) 的签名可以被恶意用于 \(letter + m2\)。
5. MD5 破解方法:彩虹表 Rainbow Table
彩虹表是一种用空间换时间的密码破解技术,适用于逆向查找 MD5 对应的明文。
5.1 彩虹表的构建(以 4 个大写字母为例)
-
产生一个随机数 \(n_0 \in [0, 26^4-1]\)。
-
找出与 \(n_0\) 对应的 4 位字母组合 \(p_0\),并计算其 MD5 值:\(m_0 = md5(p_0)\)。
-
循环运算建立哈希链:执行 100 次循环(\(j=1\) 到 \(100\)):
-
消减函数 Reduction function:计算 \(n_j = m_{j-1} \bmod 26^4\)。(把一个明文的 MD5 计算结果映射到另一个明文)
-
映射出对应的字母组合 \(p_j\)(例如 n=0 时为 "AAAA",n=26 时为 "AABA")。
-
计算新的 MD5:\(m_j = md5(p_j)\)。
-
-
在数据库中仅记录链条的起点 \(n_0\) 和终点 \(m_{100}\)。上述过程完成 SomeBigNumber 次后,得到对应数量的链,每条链隐式包含 101 个 md5 值。
5.2 彩虹表的查询与破解
假设要破解一个已知的 MD5 值 M:
-
首先在数据库的终点(\(m_{100}\))中查找 M。若找到,表示 M 是 \(j=100\) 时算出来的。从数据库取出对应的 \(n_0\),顺着算法重新计算到第 100 步,得到的 \(p_{100}\) 即为所求明文。
-
若数据库中未找到 M,则假设它是链条倒数第二个节点(即设 \(m_{99} = M\)),再次执行消减函数和 MD5 计算得出 \(m_{100}\)(对应推导出的 \(m_{j+1}\)),再到数据库中匹配。
-
若匹配成功,表示找到了该链的起点 \(n_0\)。从起点重新推导到 \(j=99\) 时的字母组合 \(p_{99}\),该组合极有可能满足 \(md5(p_{99}) == M\)。依此类推不断往前倒推验证。
SHA-1
1. 算法核心特征
-
哈希值长度:160位(20字节),由5个32位(ulong)状态值组成。比MD5多32位。
-
数据块大小:分块处理,每块64字节(512位)。
-
填充规则:当最后一块不足64字节时进行填充,机制与MD5相同。数据末尾必须追加8个字节(64位)用于记录原始报文的总位数。
-
字节序要求:SHA-1 算法严格遵循大端序(Big-Endian)。在小端序(如 Intel CPU)架构下,计算过程中需要对数据块、长度值以及最终的哈希输出进行字节序转换。
2. 核心数据结构 (SHA1_CTX)
上下文结构体用于保存哈希计算的中间状态:
-
state[5]:长度为5的unsigned long数组,总计20字节,用于存储160位的中间/最终哈希值。 -
length:包含hi和lo两个32位整数,组合成一个64位整数,用于累加记录已处理报文的二进制位数。 -
data[64]:内部缓冲区,缓存尚未达到64字节的数据块。
3. 算法执行流程
3.1 初始化 (SHA1_Init)
为5个状态变量赋初始魔数,并将报文长度清零:
-
state[0] = 0x67452301UL -
state[1] = 0xEFCDAB89UL -
state[2] = 0x98BADCFEUL -
state[3] = 0x10325476UL -
state[4] = 0xC3D2E1F0UL
3.2 数据处理 (SHA1_Update)
将输入报文分批送入内部缓冲区,满64字节即触发一次块处理:
-
计算当前缓冲区内已有的剩余字节数。
-
更新64位报文长度计数器(处理低32位进位到高32位的逻辑)。
-
如果新输入的数据能够填满64字节缓冲区,则调用核心处理函数
Process_One_Block_SHA1。 -
若输入数据极长,则通过循环直接以64字节为步长处理数据,省去拷贝到缓冲区的开销。
-
将末尾不足64字节的零头存入内部缓冲区等待下次处理。
3.3 填充与结束 (SHA1_Final)
-
记录原始长度:提取当前
length值(64位总位数),后续追加使用。 -
填充数据:
-
首先追加一个
0x80字节。 -
随后补
0x00。 -
长度判定:若缓冲区剩余空间不足以放下8字节的长度信息(即已占用 \(\ge 56\) 字节),则当前块填满
0x00后先处理一次,然后再开启一个新块填充0x00至56字节。
-
-
追加总位数:将步骤1提取的64位总位数转为大端序,填入最后8个字节,触发最后一次块计算。
-
输出摘要:提取
state数组的20字节。在小端序机器上输出时,必须先对每个ulong内部的4个字节进行大端序翻转,以确保按字节读取时顺序正确。
4. 单块核心计算 (Process_One_Block_SHA1)
对64字节数据块进行数学运算,更新内部状态:
-
数据转换:将输入的64字节(16个
ulong)转为大端序,存入数组X[0...15]。 -
数据扩充:将16个
ulong扩展为80个ulong。- 扩展公式:
X[i] = ROL(X[i-3] ^ X[i-8] ^ X[i-14] ^ X[i-16], 1)(其中ROL为循环左移)。
- 扩展公式:
-
四轮混合运算(Round):
每轮包含20步操作(共80步),分别使用不同的非线性逻辑函数和常数:
-
第 00-19 步:使用常数
y1 = 0x5A827999,函数f(u,v,w) = (u & v) | (~u & w) -
第 20-39 步:使用常数
y2 = 0x6ED9EBA1,函数h(u,v,w) = u ^ v ^ w -
第 40-59 步:使用常数
y3 = 0x8F1BBCDC,函数g(u,v,w) = (u & v) | (u & w) | (v & w) -
第 60-79 步:使用常数
y4 = 0xCA62C1D6,函数h(u,v,w) = u ^ v ^ w
-
-
状态叠加:一轮运算结束后,将运算得到的临时状态累加回主状态
state[0...4]中。