← 返回博客

彩虹表攻击详解及如何防御

2026-04-19 · 5 分钟阅读

← 返回博客

彩虹表攻击详解及如何防御

· 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 暴力破解有时比查彩虹表更快,尤其是对于有盐值但使用弱哈希的密码)。

完整防御策略

如何检测你的系统是否存在风险

# 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;

立即尝试在线工具,无需安装,免费使用。

打开工具 →

立即免费使用相关工具

免费使用 →