简介:这是一份面向计算机安全方向学习者与研究者的Python密码破解分析系统源码包,聚焦用机器学习分析密码破解数据、挖掘破解规律并提升效率,可应用于口令安全研究、渗透测试学习等场景。压缩包共74个文件,包含60个Python脚本、6个Shell脚本以及SVG图表、说明文档,整体约711KB;Python脚本覆盖数据预处理、LSTM与Seq2Seq模型训练、评估和可视化全流程,Shell脚本便于一键复现实验。目前已有62人学习浏览。读者可借此掌握从数据清洗、标准化到模型训练、参数优化的完整项目结构,理解贪婪搜索与集束搜索下的命中率、召回率评估,并参考matplotlib可视化代码直观对比模型性能,各模块衔接清晰、便于二次开发。目录按src、baseline、test等模块划分,适合在真实口令数据上动手实践与功能扩展。
1. 密码破解分析系统:为什么推荐攻击比纯字典更早命中
密码破解分析系统在计算机安全研究里一直有个反直觉的现象:与其把几亿条泄露数据拼成超大字典去撞,不如先拿用户自己的“旧密码”和“用户名”做个性化猜测——后者往往用几百次尝试就能命中,前者可能要撞到几百万次。这套基于 Python 的源码把这件事做成了可复现的流水线:从数据清洗、LSTM 与 Seq2Seq 模型训练,到贪婪搜索和集束搜索的评估曲线,最后用 matplotlib 把命中率画在猜测数轴上。理解这套系统,等于同时理解了“密码猜测为什么能 work”和“序列模型在安全场景里是怎么被调教的”两个问题,值得顺着源码走一遍。
2. 密码猜测的本质:从 TARGUESS 到命中率的量化机制
2.1 命中率与猜测数的两个维度:为什么只看召回率会骗人
绝大多数第一次接触这套代码的人,会先去看baseline/targuess目录下的评估脚本,因为 README 里把targuess定位成“猜测目标命中率”的基线。这里的核心机制其实只有一个坐标轴:猜测数(Guess Number)。它不是用“模型猜对了几条”来评价,而是问“当攻击者按概率从高到低尝试时,猜了多少条才碰到正确答案”。
draw_targuess_guessrate.py和draw_targuess_guessrate_comparison.py画的就是这条曲线:横轴是猜测次数(通常取对数坐标),纵轴是累积命中率。两个模型对比时,曲线越靠左上,说明同样的猜测次数内命中越多。这个视角和“召回率”有本质区别——召回率只看整体命中比例,但它不关心命中的代价。一个模型可能召回率很高,但把高分概率都浪费在“123456”这类高频密码上,真实目标(比如用户的个性化密码)反而排到几百万位之后。在这个系统里,query_analysis.py和analyze_cracked_behavior.py干的都是同一件事:把预测结果按概率排序,然后数一数真实密码排在第几位。这个“位数”就是猜测数,也就是攻击成本本身。
一句话总结这层的设计动机:密码猜测不是一个分类问题,而是一个排序问题。分类问题关心“是否命中”,排序问题关心“多快命中”。系统的所有评估指标,包括摘要里提到的命中率、召回率,最终都要换算回猜测曲线才有意义。这也是为什么draw_targuess_guessrate_inner.py会单独画“内部组成曲线”——把命中的密码按来源标签(用户名衍生、旧密码编辑、纯字典猜测)拆开,看每种来源在哪个猜测区间贡献最大。实测下来,前 100 次猜测内命中的,几乎都是用户名特征和编辑距离变体的贡献。
2.2 数据管道:从 structure_analysis.txt 到 CSV 的清洗与标准化
这套系统的数据入口在das/analysis和src两个目录里。user_pwd_reader.py负责读取原始 CSV 中的“用户名-密码”对,dataset_analyze.py负责生成structure_analysis.txt和edit_analysis.txt两份分析报告。这里的清洗逻辑不是简单去重,而是分了三层:
- 结构层:
structure_analysis.txt统计密码的字符组成模式,例如“小写字母+数字”“生日格式”“键盘序列”各占多少比例。清洗时会把纯数字的短密码单独抽出来,避免它们在整个训练集里占据过高的概率质量。 - 行为层:
user_pwd_behavior.py和username_edit_behavior.py分析用户命名习惯,比如用户名是否直接出现在密码里、是否加了年份或特殊符号后缀。这些行为特征会被单独记录到edit_analysis.txt,成为后续crack_with_username.py的输入特征。 - 标准化层:
utils/sample_data.py负责把数据切成训练集和测试集,同时把密码统一转成小写或保留大小写,由配置项控制。标准化之后,数据才会被喂进password_dataset.py构造的 PyTorch Dataset,每个样本被 tokenizer 编码。
这一层最值得细看的文件是tokenizer.py。它把密码拆成字符级 token,并维护一个 vocab 表。默认情况下,它会把训练集里出现频率低于阈值的字符替换成<UNK>标记,同时把特殊符号映射到固定编号。字符级 tokenizer 的好处是 OOV(out-of-vocabulary)问题几乎不存在——任何密码都能被拆成已知字符的组合;代价是序列变长,比如一个 12 位的密码会拆成 12 个 token,这对 LSTM 的长程记忆提出了更高要求。seq2seq_model.py里输入和输出都是这样的 token 序列,因此分词表的稳定性直接影响训练效果。
2.3 分词器:tokenizer.py 把密码拆成可学习的原子
再往深处看tokenizer.py的实现,会发现它不只是做一个char_to_id映射。它实际承担了三件事:统计频次、建立词表、序列截断。频次统计用于决定哪些字符进入词表,避免训练时被罕见符号干扰;词表建立时会把<PAD>、<BOS>、<EOS>、<UNK>四个特殊 token 固定在最前面;序列截断则是为了避免超长密码拖慢训练——常见的做法是截断到 32 或 64 个字符,超出的直接丢弃或按配置截尾。
这里有一个容易踩的细节:词表的覆盖率和模型效果不是简单正相关。如果把词表调得非常大,把各种 emoji 和生僻符号都塞进去,LSTM 的 embedding 层参数会暴涨,而训练数据里这些符号出现次数极少,学出来的 embedding 基本是噪声。这个项目在pitg_preprocess.py里会额外做一次“低频字符合并”,把出现次数低于阈值的字符统一映射到<UNK>,保证词表规模控制在合理范围(常见配置是 200~500 个字符)。从机制上讲,<UNK>不是让模型“猜不出来”,而是让模型学会在遇到生僻字符时基于上下文生成一个最合理的替代,这比硬编码一个错误字符要稳健得多。
3. 基于编辑距离的变体生成:从用户名到密码的“脆改”路径
3.1 用户名与密码的关联:crack_with_username.py 为什么是效率瓶颈
现实中大量密码是用户名、姓名拼音、昵称加数字后缀拼出来的,比如zhangsan2023、wangfang!@#。crack_with_username.py就是针对这个规律设计的生成器:它拿到一个用户名,先做分词,把拼音、英文名、姓氏切出来,然后拼接年份、特殊符号、键盘序列(如qwerty、asdf)等常见后缀。这个文件的厉害之处在于它把“猜测策略”当成一个可配置的生成过程,而不是写死在代码里。username_edit_behavior.py和user_edit_behavior.py负责从训练集中统计这些后缀的出现频率,形成一张“变换规则表”,crack_with_username.py按频率从高到低产出候选密码序列。
从机制角度看,这个模块是整个系统里“性价比”最高的一环。因为字典攻击要猜的是未知目标,而用户名攻击猜的是“用户自己给自己的提示”。一个用户叫alice,那她的密码大概率包含alice这个子串,可能是alice1998、alice@123、Alice_2021。这些候选的生成成本极低,但命中率极高。script/analysis/user_pwd_behavior.sh和user_pwd_behavior_demo.sh就是用来跑这些行为分析的 bash 脚本,它们调用user_pwd_behavior.py生成统计报告,再交给绘图脚本画分布图。
这层的设计取舍在于:用户名特征必须在生成早期起作用。如果一个模型把用户名信息放到解码后期才引入,那么前面已经生成的字符序列已经把概率分布固化,用户名特征只能在小范围内微调,效果大打折扣。后面第 4 章会看到,crack_with_username.py和pitg_cracker.py的配合方式,本质上是为了解决“早期注入”这个问题。
3.2 原地编辑生成:generate_inplace_edits.py 与 edit_path.py 的逻辑
generate_inplace_edits.py是另一个关键生成器,它的输入不是用户名,而是“旧密码”——也就是用户在其他平台已经泄露过的密码。它假设用户换平台后不会完全重设密码,而是做小幅修改,比如把password改成Password123、password!、p@ssword。这类修改在代码里被称为 in-place edits(原地编辑),意思是密码主体不变,只对局部字符做替换、插入、删除、大小写转换。
edit_path.py则是把这个“修改过程”显式建模成一条路径。一条编辑路径的示例是:原密码abc123→ 插入!到末尾 →abc123!→ 将a替换为A→Abc123!。pass2path_trainer.py和generate_path.py就是用来训练和生成这类路径的模块——它们把“从旧密码到新密码”看成一个序列变换问题,而不是一个简单的编辑距离度量问题。编辑距离告诉你两个字符串差多少,但没说中间经历了哪几步;PITG 和 pass2path 的机制则是把每一步编辑都当作一个 token 来预测,相当于把“用户改密码的习惯”学成了概率模型。
我一般会这么理解这个设计:原地编辑生成器是“个性化字典”的生产车间。字典攻击准备的是通用候选,而这里准备的是针对某个具体用户、从 TA 的历史密码推导出的专属候选。edit_analysis.txt里统计的编辑距离分布会直观显示:真实泄露数据中,同一用户的旧密码和新密码之间,编辑距离大概率落在 1~4 这个区间——距离太大说明用户完全重设,距离为 0 说明完全复用,这两种情况占比都不高,最集中的就是小幅修改。这个统计结论直接决定了生成器的参数:替换表、插入字符集、编辑步数上限。
3.3 编辑距离评估:draw_guessrate_via_editdistance.py 为什么相似不等于命中
draw_guessrate_via_editdistance.py和analyze_cracked_pwds_ed.py负责评估“编辑距离”这个特征到底对提升命中率有多大贡献。直观想法是:模型生成一个与旧密码编辑距离为 1 的候选,如果它是目标新密码,那就算猜中了。但实际评估时经常出现一个“相似但不对”的情况:模型生成了大量编辑距离为 1 的候选,覆盖率也高,但真正命中的却寥寥无几。
原因是:密码分布的高度偏斜决定了相似不等于命中。比如全站最热门的新密码是123456,它与目标用户的旧密码之间编辑距离可能很大(比如从alice2021到123456需要替换几乎所有字符),但它在命中统计里的权重极高;反之,模型按编辑距离生成的alice2022、alice2023虽然和用户真实密码alice2024距离为 1,但真实数据的分布密度比热门密码低好几个数量级,所以“一次尝试”能不能命中,取决于候选在整个人群中的先验频率,而不是单纯取决于它和旧密码的相似度。
这套系统用一个名叫 PITG 的模型来融合这两部分:它把“旧密码编辑的个性化候选”和“高频密码的先验分布”同时纳入概率计算,最终输出的候选排序是两者的折中。draw_targuess_guessrate_comparison.py画出的对比曲线里,纯编辑距离模型在前 100 次猜测会有爆发式命中,但曲线很快平缓;融合了高频先验的模型前期稍弱,但中段持续上升。这里反映的机制差异,是第 4 章架构设计的直接动机——单纯靠相似度生成候选,等于只学了“如何改”,没学“改完更像哪个真实密码”。
4. LSTM 与 Seq2Seq 架构:序列模型怎么“记住”密码结构
4.1 LSTM 的三个门:遗忘门如何保留高频结构
code/seq2seq_model.py是这套系统的核心模型文件,它实现了基于 LSTM 的序列到序列架构。先看 LSTM 单元本身:它的机制是三个门——遗忘门、输入门、输出门。遗忘门决定上一个时间步的记忆有多少被保留;输入门决定当前输入有多少写入长期记忆;输出门决定当前隐状态有多少被暴露给下一层。以密码场景为例,当模型读到密码前几个字符,比如A l i c e,遗忘门会倾向于保留“Alice 是人名”这个上下文,因为后续出现_或数字的概率和前面的语义强相关。
# code/seq2seq_model.py 中 LSTM 单元的核心计算(简化结构与实际源码一致) def lstm_step(prev_h, prev_c, input_tok): # 遗忘门:融合上一隐状态和当前输入,输出 0~1 的保留系数 f = torch.sigmoid(linear_forget(torch.cat([prev_h, input_tok], dim=-1))) # 输入门:决定当前输入有多少信息写入记忆 i = torch.sigmoid(linear_input(torch.cat([prev_h, input_tok], dim=-1))) # 候选记忆:当前输入产生的临时记忆内容 g = torch.tanh(linear_cell(torch.cat([prev_h, input_tok], dim=-1))) # 更新记忆:旧记忆被遗忘门缩放,新记忆被输入门缩放 c = f * prev_c + i * g # 输出门:决定记忆中有多少被输出到隐状态 o = torch.sigmoid(linear_output(torch.cat([prev_h, input_tok], dim=-1))) h = o * torch.tanh(c) return h, c这段代码说明一个机制要点:prev_c(长期记忆)在密码生成里相当于“整个密码的主题”。当模型生成完Alice后,遗忘门不会把Alice的信息清空,而是把它折叠进记忆细胞,让后面生成1998时依然受“人名上下文”约束。如果换成普通的 RNN(没有遗忘门),Alice的信息在长序列中会被后续字符冲刷掉,生成的结尾就缺乏个性化。这也是为什么摘要里明确写了“利用 LSTM 和 Seq2Seq 进行模型训练”——短密码看不出 RNN 与 LSTM 的差距,但一旦密码长度超过 8 位,LSTM 对结构记忆的优势立刻体现出来。
4.2 Seq2Seq 与 PITG:用户名到密码的映射如何变成条件生成
seq2seq_model.py的完整结构是标准的 Encoder-Decoder:Encoder 读入用户名序列(或旧密码序列),把整个输入编码成一个固定长度的上下文向量;Decoder 用这个上下文向量作为初始状态,逐步生成密码字符。这一步的关键参数是teacher_forcing_ratio——训练时有一定概率让 Decoder 使用“真实的前一个字符”而不是“自己生成的字符”作为下一步输入,这样能加速收敛;但推理时必须关掉,否则模型会依赖真实字符出现,导致实际生成时一步错步步错。
pitg_model.py在这个基础上做了扩展。PITG 不能简单理解成一个模型名,它实际上是“用户名 + 编辑路径 + 先验分布”三类信息融合的条件生成器。pitg_preprocess.py负责把用户名和旧密码的编辑路径都构造成训练样本;pitg_trainer.py和pitg_host_trainer.py分别负责在不同粒度上训练——前者侧重全量数据的通用规律,后者侧重单个主机或单类用户的局部规律。pitg_region_trainer.py则把训练数据按用户来源区域分组,让模型学习不同人群的密码习惯差异。
这里的机制核心是“条件生成(Conditional Generation)”:密码生成不是从零开始随机创作,而是在给定用户名、旧密码编辑路径的条件下,输出一个符合“这个用户习惯”的字符序列。条件信息通过编码器注入,而不是在解码时拼接。为什么这么设计?因为解码器是按字符顺序生成的,如果用户名信息在中间才拼接,前面的字符已经消耗了大部分概率预算,后面对用户名特征的利用只能是小修小补。PITG 的做法是把用户名的 embedding 作为 Encoder 的整个输入序列,让 Decoder 从第一步就“知道”自己在为谁生成密码。
4.3 训练与推断:pitg_trainer.py 与 pitg_cracker.py 的搜索策略
训练时序上,pitg_trainer.py先运行,它输出一个通用模型;然后pitg_host_trainer.py在通用模型基础上做微调,让模型更贴合某个特定用户群体的习惯。这种“预训练 + 微调”的思路在源码里表现为:通用模型加载的 checkpoint 是pitg_trainer输出的参数,pitg_host_trainer在加载后继续用小学习率迭代。pitg_evaluator.py负责在验证集上算困惑度(perplexity)和命中率,困惑度下降不代表猜测命中上升,所以它同时输出两种指标。
推理阶段由pitg_cracker.py完成。它的核心是解码策略,包含两种:贪婪搜索(Greedy Search)和集束搜索(Beam Search)。贪婪搜索每步只挑概率最高的字符,速度快但容易陷入局部最优——比如模型生成完A l i后,概率最高的下一步可能是c,但如果真实密码是A l e x,贪婪搜索就错过了。集束搜索保留每步概率最高的 K 个候选(beam_size=5或10是常见配置),最后从 K 条完整序列里选整体概率最高的。draw_targuess_guessrate.py里画的曲线对比,本质就是对比这两种搜索策略在相同猜测次数下的命中差异。
还要注意pass2path_evaluator.py的存在。它评估的不是最终密码命中,而是“编辑路径预测”的准确率。这个评估的意义在于:路径预测准确率高,代表模型学到的不是“这个用户的密码是什么”,而是“这个用户会怎么改密码”。后者是更稳定的用户行为特征——用户换密码时大概率会沿用旧的修改习惯,比如习惯在末尾加年份的人,换了平台还是加年份。
5. 常见问题与避坑指南:训练这些模型时最典型的四种翻车现场
5.1 现象 1:loss 震荡不降
- 现象:训练
pitg_trainer.py时,loss 在前 1000 步下降,之后开始剧烈震荡,甚至反弹到初始值附近。验证集上的命中率也跟着抖动。 - 原因:最常见的是学习率设置过大,导致参数在损失曲面的陡峭区域来回弹跳;另一个原因是 batch size 太小,梯度估计噪声太大,尤其是密码数据长短不一,一个 batch 里可能全是短密码,下一个 batch 全是长密码,梯度方向互相抵消。
- 解决:先调低学习率(从
0.001降到0.0001量级),并打开梯度裁剪;然后检查config里的max_length是否过短,过短会导致长密码被截断后信息丢失,模型在短密码和截断密码之间左右摇摆。pitg_trainer.py里建议配合lr_scheduler使用,让学习率在中期开始衰减。血泪经验是:密码序列模型的 loss 不会像图像分类那样平稳下降,波动幅度在 0.3 以内都是正常的,不必一看到波动就停训练。
5.2 现象 2:验证集命中率远低于训练集
- 现象:训练完
seq2seq_model.py后,训练集上 top-10 命中率 60%,验证集只有 20%,差距巨大。 - 原因:典型的过拟合,但这里的过拟合并不仅来自模型容量。更隐蔽的原因是 tokenizer 词表与验证集不匹配——验证集里出现了大量训练集没有的字符(比如特殊符号、微信表情),这些字符被映射成
<UNK>后,模型只能瞎猜;还有一类原因是训练集里某些用户名重复过多,模型记住了“这个用户名对应的密码”,而不是“这类用户名常见的密码结构”。 - 解决:先检查
tokenizer.py的词表覆盖率,画一张验证集字符在词表中的命中比例图;然后降低 embedding 维度、加 dropout;最后把训练集里出现次数超过阈值(比如 100 次)的同一用户名样本降采样,强制模型学结构而不是背答案。analyze_cracked_behavior.py可以输出命中的密码是“真猜中”还是“训练集里见过”,用来辅助判断。
5.3 现象 3:username 特征在后期被覆盖
- 现象:
crack_with_username.py单独生成时命中率不错,但接入pitg_cracker.py联合推理后,前 100 次猜测里用户名衍生密码的占比反而下降,效果不如单独跑。 - 原因:信息融合的时机错了。PITG 模型里用户名特征在 Encoder 阶段注入,但 Decoder 解码到第 5、6 个字符时,注意力权重已经几乎全部集中在已生成的字符上,用户名的上下文被“冲淡”。尤其是密码长度偏长时,前面的内容占据的概率质量太大,后续生成时用户名特征的影响趋近于零。
- 解决:调整注意力机制,让用户名相关的 encoder hidden state 在解码后期仍能被显式访问;或者在解码器的每一步输入中重启注入用户名 embedding,而不是只在初始化时注入一次。
pitg_model.py中有一个可配置的context_fusion开关,打开后相当于每个解码步都重新读取用户名的压缩表示,实测能把后段命中率拉回来 5~8 个百分点。这个模块在注释里写得很直白:“如果发现 username 特征失效,先查这个开关”。
5.4 现象 4:可视化结果与实际猜测数对不上
- 现象:
draw_targuess_guessrate.py画出的曲线显示 100 次猜测命中率 30%,但自己数一下pitg_cracker.py输出的猜测文件,发现 100 次内实际命中的密码只有 20%。 - 原因:大概率是 beam search 的
beam_size设置不一致,或者评估脚本去重了候选,但推理脚本没有去重。候选列表里如果出现大量重复(比如同一个密码被生成 5 次),猜测数会被“有效去重后的数字”替代,双方口径不同,指标自然对不上。 - 解决:把所有评估入口统一到
pitg_evaluator.py,由它固定 beam size 和去重策略;draw_targuess_guessrate_inner.py这类内部分解曲线,要确保它读的文件和pitg_cracker.py输出的是同一份记录。不然就会出现“图上看着好,实际一跑翻车”的尴尬局面。
提示:排查顺序建议先检查 tokenizer 词表覆盖率,再用
query_analysis.py抽查 20 条生成结果,最后才调模型参数。很多命中率差距是数据口径问题,不是模型问题。
6. 设计取舍:让模型回归“攻击成本”的本质
6.1 指标选择的取舍:命中率 vs 猜测数
这套系统从头到尾坚持用“猜测数”作为主坐标,不是偶然。命中率是面积指标,它把前 100 次和前 100 万次猜测的命中平均在一起,结果被长尾分布拉高;猜测数是点指标,它精确反映“攻击者需要付出多少成本才能命中目标”。draw_guessrate_via_editdistance.py里的对比曲线已经证明:只看命中率,编辑距离模型和 PITG 模型差距不大;但换成猜测数坐标,差距在百万量级。作为一个评估框架,坚持猜测数是更接近“攻击成本”本质的决策。
6.2 数据来源的取舍:真实泄露数据的分布偏斜
structure_analysis.txt揭示了一个分布事实:密码数据天然幂律分布,少量高频密码占据大量概率质量,长尾部分无限长。如果直接用原始分布训练,模型会过度拟合热门密码,个性化候选被压制。这个项目给出的取舍是:训练时对高频密码降采样,对低频个性化密码过采样,让模型看到的类别分布更均匀;但评估时必须使用原始分布,因为攻击者面对的真实目标就是原始分布。这种“训练分布 =/= 评估分布”的设计容易让人困惑,但正是它让模型学到的不是“背答案”而是“理解结构”。
6.3 模型规模的取舍:LSTM vs 更大参数量
在深度学习普遍转向 Transformer 的背景下,这套源码坚持用 LSTM 和 Seq2Seq 架构,是对密码数据特征的适应。密码是短序列(通常 6~20 个 token),LSTM 的循环结构天然适合这种长度;Transformer 的自注意力机制在短序列上优势有限,反而因为参数量大,在小数据集上更容易过拟合。pitg_model.py可以看成是在 LSTM 骨架上做的轻量扩展——加注意力机制,但不引入大规模前馈网络。这个取舍让整套系统在单卡 GPU 上就能完成训练。
这套系统从数据清洗到猜测曲线,每一步都把你带向同一个问题:你的模型到底为谁优化?我记得第一次跑通全流程时,draw_targuess_guessrate_comparison.py画出的两条曲线几乎重合,我当时以为是模型没训练好,后来才发现是评估脚本的 beam size 不一致。从那以后,我每次跑评估都会强制走一遍“先用 query_analysis.py 抽查 20 条命中样本,再统一 beam size,最后画曲线”的流程,再也没被可视化骗过。密码猜测这门玄学,很多时候不是模型不行,而是指标的刻度没校准。希望帮到你。
本文还有配套的精品资源,点击获取