共计 5963 个字符,预计需要花费 15 分钟才能阅读完成。
MD5 本质上是 单向哈希(不可逆),所以严格来说无法“解密”原文;具体就两件事:
- 生成(“加密”):把任意文本变成 MD5 摘要(hex)。
- 尝试还原(字典 / 暴力破解):通过字典匹配或穷举尝试找到能产生该 MD5 的原文(对短 / 简单密码有效,对强密码通常不可行)。
原理
概览 — MD5 在做什么
MD5(Message-Digest Algorithm 5)把任意长度的消息映射到固定的 128-bit(16 字节)哈希值。核心思想是:对分块消息做多轮的简单算术与位运算(加法、与 / 或 / 非、异或、循环左移),并把每块的结果累加到内部状态。设计目标是快速且高度混淆(混合输入位使输出看似随机)。
处理流程(逐步)
- 预处理(Padding & Length)
- 把消息按字节看成比特流,先加一个
1
位(即 0x80),再补若干0
位,直到消息长度(比特)≡ 448 (mod 512)。 - 最后附上原始消息长度(以 64 位小端整数表示)。
- 结果长度是 512 的倍数(即若干个 512-bit 块)。
- 把消息按字节看成比特流,先加一个
- 初始化 128-bit 状态(四个 32-bit 寄存器)
A = 0x67452301 B = 0xEFCDAB89 C = 0x98BADCFE D = 0x10325476
这些是固定的初始常数(小端语义)。 - 分块处理(每个 512-bit 块)
- 把块分成 16 个 32-bit 小端字 M[0..15]。
- 设 a=A, b=B, c=C, d=D。对这 16 个字进行 64 次操作(4 轮,每轮 16 步)。每步的通用形式:
a = b + leftrotate(a + f(b,c,d) + M[k] + T[i], s )
其中:f
是本轮使用的布尔函数(下文列出)。M[k]
是当前步选择的消息字(k 随步数变化)。T[i]
是第 i 个常数(下面说明)。s
是该步的循环左移位数(固定表)。- 运算均在 32 位无符号整数模 2^32 下进行(加法溢出按模处理)。
- 每轮的
f
分别为:F(X,Y,Z) = (X & Y) | (~X & Z) G(X,Y,Z) = (X & Z) | (Y & ~Z) H(X,Y,Z) = X ^ Y ^ Z I(X,Y,Z) = Y ^ (X | ~Z)
T[i]
常数定义为:T[i] = floor(2^32 * abs(sin(i+1)))
(i 从 0 到 63,使用弧度的正弦值绝对值)。- 每轮有固定的
s
(左移量)序列:- Round 1: 循环 [7,12,17,22]
- Round 2: 循环 [5,9,14,20]
- Round 3: 循环 [4,11,16,23]
- Round 4: 循环 [6,10,15,21]
- 64 步结束后,把 a,b,c,d 分别加回 A,B,C,D(模 2^32)。
- 输出
处理完所有块后,输出 A || B || C || D(按小端字节序拼接),得到 128-bit 摘要,通常显示为 32 个十六进制字符。
小端与位运算注意点
- MD5 按小端(little-endian)方式把字节组合成 32-bit 字;这影响实现和测试向量。
- 所有加法按无符号 32 位模 (2^{32}) 进行;循环左移(rotate left)是关键的扩散手段(把低位信息带到高位,并保留位数)。
复杂度与性质
- 时间复杂度:处理消息长度为 n 时为 O(n)(线性),因为每个 512-bit 块做固定次数的操作。
- 摘要长度固定 128 bit。对理想哈希函数,碰撞复杂度 ~ (2^{64}),预映像复杂度 ~ (2^{128})。
- 但 MD5 并非理想:碰撞抗性已被破解(见下文)。
安全性
- 自 2004 年起,研究者(如 Wang 等)提出了 MD5 碰撞攻击方法,实践上能在远低于 (2^{64}) 的成本下构造碰撞(即找出两个不同消息有相同 MD5)。随后实务上出现了实际滥用(伪造证书、签名等)。
- 结论:不再适合任何安全敏感用途(数字签名、证书、代码签名等禁用)。
- 对于存密码,应使用带盐和成本因子的算法(例如 bcrypt、scrypt、argon2)。对于通用哈希完整性检查,使用 SHA-256 或更强(SHA-3、BLAKE2 等)。
一个简化的伪代码
MD5(message):
msg = preprocess(message) # padding, append length (64-bit little-endian)
A,B,C,D = init_constants()
for each 512-bit chunk in msg:
M[0..15] = split_chunk_little_endian(chunk)
a,b,c,d = A,B,C,D
for i in 0..63:
if 0 <= i <= 15:
f = F(b,c,d); k = i; s = s_table_round1[i mod 4]
elif 16 <= i <= 31:
f = G(b,c,d); k = (5*i + 1) mod 16; s = s_table_round2[i mod 4]
elif 32 <= i <= 47:
f = H(b,c,d); k = (3*i + 5) mod 16; s = s_table_round3[i mod 4]
else:
f = I(b,c,d); k = (7*i) mod 16; s = s_table_round4[i mod 4]
temp = (a + f + M[k] + T[i]) mod 2^32
a = b + leftrotate(temp, s) mod 2^32
# rotate a,b,c,d for next step
a,b,c,d = d,a,b,c
A = (A + a) mod 2^32 # 累加回全局状态
B = (B + b) mod 2^32
C = (C + c) mod 2^32
D = (D + d) mod 2^32
return little_endian_bytes(A) || little_endian_bytes(B) || ...
直观理解
想像把消息切成等长片,送入一个四室搅拌机。每片消息在 64 个小步骤里通过位运算、加法和循环移位被“搅拌”进四个寄存器。每处理一片,搅拌机的当前状态又与整体状态相加,进入下一片。若输入有微小差异,经过多轮旋转与异或,输出应产生大幅变化(理想情况下)。
案例:计算 MD5,或者用字典或可配置的暴力穷举来“尝试破解”目标 MD5。
以下是脚本文件:
#!/usr/bin/env python3
"""
md5_tool.py
功能:- 计算字符串的 MD5
- 使用字典文件尝试破解 MD5
- 使用简单暴力穷举(指定字符集与最大长度)尝试破解 MD5
注意:MD5 是单向哈希。真正强密码通常无法被破解。此脚本仅用于学习 / 合法测试。python
import hashlib
import argparse
import itertools
import sys
import time
def md5_hash(s: str) -> str:
return hashlib.md5(s.encode('utf-8')).hexdigest()
def check_md5(candidate: str, target_hash: str) -> bool:
return md5_hash(candidate) == target_hash.lower()
def dict_attack(target_hash: str, dict_path: str, encoding='utf-8'):
"""逐行读取字典文件,寻找匹配的原文"""
target_hash = target_hash.lower()
with open(dict_path, 'r', encoding=encoding, errors='ignore') as f:
for lineno, line in enumerate(f, 1):
word = line.rstrip('\n\r')
if not word:
continue
if md5_hash(word) == target_hash:
return word, lineno
return None, None
def brute_force_attack(target_hash: str, charset: str, max_len: int, show_progress=True):
"""用给定字符集做逐长度穷举(注意:复杂度随 max_len 指数增长)"""
target_hash = target_hash.lower()
start = time.time()
tried = 0
for length in range(1, max_len + 1):
if show_progress:
print(f"尝试长度 {length} ...", flush=True)
# itertools.product 生成笛卡尔积(等于所有可能的长度为 length 的字符串)for tup in itertools.product(charset, repeat=length):
tried += 1
cand = ''.join(tup)
if md5_hash(cand) == target_hash:
elapsed = time.time() - start
return cand, length, tried, elapsed
# 少量输出避免太慢:每 200000 次打印一次
if show_progress and (tried % 200000 == 0):
elapsed = time.time() - start
print(f"已尝试 {tried} 个候选,耗时 {elapsed:.1f}s", flush=True)
return None, None, tried, time.time() - start
def main():
parser = argparse.ArgumentParser(description="MD5 生成与(字典 / 暴力)尝试还原工具")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--hash', '-H', help="计算给定文本的 MD5(搭配 --text)")
group.add_argument('--crack', '-c', help="要破解 / 匹配的目标 MD5(hex)")
parser.add_argument('--text', '-t', help="与 --hash 一起使用:要计算 MD5 的文本")
parser.add_argument('--dict', '-d', help="字典文件路径:逐行尝试匹配 MD5")
parser.add_argument('--bruteforce', '-b', action='store_true', help="启用暴力穷举(需指定 --charset 和 --maxlen)")
parser.add_argument('--charset', default='abcdefghijklmnopqrstuvwxyz0123456789', help="暴力穷举字符集(默认:小写字母 + 数字)")
parser.add_argument('--maxlen', type=int, default=4, help="暴力穷举最大长度(默认 4)")
parser.add_argument('--no-progress', action='store_true', help="禁止打印长进度信息(暴力模式)")
args = parser.parse_args()
if args.hash:
if args.text is None:
print("使用 --hash 时请同时提供 --text 要计算的文本。", file=sys.stderr)
sys.exit(2)
print(f"文本: {args.text}")
print(f"MD5 : {md5_hash(args.text)}")
return
# 下面处理破解流程
target = args.crack.lower()
if len(target) != 32:
print("目标 MD5 长度应为 32 个 hex 字符。", file=sys.stderr)
sys.exit(2)
# 优先字典攻击(如果提供)if args.dict:
print(f"开始字典攻击,字典文件:{args.dict}")
start = time.time()
found, lineno = dict_attack(target, args.dict)
elapsed = time.time() - start
if found:
print(f"匹配成功!原文 ='{found}'(字典第 {lineno} 行),耗时 {elapsed:.2f}s")
return
else:
print(f"字典未命中(耗时 {elapsed:.2f}s)。")
# 如果指定暴力
if args.bruteforce:
print(f"开始暴力穷举(charset 长度 ={len(args.charset)}, maxlen={args.maxlen})")
cand, length, tried, elapsed = brute_force_attack(target, args.charset, args.maxlen, show_progress=not args.no_progress)
if cand:
print(f"匹配成功!原文 ='{cand}'(长度 {length}),尝试次数 {tried},耗时 {elapsed:.2f}s")
return
else:
print(f"暴力穷举未命中(尝试 {tried} 个候选,耗时 {elapsed:.2f}s)。请增大 charset 或 maxlen 或使用字典。")
return
print("未指定破解方法:既未提供字典也未启用 --bruteforce。请使用 --dict 或 --bruteforce。", file=sys.stderr)
sys.exit(2)
if __name__ == '__main__':
main()
使用示例
- 计算 MD5:
python md5_tool.py --hash --text "password123"
# 输出示例:# 文本: password123
# MD5 : 482c811da5d5b4bc6d497ffa98491e38
- 字典攻击:
python md5_tool.py --crack 482c811da5d5b4bc6d497ffa98491e38 --dict ./common_passwords.txt
- 暴力穷举(小写字母 + 数字,最大长度 5):
python md5_tool.py --crack < 目标 md5> --bruteforce --charset abcdefghijklmnopqrstuvwxyz0123456789 --maxlen 5
总结:
- MD5 不安全:不要把它用于保护密码或关键数据。现代应用应使用
bcrypt
,scrypt
,argon2
等带盐(salt)和成本因子的方案。 - 时间成本:暴力穷举复杂度是 |charset|^length,很快变得不可行。字典攻击在真实世界更实用(如果密码是常见词)。
- 合法性:只对你有权测试的哈希 / 系统使用本脚本。未经授权的破解是违法的。
- 优化方向:可并行化、使用 GPU、或引入彩虹表、预计算表或更智能的规则(变体、大小写替换、数字后缀等)来提高命中率。
正文完