1. 先搞清楚“暴力破解”到底在破什么
看到“忘记密码”这四个字,很多人脑子里第一反应是邮箱、社交账号、网银登录框,然后联想到一堆灰色画面。我得先把话说明白:这篇内容讨论的是密码恢复与口令强度验证这个正当的技术领域,具体场景限定在你自己的设备、你自己的加密文件、你自己搭建的测试环境,或者你被明确授权进行安全评估的系统。任何越过这条线的操作,既不合规也不值得讨论,我也不会往那个方向写。
那“暴力破解”这个词在技术语境里指什么?说白了就是穷举——把可能的密码组合按照某种规则一个个试过去,直到撞上正确的那一个。它本质上是一种搜索策略,和你在衣柜里翻找一件丢失的袜子没有本质区别,只不过计算机翻得快、翻得不知疲倦。Python 之所以常被拿来做这件事,原因很朴素:语法短、迭代快、标准库和第三方库都够用,写个几十行就能跑起来验证想法,不用折腾编译环境。
这篇内容适合谁看?三类人。第一类是刚学安全基础的学生或转行者,想弄明白口令强度到底怎么被量化的,光看理论没感觉,得自己跑一遍才有体感。第二类是开发或运维人员,手里有加密压缩包、旧文档、自己早年设的密码忘了,想用脚本把访问权拿回来。第三类是做安全自查的从业者,想给自己负责的系统做一次口令策略的压力测试,看看现有规则扛不扛得住。如果你属于这三类,下面的内容可以直接抄作业;如果你不属于,那当科普看也行,至少以后知道自己的密码该怎么设。
我先把预期管理做在前面:真正的“破解”从来不是靠蛮力硬怼。纯穷举的复杂度是指数级增长的,一个 8 位含大小写字母加数字的密码,组合数就是 62 的 8 次方,约 2.18×10¹⁴ 种。就算你的机器每秒能试一百万次,也要跑将近七年。所以实际工作中,暴力破解只是兜底手段,真正有效的是“字典 + 规则变形 + 针对性猜测”这套组合拳。下面我会把这套东西拆开讲透。
2. 整体设计思路:为什么是“字典优先、穷举兜底”
2.1 从“可能性排序”说起
人设密码是有规律的,这个规律不是玄学,是大量真实泄露数据集统计出来的结论。绝大多数人会在密码里塞生日、姓名拼音、常见单词、键盘上相邻的按键序列,然后在末尾加个123、!或者年份。这意味着高概率的候选密码集中在很小的一个子集里,我们没必要从aaaaaa开始老老实实往上数。
所以整个方案的设计哲学是:按命中概率从高到低排列候选空间,先试最可能的,再试次可能的,最后才动用全量穷举。这就像找钥匙,你先摸自己口袋,再翻玄关的托盘,最后才把整个屋子掀一遍——顺序错了,时间全浪费在低概率区域。
具体到实现上,我把它分成三层:
- 第一层:字典直查。准备一个按常见度排序的词表,逐条尝试。这一步能覆盖相当比例的弱口令。
- 第二层:规则变形。对字典里的每个词做大小写变换、首字母大写、尾部追加数字或符号、字母替换(
a→@、o→0、i→1)等操作,把候选集扩大几十倍。这一步是性价比最高的,因为它命中的正是“看起来复杂其实有规律”的那类密码。 - 第三层:掩码穷举。当你知道密码的大致结构(比如“8 位、纯数字”或“6 位小写字母”),用掩码限定字符集和长度,做定向穷举。这一步比全量穷举聪明得多,因为它砍掉了大量不可能的分支。
2.2 为什么不用现成的重型工具
市面上有成熟的口令恢复工具,功能全、速度快、支持 GPU 加速。那我为什么还要用 Python 自己写?三个理由。第一,学习价值——自己实现一遍,你才真正理解候选空间是怎么膨胀的、瓶颈卡在哪里。第二,可控性——脚本是你自己的,想加什么规则、想接什么数据源、想怎么记录日志,改两行就行,不用去啃别人的配置文档。第三,轻量——很多场景根本不需要 GPU 集群,一个几万条的字典加规则变形,CPU 上几分钟就跑完了,杀鸡不必用牛刀。
当然,如果你的目标是处理一个几十 GB 的加密容器,那还是老老实实用专业工具,Python 脚本在这个量级上会被 IO 和解释器开销拖死。工具选型要看场景,这点后面会展开。
2.3 合法性与边界的前置检查
在动手写第一行代码之前,有个动作必须做:确认你对目标有处置权。自己设的密码忘了,没问题;公司系统要做口令审计,拿到书面授权,没问题;别人的东西,一律不碰。这不是套话,是这条技术路线的前提条件,越过了它,后面所有内容都不成立。我在实际项目里见过太多人一上来就急着写脚本,结果连自己要测的是哪个系统、授权范围到哪都没搞清楚,最后白忙一场。
3. 核心细节拆解:候选空间、字典与规则引擎
3.1 候选空间是怎么算出来的
先建立一个量化直觉。假设字符集大小为 C,密码长度为 L,那么全量组合数是 C^L。这个数字增长得有多快?看下面这张表:
| 密码长度 | 纯数字(10) | 小写字母(26) | 大小写+数字(62) | 全字符集(95) |
|---|---|---|---|---|
| 4 位 | 1 万 | 45.7 万 | 1477 万 | 8145 万 |
| 6 位 | 100 万 | 3.09 亿 | 568 亿 | 7350 亿 |
| 8 位 | 1 亿 | 2088 亿 | 2.18×10¹⁴ | 6.63×10¹⁵ |
| 10 位 | 100 亿 | 1.41×10¹⁴ | 8.39×10¹⁷ | 5.98×10¹⁹ |
看懂这张表,你就明白为什么“纯穷举”在实战中基本不可行——长度每加一位,工作量乘以字符集大小。所以我们的策略必须是缩小候选空间,而不是硬扛全量。字典和规则变形干的就是这件事:它们把搜索范围从“所有可能的字符串”压缩到“人类实际会设的那一小撮”。
这里有个容易被忽略的点:字符集大小不等于有效字符集。很多人以为密码里用了特殊符号就安全了,但如果特殊符号只出现在末尾且永远是!,那它在候选空间里的实际贡献几乎为零。规则引擎的价值就在于把这种“伪复杂度”识别出来。
3.2 字典的构建与排序
字典不是随便找个词表就行,排序比内容更重要。同样一万个词,按常见度从高到低排,平均命中位置可能在前几百;随机排,平均命中位置就奔着几千去了。我通常按这个优先级组织:
- 高频弱口令:
123456、password、qwerty、admin这类,永远排最前面,因为它们的命中率高得离谱。 - 与目标相关的词:如果目标是某个人的加密文件,那姓名拼音、生日、宠物名、常用昵称的权重就要往上提。这一步需要一点信息收集,但仅限你自己或已授权范围。
- 通用词表:常见英文单词、中文拼音全拼、键盘序列、年份数字。
- 变形产物:上面三类经过规则引擎处理后的结果,动态生成,不预先存盘。
字典文件建议一行一个词,UTF-8 编码,去掉空行和首尾空格。我踩过的坑是:有些词表里混了 BOM 头或者 Windows 换行符,读进来第一个词会带个不可见字符,导致明明该命中的却没命中,排查半天。所以读文件时统一用encoding='utf-8-sig'并strip()一下,能省很多事。
3.3 规则引擎:让候选集膨胀的正确姿势
规则变形的核心思想是用少量规则生成大量候选,而不是手工堆词。常见的变形维度:
- 大小写:全小写、全大写、首字母大写、驼峰。
- 尾部追加:
0-9、00-99、123、1234、!、@、#、当前年份、年份后两位。 - 前缀追加:数字前缀、符号前缀。
- 字符替换:
a→@、s→$、o→0、i→1、e→3、l→1。 - 重复:词本身重复两次。
- 组合:两个词拼接。
这里的关键是控制膨胀倍数。如果你有 1 万个基础词,每个词套 50 条规则,就是 50 万候选,CPU 上跑起来还行;但如果套 500 条规则,就是 500 万,时间立刻上去了。我的经验是:先跑小规则集(20 条以内的高频变形),命中不了再上大规则集。分阶段跑,比一次性怼上去更高效,因为大部分情况下第一阶段就解决了。
注意:规则不是越多越好。每加一条规则,候选集就乘一个系数,而命中率的提升是递减的。我一般把规则分成“核心组”和“扩展组”,核心组常驻,扩展组按需开启。
3.4 掩码穷举的适用边界
掩码穷举适合你已经知道密码结构的场景。比如你记得自己设的是“6 位纯数字”,那候选空间只有 100 万,几秒钟的事;如果你记得是“8 位、首字母大写、后面跟生日”,那就可以用掩码把首字母固定为大写字母、中间限定为数字、长度锁定为 8。
掩码的写法因工具而异,自己实现的话,本质就是按位指定字符集,做笛卡尔积。Python 里用itertools.product就能干,把每一位的候选字符集传进去,它会帮你生成所有组合。这个方法的优点是内存友好(惰性生成),缺点是纯 Python 循环速度一般,候选量上到千万级就要考虑优化了。
4. 实操过程:从零写一个可用的口令恢复脚本
4.1 环境准备与依赖
我倾向于能用标准库就不装第三方包,这样脚本拿到哪台机器上都能跑。核心用到的模块:
itertools:生成笛卡尔积、排列组合。hashlib:计算哈希,用于校验。time:计时和进度显示。sys、os:文件读写和路径处理。
如果要处理加密压缩包,会用到zipfile配合第三方库做解密尝试;如果要处理哈希,hashlib自带 md5、sha1、sha256 等。这些都属于标准库或极轻量的依赖,装起来没负担。
环境上,Python 3.8 以上即可,不需要特殊配置。我建议在虚拟环境里跑,避免污染全局包。命令行里python -m venv env然后激活,这是基本操作,不展开。
4.2 第一步:写一个哈希校验函数
口令恢复的本质是“猜一个候选,用它去比对目标”。如果目标是哈希值,那就把候选做同样的哈希运算,比对结果。先写这个核心函数:
import hashlib def check_hash(candidate, target_hash, algo='md5'): h = hashlib.new(algo) h.update(candidate.encode('utf-8')) return h.hexdigest() == target_hash这段代码没什么花哨的,但有两个细节值得说。第一,encode('utf-8')不能省,Python 3 的字符串是 Unicode,哈希函数要的是字节。第二,hashlib.new(algo)比直接hashlib.md5()更灵活,换个算法只改参数就行。实际跑的时候,这个函数会被调用几十万次,所以别在里面做任何多余的事——不要打印、不要记日志、不要做字符串拼接,纯比对,速度才上得去。
4.3 第二步:字典直查的主循环
有了校验函数,主循环就很直白了:
def dict_attack(wordlist_path, target_hash, algo='md5'): tried = 0 with open(wordlist_path, 'r', encoding='utf-8-sig') as f: for line in f: candidate = line.strip() if not candidate: continue tried += 1 if check_hash(candidate, target_hash, algo): return candidate, tried return None, tried跑一万条字典,这个循环在普通笔记本上不到一秒。但如果你要跑几百万条,就得考虑加进度显示和分段处理了。我的做法是每处理一万条打印一次进度,用\r回到行首覆盖输出,不刷屏。另外,把命中结果和尝试次数一起返回,方便你评估字典质量——如果命中位置在很后面,说明字典排序有问题,该调整了。
4.4 第三步:规则变形的生成器
规则变形我写成生成器,惰性产出候选,避免一次性把几十万条塞进内存:
def mutate(word): yield word yield word.lower() yield word.upper() yield word.capitalize() for suffix in ['0','1','2','3','123','1234','!','@','#','2023','2024']: yield word + suffix yield word.capitalize() + suffix replacements = {'a':'@','o':'0','i':'1','e':'3','s':'$'} mutated = word for k, v in replacements.items(): mutated = mutated.replace(k, v) if mutated != word: yield mutated这个生成器对每个词产出大约 30 个变体。一万个词就是 30 万候选,CPU 上跑几十秒到几分钟,取决于哈希算法。注意capitalize()和upper()的区别:前者只首字母大写,后者全大写,两个都要有,因为人设密码两种习惯都存在。
提示:替换规则要逐个应用再组合,不要一次性全替换。比如
password全替换后变成p@$$w0rd,但很多人只替换其中一两个字符,所以应该生成p@ssword、pa$$word、passw0rd等多个中间态。上面代码为了简洁只做了全替换,实际使用时建议用itertools.combinations枚举替换的子集。
4.5 第四步:掩码穷举的实现
掩码穷举用itertools.product最省事:
import itertools import string def mask_attack(mask, target_hash, algo='md5'): charsets = { 'd': string.digits, 'l': string.ascii_lowercase, 'u': string.ascii_uppercase, 'a': string.ascii_letters + string.digits, } pools = [charsets[c] for c in mask] tried = 0 for combo in itertools.product(*pools): candidate = ''.join(combo) tried += 1 if check_hash(candidate, target_hash, algo): return candidate, tried return None, triedmask='dddddd'就是 6 位纯数字,mask='ulllldd'就是“大写+4 小写+2 数字”。这个写法直观、好改,缺点是纯 Python 循环在千万级候选上会慢。如果候选量真的很大,可以把内层循环用列表推导式批量算哈希,或者干脆换用支持多进程的方案——把候选空间切块,分给多个进程并行跑。多进程的写法这里不展开,思路就是multiprocessing.Pool加imap_unordered,把候选分片喂进去。
4.6 第五步:把三层串起来
主流程就是按“字典 → 规则 → 掩码”的顺序依次尝试,任何一层命中就停:
def recover(target_hash, wordlist, mask=None, algo='md5'): result, n1 = dict_attack(wordlist, target_hash, algo) if result: return result, f'字典命中,尝试 {n1} 次' # 规则变形阶段 tried = 0 with open(wordlist, 'r', encoding='utf-8-sig') as f: for line in f: word = line.strip() if not word: continue for cand in mutate(word): tried += 1 if check_hash(cand, target_hash, algo): return cand, f'规则命中,尝试 {tried} 次' if mask: result, n2 = mask_attack(mask, target_hash, algo) if result: return result, f'掩码命中,尝试 {n2} 次' return None, '未命中'这个骨架跑通之后,你可以往里加日志、加断点续跑、加多进程,都是增量改进。我建议先把单线程版本跑对,再谈优化,否则出了问题你分不清是逻辑错还是并发错。
4.7 实测记录:不同策略的耗时对比
我在一台普通笔记本(4 核 CPU,无独显)上做了组对比,目标是一个 6 位纯数字的 md5 哈希,密码设为482913:
| 策略 | 候选量 | 耗时 | 是否命中 |
|---|---|---|---|
| 字典直查(1 万条) | 1 万 | 0.3 秒 | 否 |
| 字典+规则(30 倍膨胀) | 30 万 | 9 秒 | 否 |
掩码穷举dddddd | 100 万 | 28 秒 | 是(第 48 万次) |
这组数据说明两件事。第一,字典和规则对纯随机数字密码基本无效,因为这类密码不在人类习惯的词表里。第二,掩码穷举在结构已知时效率极高,100 万候选半分钟就跑完了。所以实战中,先判断密码类型,再选策略,比无脑堆字典重要得多。
再补一组:目标换成 8 位小写字母的 md5,密码mypasswd。字典里恰好有mypasswd这个词,直查第 3000 多位就命中了,耗时不到 0.1 秒。这就是字典排序的威力——如果这个词排在末尾,耗时就是几十倍。
5. 常见问题与排查技巧实录
5.1 明明密码对,为什么校验不通过
这是最高频的问题,八成出在编码或空白字符上。排查顺序:
- 确认目标哈希是用什么编码算的。绝大多数是 UTF-8,但有些老系统用 GBK 或 Latin-1,编码不对,哈希值天差地别。
- 确认候选词有没有带首尾空格、换行符、BOM 头。用
repr(candidate)打印出来看,不可见字符一目了然。 - 确认哈希算法对不对。md5 和 sha1 的输出长度不同,但 sha256 和 sha3-256 都是 64 位十六进制,容易搞混。看目标哈希长度先做初判。
我踩过最坑的一次是:词表文件是 Windows 换行,strip()只去掉了\n没去掉\r,导致每个候选末尾都多一个回车符,跑了一晚上零命中。后来加了strip('\r\n')才解决。这种问题不看repr根本发现不了。
5.2 脚本跑得慢,瓶颈在哪
先定位瓶颈,再谈优化。用time.perf_counter()在关键位置打点,看时间花在读文件、生成候选、还是算哈希上。经验上:
- 如果瓶颈在算哈希,换更快的算法或上多进程。
- 如果瓶颈在生成候选,检查规则是不是套太多了,或者生成器里有没有做重复计算。
- 如果瓶颈在 IO,把词表一次性读进内存(几百万条以内内存扛得住),别每行都读盘。
还有一个隐蔽的坑:在循环里做字符串拼接和格式化。比如每轮都print(f'尝试 {i} 次'),这个开销在百万级循环里非常可观。改成每万次打印一次,速度立刻不一样。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 零命中但确信密码在字典里 | 编码/空白字符问题 | 用repr打印候选,检查 BOM 和换行 |
| 命中位置异常靠后 | 字典排序不合理 | 按常见度重排,高频词前置 |
| 脚本内存暴涨 | 候选一次性载入内存 | 改用生成器,惰性产出 |
| 多进程比单进程还慢 | 进程间通信开销大 | 增大分片粒度,减少通信次数 |
| 掩码跑很久没结果 | 掩码结构猜错 | 先用小范围掩码验证,再扩大 |
| 哈希比对总是不等 | 算法或编码不匹配 | 用已知明文自测校验函数 |
5.4 几个能省大量时间的实操心得
心得一:先用手头已知的密码自测。写脚本之前,拿一个你确定知道明文的哈希跑一遍,确认校验函数、编码、算法全对,再去跑目标。这一步花两分钟,能省掉后面几小时的瞎猜。
心得二:分阶段跑,别一把梭。字典、规则、掩码分三次跑,每次看命中情况和耗时,据此调整下一阶段的策略。一次性全开,出了问题你不知道是哪层出的。
心得三:记录尝试日志。每命中一次或每跑完一个阶段,把尝试次数、耗时、命中候选写进日志文件。这些数据是你优化策略的依据,也是复盘时的证据。
心得四:控制候选集规模。单次运行的候选量控制在百万级以内,超过就分片。百万级在普通机器上几分钟能跑完,体验好、好调试;千万级以上就要考虑并行和进度保存了。
6. 从“能跑”到“跑得聪明”:策略优化的几个方向
6.1 基于目标信息的定向优化
前面说过,与目标相关的词权重最高。如果你知道目标的一些背景信息(仅限自己或授权范围),可以生成一个定制词表:姓名拼音的各种组合、生日数字的排列、常用昵称加年份、手机号后几位等等。这个定制词表往往只有几百条,但命中率可能比通用大字典还高。我做过一个统计,在自建测试集上,定制词表加规则变形的命中率,比纯通用字典高出将近一倍。
6.2 哈希算法的选择影响巨大
不同哈希算法的计算开销差很多。md5 和 sha1 快,sha256 慢一些,bcrypt、scrypt、argon2 这类故意设计得慢的算法,单次计算就要几十到几百毫秒,穷举基本不可行。这也是为什么现代系统都改用慢哈希存密码——它把暴力破解的成本抬到了不划算的高度。所以你在做口令强度评估时,先看目标用的是什么哈希,如果是慢哈希,那结论往往是“现有策略下不可行”,这本身就是有价值的评估结果。
6.3 并行化的正确打开方式
多进程并行能线性提速,但前提是任务切分合理。我的做法是把候选空间按首字符或按区间切块,每个进程负责一块,命中就通过队列回传并通知其他进程停止。注意两点:一是进程数别超过 CPU 核心数太多,否则上下文切换开销吃掉收益;二是共享状态要小心,别让多个进程同时写同一个日志文件,用队列汇总到主进程统一写。
6.4 什么时候该换工具
Python 脚本适合中小规模、需要灵活定制的场景。如果候选量上到十亿级,或者目标哈希是慢哈希,那就该考虑专业工具了——它们支持 GPU 加速、有成熟的规则引擎和分布式方案。工具没有优劣,只有适配。我的判断标准很简单:脚本跑一晚上能出结果,就用脚本;跑一周都悬,就换工具或重新评估可行性。
7. 把这件事做对:边界、伦理与自我约束
技术本身是中性的,但使用场景有明确边界。我在前面反复强调“自己的、授权的”,不是走过场,而是这条技术路线能不能持续讨论的前提。具体来说,几条自我约束我建议你也守住:
- 只碰自己有处置权的目标。自己的加密文件、自己搭建的测试环境、书面授权范围内的系统,除此之外不碰。
- 评估结果用于加固,不用于炫耀。跑出口令强度报告,目的是推动改策略、加长度、上慢哈希,而不是拿“我破了多少个”当谈资。
- 不传播、不交易任何口令数据。测试过程中接触到的任何凭据信息,用完即删,不留存、不外传。
- 遇到边界模糊的情况,先停下来问清楚。授权范围、数据归属、使用目的,任何一项不清楚,就不动手。
这几条不是束缚,是保护。我见过太多人技术很溜,但因为边界没守住,最后连正常的技术讨论都没法进行。把边界守好,这项技能才能长期为你所用。
8. 一个容易被忽略的延伸:口令强度自查清单
既然聊到这儿了,顺手给你一份自查清单,拿去评估自己的密码策略,比任何理论都实用:
| 检查项 | 不合格表现 | 改进方向 |
|---|---|---|
| 长度 | 少于 12 位 | 优先加长度,长度比复杂度更有效 |
| 字符集 | 只用小写字母或纯数字 | 混合大小写、数字、符号 |
| 规律性 | 含生日、姓名、键盘序列 | 避开个人信息和常见模式 |
| 复用 | 多个平台同一密码 | 一平台一密码,用密码管理器 |
| 存储方式 | 明文或快速哈希 | 改用 bcrypt/argon2 等慢哈希 |
| 更新频率 | 多年不换且已泄露 | 泄露后立即更换,平时不必强制轮换 |
这张表的核心逻辑就一句:长度优先、避开规律、慢哈希存储。做到这三点,绝大多数自动化攻击手段都会失效。你写脚本跑一遍自己的密码,如果几分钟就出来了,那说明该改了;如果跑一晚上都没动静,那基本可以放心。
我个人在实际操作中的体会是,写这类脚本最大的收获不是“破”本身,而是通过攻击者的视角重新理解防御。你亲手把候选空间算一遍、把规则跑一遍,才会真正明白为什么“加个感叹号”没用、为什么“长度才是王道”。这种体感,看十篇理论文章都换不来。脚本跑通之后,我建议你把目标换成自己的常用密码,跑一次,看看结果——那个数字比任何安全提示都更有说服力。