在这里插入图片描述

文章目录


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:哈希的基本过程

明文密码
password123

哈希函数
MD5 / SHA1 / SHA256

哈希值
482c811d...


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:暴力破解

候选空间
0000 ~ 9999

逐个计算哈希

对比目标哈希

命中或失败

优点:

  • 不需要提前准备;
  • 理论上覆盖完整空间。

缺点:

  • 很慢;
  • 密码空间稍大就不可接受。

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 函数不是解密,而是映射

哈希值
e10adc39...

Reduction 函数 R

新的候选明文
4577


7. 哈希链:彩虹表的基本结构

一条链的生成过程如下:

明文 -> H -> 哈希 -> R -> 新明文 -> H -> 哈希 -> R -> 新明文 ...

例如:

1234 --H--> h1 --R--> 8372 --H--> h2 --R--> 0491 --H--> h3 --R--> 6620

只保存:

起点:1234
终点:6620

中间的内容不保存。

图 7:一条哈希链

起点
1234

H

h1

R

8372

H

h2

R

0491

H

h3

R

终点
6620

彩虹表保存的是很多条这样的链:

起点 终点
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:彩虹表查询思路

目标哈希 target_hash

从不同位置假设它在链中

反复执行 R -> H -> R -> H

是否得到某个终点?

找到对应链的起点

从起点重新生成整条链

检查目标哈希是否出现

若出现,得到对应明文

继续尝试其他位置


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:普通哈希链容易合并

链 A 起点

...

相同中间值

链 B 起点

...

后续完全相同

相同终点

图 10:彩虹表使用不同 R 函数降低合并概率

明文 P0

H

hash1

R1

P1

H

hash2

R2

P2

H

hash3

R3

P3


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:彩虹表失败原因

彩虹表查不到

密码空间问题

字符集不匹配

长度不匹配

规则不匹配

表本身问题

链数太少

链长不合适

碰撞与覆盖不足

系统防护

加盐

慢哈希

Pepper

多因素认证


14. 彩虹表和普通字典表的区别

很多初学者容易混淆:

彩虹表不是简单的“哈希字典”。

普通字典表

password -> hash
123456   -> e10adc...
admin    -> 21232f...

它保存完整映射。

彩虹表

chain_start -> chain_end

它只保存链起点和链终点。

中间节点需要查询时重新计算。

图 12:普通字典表 vs 彩虹表

彩虹表

链起点 1234

链终点 7340

链起点 8888

链终点 1372

链起点 0000

链终点 9261

普通字典表

123456

e10adc...

admin

21232f...

password

5f4dcc...


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 加盐

加盐

salt1 + password123

H(salt1 + password123)

哈希 A

salt2 + password123

H(salt2 + password123)

哈希 B

未加盐

password123

H(password123)

固定哈希

同一个密码,使用不同 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 让预计算失去通用性

一张未加盐彩虹表

可尝试匹配大量未加盐哈希

用户 A 的 salt

需要 A 专用表

用户 B 的 salt

需要 B 专用表

用户 C 的 salt

需要 C 专用表


19. Pepper 又是什么?

除了 salt,还有一个概念叫 pepper。

是否公开存储 是否每个用户不同 作用
salt 通常公开存数据库 防止预计算和彩虹表
pepper 不存数据库,放配置/密钥管理系统 通常全局或分组 即使数据库泄露,也增加攻击成本

常见形式:

hash = HMAC(pepper, password + salt)

或者在密码哈希前后加入 pepper。

图 15:salt 与 pepper 的区别

用户密码

加入 salt

密码哈希算法

pepper
存放在数据库外

最终密码哈希

数据库

保存 salt + hash


20. 慢哈希:让每次猜测都更贵

MD5、SHA1、SHA256 都太快,不适合直接存密码。

安全的密码存储应使用专门的密码哈希方案,例如:

bcrypt
scrypt
Argon2
PBKDF2

它们的目标不是“越快越好”,而是:

让攻击者每猜一次密码都更贵。

图 16:普通哈希与慢哈希

密码

MD5/SHA1/SHA256
速度很快

攻击者可高速尝试

密码

bcrypt / scrypt / Argon2 / PBKDF2
带成本参数

每次尝试成本更高


21. 彩虹表在真实世界为什么越来越少见?

彩虹表在历史上很重要,但在现代系统中威力下降,原因包括:

  1. 越来越多系统使用 salt
  2. 现代密码哈希算法带成本参数
  3. GPU 暴力破解和规则字典攻击非常强;
  4. 大规模泄露密码数据让攻击者更偏向概率字典、规则变换、掩码攻击;
  5. 长随机密码和密码管理器普及后,固定密码空间的预计算价值下降。

但彩虹表仍然值得学习,因为它能帮你理解:

  • 哈希为什么不能“解密”;
  • 为什么不能用 MD5 存密码;
  • 为什么 salt 非常重要;
  • 什么是时间-空间折中;
  • 为什么密码存储不能只靠“算法保密”。

22. CTF 中怎么看彩虹表相关题?

CTF 中,彩虹表常见于这些场景:

22.1 直接给出弱哈希

题目可能给:

e10adc3949ba59abbe56e057f20f883e

你需要判断:

这可能是 MD5("123456")

22.2 给出固定格式

例如题目提示:

flag 是 6 位数字
hash = xxxxx

这时可以枚举或建表。

22.3 给出多个未加盐哈希

如果多个哈希都来自同一算法和同一密码空间,就适合使用预计算思想。

图 17:CTF 中的判断流程

有 salt

无 salt

拿到哈希

判断算法类型

是否有 salt?

彩虹表价值降低

判断密码空间

数字?小写?固定长度?

可尝试字典 / 枚举 / 预计算


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:安全密码存储流程

用户注册输入密码

生成随机 salt

使用 Argon2id / bcrypt / scrypt

得到 password_hash

数据库保存 salt + hash + 参数

登录验证流程

用户登录输入密码

读取数据库中的 salt 和参数

使用相同算法重新计算 hash

是否等于数据库 hash?

登录成功

登录失败


25. 安全建议清单

对个人用户

  • 不要使用短密码,例如 123456password、生日;
  • 使用密码管理器生成长随机密码;
  • 不同网站不要复用密码;
  • 开启多因素认证;
  • 发现泄露后及时更换密码。

对开发者

  • 不要明文存储密码;
  • 不要直接使用 MD5、SHA1、SHA256 存密码;
  • 为每个用户生成随机 salt;
  • 使用 Argon2id、bcrypt、scrypt 或 PBKDF2;
  • 设置合理成本参数;
  • 可以考虑 pepper,并放在数据库之外;
  • 限制登录尝试频率;
  • 监控异常登录行为;
  • 定期升级密码哈希策略。

26. 彩虹表、暴力破解、字典攻击总结对比

维度 暴力破解 字典攻击 完整哈希表 彩虹表
是否预计算 可选
存储成本 很高
计算成本
是否依赖密码规律 不一定 依赖 依赖预设空间 依赖预设空间
是否怕 salt 非常怕
适合场景 小空间 弱口令 未加盐哈希 未加盐、固定空间

27. 一张图总结彩虹表

选择密码空间
如 0000-9999

选择哈希算法
如 MD5

设计多个 R 函数
R1 R2 R3...

生成大量哈希链

只保存链起点和链终点

拿到目标哈希

模拟目标哈希到链尾

链尾是否在表中?

从链起点重建链

找到目标哈希对应明文

查找失败


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.
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