☰
Python实现口令恢复与密码强度验证:字典、规则与掩码穷举实战
2026/11/3 20:14:14 网站建设 项目流程

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 字典的构建与排序

字典不是随便找个词表就行,排序比内容更重要。同样一万个词,按常见度从高到低排,平均命中位置可能在前几百;随机排,平均命中位置就奔着几千去了。我通常按这个优先级组织:

  1. 高频弱口令:123456、password、qwerty、admin这类,永远排最前面,因为它们的命中率高得离谱。
  2. 与目标相关的词:如果目标是某个人的加密文件,那姓名拼音、生日、宠物名、常用昵称的权重就要往上提。这一步需要一点信息收集,但仅限你自己或已授权范围。
  3. 通用词表:常见英文单词、中文拼音全拼、键盘序列、年份数字。
  4. 变形产物:上面三类经过规则引擎处理后的结果,动态生成,不预先存盘。

字典文件建议一行一个词,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, tried

mask='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 秒否
掩码穷举dddddd100 万28 秒是(第 48 万次)

这组数据说明两件事。第一,字典和规则对纯随机数字密码基本无效,因为这类密码不在人类习惯的词表里。第二,掩码穷举在结构已知时效率极高,100 万候选半分钟就跑完了。所以实战中,先判断密码类型,再选策略,比无脑堆字典重要得多。

再补一组:目标换成 8 位小写字母的 md5,密码mypasswd。字典里恰好有mypasswd这个词,直查第 3000 多位就命中了,耗时不到 0.1 秒。这就是字典排序的威力——如果这个词排在末尾,耗时就是几十倍。

5. 常见问题与排查技巧实录

5.1 明明密码对,为什么校验不通过

这是最高频的问题,八成出在编码或空白字符上。排查顺序:

  1. 确认目标哈希是用什么编码算的。绝大多数是 UTF-8,但有些老系统用 GBK 或 Latin-1,编码不对,哈希值天差地别。
  2. 确认候选词有没有带首尾空格、换行符、BOM 头。用repr(candidate)打印出来看,不可见字符一目了然。
  3. 确认哈希算法对不对。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 等慢哈希
更新频率多年不换且已泄露泄露后立即更换,平时不必强制轮换

这张表的核心逻辑就一句:长度优先、避开规律、慢哈希存储。做到这三点,绝大多数自动化攻击手段都会失效。你写脚本跑一遍自己的密码,如果几分钟就出来了,那说明该改了;如果跑一晚上都没动静,那基本可以放心。

我个人在实际操作中的体会是,写这类脚本最大的收获不是“破”本身,而是通过攻击者的视角重新理解防御。你亲手把候选空间算一遍、把规则跑一遍,才会真正明白为什么“加个感叹号”没用、为什么“长度才是王道”。这种体感,看十篇理论文章都换不来。脚本跑通之后,我建议你把目标换成自己的常用密码,跑一次,看看结果——那个数字比任何安全提示都更有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询