彩虹表(Rainbow Table)详解:从哈希、暴力破解到时间-空间折中

文章目录
-
- 1. 什么是彩虹表
- 2. 什么是哈希
- 3. 为什么攻击者想“反查”哈希?
- 4. 常见破解方式对比
- 5. 彩虹表的核心:H 函数和 R 函数
- 6. 什么是 Reduction Function?
- 7. 哈希链:彩虹表的基本结构
- 8. 查询彩虹表时怎么找明文?
- 9. 为什么叫“彩虹表”?
- 10. 一个极简例子:4 位数字密码
- 11. Python 玩具实验:生成一个小型彩虹表
- 12. Python 玩具实验:用彩虹表查找目标哈希
- 13. 彩虹表为什么可能查不到?
- 14. 彩虹表和普通字典表的区别
- 15. 彩虹表的优势
- 16. 彩虹表的局限
- 17. 什么是加盐?
- 18. 为什么 salt 能防彩虹表?
- 19. Pepper 又是什么?
- 20. 慢哈希:让每次猜测都更贵
- 21. 彩虹表在真实世界为什么越来越少见?
- 22. CTF 中怎么看彩虹表相关题?
- 23. 常见误区
- 24. 开发者应该如何安全存储密码?
- 25. 安全建议清单
- 26. 彩虹表、暴力破解、字典攻击总结对比
- 27. 一张图总结彩虹表
- 28. 结论
- 参考资料
1. 什么是彩虹表
彩虹表是一种“提前算好一部分哈希结果”的查询表,用来在拿到密码哈希后更快地反推出可能的明文密码。
更准确地说:
一种破解哈希函数的技术
彩虹表不是直接保存“所有明文 → 哈希”的完整字典,而是通过“哈希函数 H + 还原函数 R”构造很多条链,只保存每条链的起点和终点,从而用更少的存储空间换取较快的查找速度。
它的核心思想叫:
Time-Memory Tradeoff
时间-空间折中
也就是:
- 不想每次都从头暴力破解,太慢;
- 也不想保存所有明文和哈希,太占空间;
- 那就提前计算一部分链,只保存关键节点;
- 查询时再临时补算链条,找到可能的明文。
总而言之,就是将口令映射的Hash的映射表压缩。
2. 什么是哈希
2.1 什么是哈希?
哈希函数可以把任意长度的数据转换成固定长度的摘要。
例如:
password123 --MD5--> 482c811da5d5b4bc6d497ffa98491e38
admin --MD5--> 21232f297a57a5a743894a0e4a801fc3
123456 --MD5--> e10adc3949ba59abbe56e057f20f883e
2.2 哈希的几个特点
| 特点 | 含义 |
|---|---|
| 单向性 | 从明文容易算出哈希,但从哈希反推明文非常困难 |
| 固定长度 | 无论输入多长,输出长度通常固定 |
| 雪崩效应 | 输入改一点,输出会大幅变化 |
| 抗碰撞性 | 不同输入理论上可能得到相同哈希 |
| 速度快 | MD5、SHA1、SHA256 这类通用哈希计算很快 |
还有抗原像攻击(无法逆向),混合变幻(哈希变化均匀)等特性。
图 1:哈希的基本过程
3. 为什么攻击者想“反查”哈希?
很多系统不会直接存储用户密码,而是存储密码哈希。
假设数据库中有一条记录:
username: zhangsan
password_hash: e10adc3949ba59abbe56e057f20f883e
如果系统使用的是 MD5,那么这个哈希可能对应:
123456
攻击者如果拿到了数据库,就会尝试:
猜测密码 -> 计算哈希 -> 和数据库哈希对比
图 2:离线口令猜测流程
4. 常见破解方式对比
在理解彩虹表前,先看三种常见方式。
4.1 暴力破解
暴力破解就是枚举所有可能密码。
例如只考虑 4 位数字密码:
0000
0001
0002
...
9999
总共只有:
10^4 = 10000
但如果是 8 位大小写字母 + 数字:
62^8 = 218,340,105,584,896
数量会非常巨大。
图 3:暴力破解
优点:
- 不需要提前准备;
- 理论上覆盖完整空间。
缺点:
- 很慢;
- 密码空间稍大就不可接受。
4.2 普通字典表
普通字典表提前保存大量:
明文密码 -> 哈希值
示例:
| 明文 | MD5 |
|---|---|
| 123456 | e10adc3949ba59abbe56e057f20f883e |
| password | 5f4dcc3b5aa765d61d8327deb882cf99 |
| admin | 21232f297a57a5a743894a0e4a801fc3 |
查询时直接查表。
图 4:普通哈希字典表
优点:
- 查询非常快。
缺点:
- 表非常大;
- 每种哈希算法都要重新建表;
- 如果加盐,基本失效。
4.3 彩虹表
彩虹表介于“暴力破解”和“完整字典表”之间。
| 方法 | 预计算 | 存储量 | 查询速度 | 适用场景 |
|---|---|---|---|---|
| 暴力破解 | 无 | 很小 | 慢 | 密码空间小 |
| 完整字典表 | 很多 | 极大 | 很快 | 未加盐哈希 |
| 彩虹表 | 较多 | 中等 | 较快 | 未加盐、弱哈希、密码空间可控 |
图 5:三种方式对比
5. 彩虹表的核心:H 函数和 R 函数
彩虹表里最重要的是两个函数:
| 函数 | 名称 | 作用 |
|---|---|---|
| H | Hash Function,哈希函数 | 把明文变成哈希 |
| R | Reduction Function,还原/归约函数 | 把哈希转换回一个“候选明文格式” |
注意:
R 不是解密函数,也不能真正把哈希还原成原密码。
R 的作用只是把一个哈希值映射成另一个可能的候选密码。
再次强调,R函数不是逆向。
具体参考下面。
6. 什么是 Reduction Function?
假设密码空间是:
0000 ~ 9999
也就是 4 位数字。
哈希值可能是很长的一串:
e10adc3949ba59abbe56e057f20f883e
Reduction Function 可以做一个简单映射:
取哈希值中的一部分 -> 转成数字 -> 对 10000 取模 -> 格式化成 4 位数字
示例:
R("e10adc3949ba59abbe56e057f20f883e")
= int("e10adc39", 16) % 10000
= 4577
= "4577"
这就得到一个新的候选密码:
4577
图 6:R 函数不是解密,而是映射
7. 哈希链:彩虹表的基本结构
一条链的生成过程如下:
明文 -> H -> 哈希 -> R -> 新明文 -> H -> 哈希 -> R -> 新明文 ...
例如:
1234 --H--> h1 --R--> 8372 --H--> h2 --R--> 0491 --H--> h3 --R--> 6620
只保存:
起点:1234
终点:6620
中间的内容不保存。
图 7:一条哈希链
彩虹表保存的是很多条这样的链:
| 起点 | 终点 |
|---|---|
| 1234 | 6620 |
| 8888 | 1372 |
| 0000 | 9261 |
| 4321 | 7019 |
这就是彩虹表的“表”。
这里的R函数不一定相同,可能是不同R函数将口令关联。
怎么说呢,就是一条彩虹链里每一步用的 R 函数是不一样的;不同链之间同位置的 R 函数是相同的。
具体参考第9项。
8. 查询彩虹表时怎么找明文?
假设目标哈希是:
target_hash
它可能出现在某条链的中间,而不是终点。
所以查询时要倒着模拟不同位置:
假设 target_hash 在链的最后一步前
假设 target_hash 在链的倒数第二步前
假设 target_hash 在链的倒数第三步前
...
每次都通过 R 和 H 往后推,看看能不能推到某个已知终点。
图 8:彩虹表查询思路
9. 为什么叫“彩虹表”?
早期的普通哈希链使用同一个 R 函数:
H -> R -> H -> R -> H -> R
这样容易出现链合并。
例如两条链只要中途碰到同一个值,后面就完全一样:
链 A: 1234 -> ... -> 8888 -> 1111 -> 2222
链 B: 5678 -> ... -> 8888 -> 1111 -> 2222
这会浪费覆盖率。
彩虹表使用多个不同的 R 函数:
R1, R2, R3, R4, ...
就像不同颜色一样,所以叫 Rainbow Table。
图 9:普通哈希链容易合并
图 10:彩虹表使用不同 R 函数降低合并概率
10. 一个极简例子:4 位数字密码
为了方便理解,假设:
密码空间:0000 ~ 9999
哈希函数:MD5
链长:5
链数:若干
生成链时:
起点 1234
第 1 轮:
1234 -> MD5 -> hash1 -> R1 -> 8372
第 2 轮:
8372 -> MD5 -> hash2 -> R2 -> 0491
第 3 轮:
0491 -> MD5 -> hash3 -> R3 -> 6620
第 4 轮:
6620 -> MD5 -> hash4 -> R4 -> 2918
第 5 轮:
2918 -> MD5 -> hash5 -> R5 -> 7340
最终表里只保存:
1234 -> 7340
如果有 1000 条链,每条链长度为 5,理论上可以覆盖最多:
1000 × 5 = 5000
个候选密码。
当然实际覆盖率会因为碰撞、重复、链合并而降低。
11. Python 玩具实验:生成一个小型彩虹表
下面代码只用于学习原理。密码空间限制为 4 位数字,方便演示。
import hashlib
import random
PASSWORD_SPACE = 10_000
CHAIN_LENGTH = 5
CHAIN_COUNT = 20
def md5_hex(s: str) -> str:
return hashlib.md5(s.encode()).hexdigest()
def reduce_func(hash_hex: str, round_index: int) -> str:
"""
将哈希值映射回 4 位数字密码空间。
round_index 用于模拟不同颜色的 R1、R2、R3...
"""
num = int(hash_hex[:8], 16)
value = (num + round_index * 997) % PASSWORD_SPACE
return f"{value:04d}"
def generate_chain(start: str):
current = start
for i in range(CHAIN_LENGTH):
h = md5_hex(current)
current = reduce_func(h, i)
return start, current
def build_rainbow_table():
table = {}
starts = set()
while len(starts) < CHAIN_COUNT:
start = f"{random.randint(0, PASSWORD_SPACE - 1):04d}"
if start in starts:
continue
starts.add(start)
chain_start, chain_end = generate_chain(start)
table[chain_end] = chain_start
return table
if __name__ == "__main__":
table = build_rainbow_table()
print("Rainbow Table: end -> start")
for end, start in table.items():
print(f"{end} -> {start}")
运行后得到:

