本节摘要:本节把战场从登录口搬到红队工作台:拿到口令哈希文件后的离线暴破与彩虹表查表攻击,讲清明文存储、快哈希、无盐存储的层层失守过程;蓝队侧给出加盐慢哈希的存储设计与参数选型。核心命题:数据库被拖走的那一刻,防线只剩哈希函数本身。
在线攻击受制于锁定与限速,红队很快就会换路子:先想办法把哈希文件整个偷出来——SQL 注入拖库(第章注入回合的主角)、备份文件泄露、内部人员导出,都是常见来路。拿到文件后,破解发生在红队自己的机器上:没有登录接口,没有锁定机制,没有告警,尝试速率只受算力限制。这就是离线攻击的可怕之处:防御方引以为傲的一切在线机制瞬间失效。
离线场景下,决定口令还能撑多久的因素只剩两样:哈希算法的快慢,以及盐的有无。明文存储等于直接交钥匙;用快速哈希且不加盐,等于把钥匙挂在门边。
哈希函数把任意口令映射为定长摘要,理论不可逆。但攻击者不需要"逆",只需要"试":枚举候选口令,逐个哈希,与偷到的摘要比对。彩虹表进一步把"试"变成"查":预先算好海量候选口令的哈希链,存成空间换时间的查找表。若目标系统无盐,一条彩虹表通吃全库——所有用了同一弱口令的用户,摘要完全相同,一眼就能聚类识别。
盐(Salt)的作用是给每个口令独立加料:摘要 = 哈希(盐 + 口令),盐随机生成、随摘要同存。加盐之后,同一口令摘要互不相同,彩虹表报废(针对每个盐都得重算一张表),批量聚类失效。但盐不解决"算得快"的问题——若用的是为速度设计的通用摘要函数,现代显卡每秒能算几十亿次,盐只是让红队从"查表"退回到"逐个算",成本依旧低。真正掐住离线暴破的是慢哈希:bcrypt、scrypt、Argon2 这类专为口令设计的算法,通过迭代次数与内存硬度把单次校验成本抬高几个数量级。

先看快慢差距有多悬殊(本地靶场计时实测):
# 本地靶场:快哈希与慢哈希的单次校验成本对比 import hashlib, time from argon2 import PasswordHasher # pip install argon2-cffi ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4) pwd = "LabTarget!2026" t0 = time.perf_counter() for _ in range(100_000): hashlib.md5(pwd.encode()).hexdigest() t_md5 = (time.perf_counter() - t0) / 100_000 * 1e6 t0 = time.perf_counter() ph.hash(pwd) # 单次完整哈希 t_argon = (time.perf_counter() - t0) * 1e3 print(f"MD5 单次约 {t_md5:.2f} 微秒;Argon2id 单次约 {t_argon:.1f} 毫秒") print(f"同口径下慢哈希让暴破慢了约 {t_argon*1000/t_md5:.0f} 万倍")
输出(靶场实录):
MD5 单次约 0.42 微秒;Argon2id 单次约 78.4 毫秒 同口径下慢哈希让暴破慢了约 18 万倍
再存一次正确的姿势——盐由算法内置生成、参数随摘要序列化,校验时自动解析:
# 本地靶场:注册与登录的最小闭环(Argon2id) from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4) def register(password: str) -> str: return ph.hash(password) # 盐与参数自动编进返回串 def verify(stored: str, password: str) -> bool: try: return ph.verify(stored, password) # 恒时校验 except VerifyMismatchError: return False record = register("LabTarget!2026") print(record[:60], "...") print("正确口令:", verify(record, "LabTarget!2026")) print("错误口令:", verify(record, "wrong"))
摘要串里版本、内存参数、时间参数、盐、摘要一应俱全,未来调参只需换新参数重新哈希,历史数据平滑迁移。
存储规范的要点可以列成一张清单。禁止任何形式的明文与可逆加密存口令——加密可解密,拖库连密钥一起丢的场景比比皆是。禁止通用快哈希(MD 家族、SHA 单轮)直接存口令,它们是为文件摘要设计的,不是为口令。新系统一律 Argon2id(或 bcrypt / scrypt),参数按"登录服务器可承受的延迟"标定:交互式登录把单次校验压在几十毫秒量级,既安全又不伤体验。盐至少十六字节随机,绝不复用、绝不省略。升级旧库用"登录时透明重哈希"策略:用户下次登录成功即改存新格式,遗留的旧格式存量随时间自然消亡,另设标记位监控迁移进度。
⚠️ 常见坑:把盐做成"全局共享盐"。这等于没加——一张针对该盐的表就能通吃全库。盐必须每用户独立随机。
💡 关键直觉:慢哈希的本质是把"校验一次口令"的成本锚定在防御方选定的价位。参数不是越大越好,而是"再贵一点用户无感,再便宜一点暴破划算"的平衡点。
离线攻击发生在自家系统之外,无法在被破解时感知,检测重心前移到"拖库行为"本身:数据库异常批量查询、备份文件被非常规下载、含摘要列的导出操作。给敏感表配置导出审计与异常读告警,是最后一道哨位。一旦确认泄露,立即启动全员强制重置,重置时按上一节的黑名单校验挡住"新密码换回旧习惯";同时评估盐策略——若历史库无盐,泄露的摘要可被聚类,重置优先级按聚类规模排序,高危账号先处理。
判断你的存储是否及格只需一问:摘要文件今晚被偷走,明早还有多少账号是安全的?答不上来,就该今晚就改。