彩虹表攻击详解及如何防御
← 返回博客
彩虹表攻击详解及如何防御
· 7 分钟阅读
从暴力破解到预计算:攻击演进史
要理解彩虹表,先要理解它解决的问题。最朴素的密码破解方式是暴力破解:尝试所有可能的密码组合,对每个候选密码计算哈希,与目标哈希比对。这种方法在每次破解时都要重新计算,时间成本高。于是出现了更聪明的方案:为所有可能的密码预先计算哈希,存成一张巨大的对照表("密码→哈希"字典),破解时直接查表——用存储空间换时间。这就是查找表(Lookup Table)攻击,彩虹表是其更精巧的变体。
彩虹表的核心思想:时空权衡
简单查找表的问题是体积太大——存储所有 8 位字母数字密码的 MD5 哈希需要约 860 GB。1980 年,Martin Hellman 提出了"时空权衡"方案,1994 年 Philippe Oechslin 进一步优化为彩虹表。彩虹表的核心是"链":从一个密码出发,交替执行哈希(H)和规约(R)函数,生成一条密码→哈希→密码→哈希→...的链,最终只存储链的头部和尾部。破解时,对目标哈希执行规约函数,在链尾部查找是否匹配,如果找到对应的链头,从头重新计算即可还原密码。
/* Rainbow table chain construction */
p0 → H → h0 → R0 → p1 → H → h1 → R1 → p2 → ... → pt
Store: (p0, pt) only
/* Key insight:
H = hash function (MD5/SHA1 etc.)
R = reduction function (maps hash back to a candidate password)
Different R functions for each step (hence "rainbow")
Only the chain endpoints are stored - huge space saving */
/* Cracking example (target hash = hx): */
// Step 1: Apply Rt → pt' — is pt' in any chain tail?
// Step 2: Apply Rt-1 then H → ht-1' — is Rt(ht-1') in any tail?
// ... repeat until match found or exhausted
// Once tail match found, replay from chain head to find preimage
彩虹表的实际威力
彩虹表对无盐哈希的威胁是实质性的。一张覆盖所有 8 位字符(大小写字母+数字+常用符号)的 MD5 彩虹表,下载大小约 1–8 GB,查询时间在普通硬件上通常不超过几秒。知名彩虹表数据库(如 RainbowCrack、Free Rainbow Tables、CrackStation 等)已经预计算了大量常见密码格式的哈希,包括:全部 8 位以内的纯数字密码、全部 8 位以内的小写字母密码、数百万条常用密码词典。这意味着如果数据库泄露的是无盐 MD5,几乎所有弱密码都可以在分钟内被还原。
盐值(Salt):最有效的彩虹表克星
盐值是防御彩虹表的最直接方法。盐值是在哈希密码前附加的随机字符串,每个用户的盐值不同。有了盐值,相同的密码对不同用户会产生完全不同的哈希——攻击者必须为每个用户单独重建彩虹表,而这在计算上是不可行的。正确的盐值实现要点:每个用户独立生成随机盐值(至少 16 字节),使用密码学安全的随机数生成器,盐值明文存储在数据库中(盐值的目的不是保密,而是唯一性)。
# Python: correct salted hashing
import os, hashlib
def hash_password(password: str) -> tuple[str, str]:
# Generate 16-byte cryptographically secure random salt
salt = os.urandom(16).hex() # 32-char hex string
salted = salt + password
hashed = hashlib.sha256(salted.encode()).hexdigest()
return salt, hashed # store both in database
def verify_password(password: str, salt: str, stored_hash: str) -> bool:
salted = salt + password
computed = hashlib.sha256(salted.encode()).hexdigest()
return computed == stored_hash
# Usage
salt, hash_val = hash_password("user_password")
# Store: username, salt, hash_val in DB
# Verification
is_valid = verify_password("user_password", salt, hash_val)
# Why this defeats rainbow tables:
# Attacker would need to build a separate rainbow table
# for EACH unique salt value — completely impractical
为什么 bcrypt/Argon2 比手动加盐更好
虽然手动加盐 + SHA256 可以抵抗彩虹表,但它无法抵抗另一种攻击:针对特定用户的暴力破解。SHA256 速度极快,攻击者即使必须为每个盐值单独暴力破解,在 GPU 上每秒仍可尝试数十亿次。bcrypt 和 Argon2 解决了这两个问题:自动生成并嵌入盐值(防彩虹表),通过成本因子让每次哈希变慢(防暴力破解)。在现代 GPU 上,bcrypt(cost=12) 每秒只能计算约 10 万次,Argon2id 由于额外的内存要求,抵抗力更强。
// Node.js: bcrypt automatically handles salting
const bcrypt = require('bcryptjs');
// Hash: auto-generates and embeds a random salt
const hash = await bcrypt.hash(password, 12); // cost factor = 12
// Result: "$2b$12$[22-char salt embedded][31-char hash]"
// The embedded salt format means:
// - Each user has a unique salt (rainbow table useless)
// - 2^12 = 4096 iterations (slow - brute force expensive)
// - Everything needed for verification in one string
// Verify: automatically extracts salt from stored hash
const isValid = await bcrypt.compare(password, storedHash);
// Argon2id (even better for 2025)
const argon2 = require('argon2');
const hash2 = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB memory required
timeCost: 3, // 3 iterations
parallelism: 4 // 4 parallel threads
});
// Memory requirement defeats GPU/ASIC acceleration
现实中彩虹表攻击的局限
彩虹表并非万能,它有几个固有限制:覆盖范围有限(只能覆盖预先计算的密码空间;长度超过 10 位的随机密码实际上无法被彩虹表覆盖)、只对无盐哈希有效(任何正确实现了盐值的系统都能抵御彩虹表)、存储成本不低(完整覆盖 10 位字符密码的表需要 TB 级存储)、GPU 暴力破解往往更实用(对于 MD5 等快速哈希,直接 GPU 暴力破解有时比查彩虹表更快,尤其是对于有盐值但使用弱哈希的密码)。
完整防御策略
- **使用设计用于密码的哈希函数:**bcrypt、Argon2id 或 PBKDF2(按推荐顺序);绝不对密码使用 MD5、SHA1、SHA256 等通用哈希
- **盐值由库自动处理:**使用上述函数时无需手动管理盐值——它们会自动生成、嵌入并在验证时提取盐值
- **如果必须手动加盐(非密码场景):**为每条记录生成至少 16 字节的密码学随机盐值,将盐值与哈希值一起存储
- **检测已有数据库的漏洞:**检查存储的哈希是否以
$2b$(bcrypt)或$argon2开头;如果只是纯 MD5/SHA 哈希(32/40/64 位十六进制),意味着没有盐值,需要立即迁移 - **数据库泄露后的响应:**强制所有用户重置密码,即使使用了正确的密码哈希,因为弱密码在足够的算力下仍可能被破解
如何检测你的系统是否存在风险
# Signs your database is vulnerable to rainbow table attacks:
# 1. Hashes are exactly 32 hex chars → MD5, no salt
SELECT password_hash FROM users LIMIT 5;
-- 5f4dcc3b5aa765d61d8327deb882cf99 ← MD5("password"), no salt
# 2. Hashes are exactly 40 hex chars → SHA1, possibly no salt
-- aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
# 3. Hashes are exactly 64 hex chars → SHA256, may or may not have salt
-- 5e884898da28047151d0e56f8dc6292773603d0...
# Safe hash formats (salt embedded):
# bcrypt: $2b$12$[53 base64 chars]
# Argon2: $argon2id$v=19$m=65536,t=3,p=4$[salt]$[hash]
# PBKDF2: pbkdf2_sha256$260000$[salt]$[hash]
# Quick audit query (PostgreSQL/MySQL)
SELECT
COUNT(*) as total,
SUM(CASE WHEN password_hash LIKE '$2b$%' THEN 1 ELSE 0 END) as bcrypt,
SUM(CASE WHEN password_hash LIKE '$argon2%' THEN 1 ELSE 0 END) as argon2,
SUM(CASE WHEN LENGTH(password_hash) = 32 THEN 1 ELSE 0 END) as md5_unsalted,
SUM(CASE WHEN LENGTH(password_hash) = 64 THEN 1 ELSE 0 END) as sha256_maybe_unsalted
FROM users;
立即尝试在线工具,无需安装,免费使用。
打开工具 →
立即免费使用相关工具
免费使用 →