12. Python 玩具实验:用彩虹表查找目标哈希
继续补充查询逻辑:
import hashlib
import random
PASSWORD_SPACE = 10_000
CHAIN_LENGTH = 5
CHAIN_COUNT = 1000
def md5_hex(s: str) -> str:
return hashlib.md5(s.encode()).hexdigest()
def reduce_func(hash_hex: str, round_index: int) -> str:
num = int(hash_hex[:8], 16)
value = (num + round_index * 997) % PASSWORD_SPACE
return f"{value:04d}"
def generate_chain(start: str):
current = start
for i in range(CHAIN_LENGTH):
h = md5_hex(current)
current = reduce_func(h, i)
return start, current
def build_rainbow_table():
table = {}
starts = set()
while len(starts) < CHAIN_COUNT:
start = f"{random.randint(0, PASSWORD_SPACE - 1):04d}"
if start in starts:
continue
starts.add(start)
chain_start, chain_end = generate_chain(start)
table[chain_end] = chain_start
return table
def regenerate_chain_and_find(start: str, target_hash: str):
"""
从某条链的起点重新生成整条链,
检查目标哈希是否出现在链中。
"""
current = start
for i in range(CHAIN_LENGTH):
h = md5_hex(current)
if h == target_hash:
return current
current = reduce_func(h, i)
return None
def crack_with_rainbow_table(target_hash: str, table: dict):
"""
尝试在彩虹表中恢复 target_hash 对应的明文。
"""
for possible_position in range(CHAIN_LENGTH - 1, -1, -1):
h = target_hash
current = None
# 假设 target_hash 出现在 possible_position 这个位置,
# 从该位置一路推到链尾。
for i in range(possible_position, CHAIN_LENGTH):
current = reduce_func(h, i)
h = md5_hex(current)
# current 是推导出的链尾候选
if current in table:
chain_start = table[current]
result = regenerate_chain_and_find(chain_start, target_hash)
if result is not None:
return result
return None
if __name__ == "__main__":
table = build_rainbow_table()
target_password = "1234"
target_hash = md5_hex(target_password)
print("[+] target password:", target_password)
print("[+] target hash:", target_hash)
result = crack_with_rainbow_table(target_hash, table)
if result:
print("[+] found:", result)
else:
print("[-] not found")
你可能会看到:
[+] target password: 1234
[+] target hash: 81dc9bdb52d04dc20036dbd8313ed055
[+] found: 1234
也可能看到:
[-] not found
为什么?
因为小型彩虹表不一定覆盖了 1234 所在的链。
13. 彩虹表为什么可能查不到?
彩虹表不是万能的。
查不到可能有几个原因:
| 原因 | 解释 |
|---|---|
| 密码不在覆盖空间内 | 例如表只覆盖数字密码,但真实密码含字母 |
| 表覆盖率不足 | 链数和链长不够 |
| 链发生碰撞 | 多条链重复覆盖同一片区域 |
| 哈希算法不匹配 | MD5 表不能查 SHA1 |
| 加盐 | 每个用户哈希都不同,预计算表失效 |
| 使用慢哈希 | bcrypt、Argon2、scrypt 会显著增加计算成本 |
图 11:彩虹表失败原因
14. 彩虹表和普通字典表的区别
很多初学者容易混淆:
彩虹表不是简单的“哈希字典”。
普通字典表
password -> hash
123456 -> e10adc...
admin -> 21232f...
它保存完整映射。
彩虹表
chain_start -> chain_end
它只保存链起点和链终点。
中间节点需要查询时重新计算。
图 12:普通字典表 vs 彩虹表
15. 彩虹表的优势
15.1 查询比纯暴力更快
彩虹表提前做了大量计算,查询时不用从头枚举整个密码空间。
15.2 存储比完整字典更省
完整字典要存:
每个明文 + 每个哈希
彩虹表只存:
每条链的起点 + 终点
15.3 适合重复查询
如果你有很多同类型哈希需要分析,例如:
同一种算法
同一种密码长度范围
同一种字符集
没有加盐
彩虹表会更有价值。
16. 彩虹表的局限
16.1 只对特定算法有效
MD5 彩虹表不能用于 SHA1。
MD5(password) ≠ SHA1(password)
每一种哈希算法都需要单独建表。
16.2 只对特定密码空间有效
如果表只覆盖:
000000 ~ 999999
那它无法覆盖:
abc123
P@ssw0rd!
我爱安全123
16.3 加盐后几乎失效
这是最重要的一点。
17. 什么是加盐?
加盐就是在计算密码哈希时,引入一个随机值 salt。
原本:
hash = H(password)
加盐后:
hash = H(salt + password)
或者:
hash = H(password + salt)
每个用户的 salt 都应该不同。
图 13:未加盐 vs 加盐
同一个密码,使用不同 salt,会得到不同哈希。
例如:
H("saltA" + "123456") = hash_A
H("saltB" + "123456") = hash_B
这样攻击者不能用一张通用彩虹表查所有用户。
18. 为什么 salt 能防彩虹表?
彩虹表依赖“提前计算”。
如果没有 salt:
MD5("123456") 永远是 e10adc...
攻击者可以提前建表。
但如果每个用户 salt 不同:
MD5("salt_001" + "123456")
MD5("salt_002" + "123456")
MD5("salt_003" + "123456")
...
攻击者就必须为每个 salt 重新建表。
如果 salt 足够随机、足够长,预计算成本会爆炸。
图 14:salt 让预计算失去通用性
19. Pepper 又是什么?
除了 salt,还有一个概念叫 pepper。
| 项 | 是否公开存储 | 是否每个用户不同 | 作用 |
|---|---|---|---|
| salt | 通常公开存数据库 | 是 | 防止预计算和彩虹表 |
| pepper | 不存数据库,放配置/密钥管理系统 | 通常全局或分组 | 即使数据库泄露,也增加攻击成本 |
常见形式:
hash = HMAC(pepper, password + salt)
或者在密码哈希前后加入 pepper。
图 15:salt 与 pepper 的区别
20. 慢哈希:让每次猜测都更贵
MD5、SHA1、SHA256 都太快,不适合直接存密码。
安全的密码存储应使用专门的密码哈希方案,例如:
bcrypt
scrypt
Argon2
PBKDF2
它们的目标不是“越快越好”,而是:
让攻击者每猜一次密码都更贵。
图 16:普通哈希与慢哈希
21. 彩虹表在真实世界为什么越来越少见?
彩虹表在历史上很重要,但在现代系统中威力下降,原因包括:
- 越来越多系统使用 salt;
- 现代密码哈希算法带成本参数;
- GPU 暴力破解和规则字典攻击非常强;
- 大规模泄露密码数据让攻击者更偏向概率字典、规则变换、掩码攻击;
- 长随机密码和密码管理器普及后,固定密码空间的预计算价值下降。
但彩虹表仍然值得学习,因为它能帮你理解:
- 哈希为什么不能“解密”;
- 为什么不能用 MD5 存密码;
- 为什么 salt 非常重要;
- 什么是时间-空间折中;
- 为什么密码存储不能只靠“算法保密”。
22. CTF 中怎么看彩虹表相关题?
CTF 中,彩虹表常见于这些场景:
22.1 直接给出弱哈希
题目可能给:
e10adc3949ba59abbe56e057f20f883e
你需要判断:
这可能是 MD5("123456")
22.2 给出固定格式
例如题目提示:
flag 是 6 位数字
hash = xxxxx
这时可以枚举或建表。
22.3 给出多个未加盐哈希
如果多个哈希都来自同一算法和同一密码空间,就适合使用预计算思想。
图 17:CTF 中的判断流程
23. 常见误区
误区 1:哈希可以解密
错误。
哈希不是加密,没有密钥,也没有解密过程。
正确说法是:
找到一个明文,使得 H(明文) = 目标哈希
这叫“碰撞查找”或“原像查找”的实际口令恢复场景。
误区 2:彩虹表一定能破解密码
错误。
彩虹表只覆盖特定算法、特定字符集、特定长度范围。
误区 3:只要用 SHA256 存密码就安全
错误。
SHA256 很快,不适合直接存密码。
更推荐:
Argon2id
bcrypt
scrypt
PBKDF2
误区 4:salt 必须保密
不一定。
salt 的核心作用不是保密,而是让相同密码产生不同哈希,抵抗预计算攻击。
真正需要保密的是 pepper。
误区 5:加密和哈希是一回事
错误。
| 对比 | 加密 | 哈希 |
|---|---|---|
| 是否可逆 | 可逆 | 通常不可逆 |
| 是否有密钥 | 通常有 | 通常没有 |
| 典型用途 | 保护数据机密性 | 完整性校验、密码摘要 |
| 示例 | AES、RSA | MD5、SHA256、bcrypt |
24. 开发者应该如何安全存储密码?
推荐做法
password_hash = Argon2id(password, salt, cost)
数据库保存:
user_id
salt
hash
algorithm
cost_parameters
例如:
algorithm: argon2id
salt: random_128bit_salt
memory_cost: 65536
time_cost: 3
parallelism: 4
hash: xxxxxx
图 18:安全密码存储流程
登录验证流程
25. 安全建议清单
对个人用户
- 不要使用短密码,例如
123456、password、生日; - 使用密码管理器生成长随机密码;
- 不同网站不要复用密码;
- 开启多因素认证;
- 发现泄露后及时更换密码。
对开发者
- 不要明文存储密码;
- 不要直接使用 MD5、SHA1、SHA256 存密码;
- 为每个用户生成随机 salt;
- 使用 Argon2id、bcrypt、scrypt 或 PBKDF2;
- 设置合理成本参数;
- 可以考虑 pepper,并放在数据库之外;
- 限制登录尝试频率;
- 监控异常登录行为;
- 定期升级密码哈希策略。
26. 彩虹表、暴力破解、字典攻击总结对比
| 维度 | 暴力破解 | 字典攻击 | 完整哈希表 | 彩虹表 |
|---|---|---|---|---|
| 是否预计算 | 否 | 可选 | 是 | 是 |
| 存储成本 | 低 | 中 | 很高 | 中 |
| 计算成本 | 高 | 中 | 低 | 中 |
| 是否依赖密码规律 | 不一定 | 依赖 | 依赖预设空间 | 依赖预设空间 |
| 是否怕 salt | 是 | 是 | 是 | 非常怕 |
| 适合场景 | 小空间 | 弱口令 | 未加盐哈希 | 未加盐、固定空间 |
27. 一张图总结彩虹表
28. 结论
彩虹表的本质不是“神奇解密”,而是:
用预计算 + 哈希链 + 起终点存储
在时间和空间之间做折中
它能对未加盐、弱哈希、固定密码空间的系统造成威胁。
但现代防御并不复杂:
随机 salt + 慢哈希 + 合理成本参数 + 不复用密码 + MFA
一句话总结:
彩虹表告诉我们:密码哈希不能只追求“算得出来”,还要让攻击者“猜不起”。
参考资料
- NIST SP 800-63B:数字身份指南中关于密码存储、salt、成本因子的建议。
- OWASP Password Storage Cheat Sheet:密码存储安全实践,包括 salt、pepper、工作因子和现代密码哈希算法。
- Philippe Oechslin, “Making a Faster Cryptanalytic Time-Memory Trade-Off”, CRYPTO 2003.
- Martin Hellman, “A Cryptanalytic Time-Memory Trade-Off”, IEEE Transactions on Information Theory.
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)