简介:这是一份面向安全运维、密码安全研究及Python开发学习者的完整项目包,围绕构建基于LSTM与Seq2Seq模型的密码破解分析系统展开,涵盖数据清洗与标准化、模型训练、评估及可视化全流程,适合希望了解机器学习在口令安全领域落地应用的读者。压缩包共74个文件,约711KB,其中60个Python脚本构成核心代码,6个Shell脚本用于一键运行与复现,另有SVG架构图、JSON配置与说明文档,目录按baseline、src、script、figure等模块划分,便于快速定位。目前已有62人浏览学习。读者可从中获得可直接运行的密码破解分析代码、数据集生成与预处理脚本、基于贪心/集束搜索的评估实现,以及模型命中率与召回率对比的可视化绘图模块,对理解口令猜解攻击的建模思路、复现实验和二次开发均有较强参考价值。
1. 当密码猜解进入深度学习时代
做安全审计的人大概率遇到过这种场景:拿到一批合规授权下的泄露密码样本,想分析用户怎么构造密码,结果人工看了几百条就头大。规则匹配只能覆盖password123、qwerty这类弱口令,稍微带点个人信息的变体就漏成筛子。某开发者的做法是换个思路——不再指望正则表达式,而是用 Seq2Seq 模型去学用户名到密码、老密码到新密码的变换规律。这个基于 Python 的密码破解分析系统就是干这件事的:从数据清洗、LSTM 和序列到序列模型训练,到贪婪搜索与集束搜索评估,再到命中率曲线可视化,整条链路全部打通。适合做密码强度评估、安全研究实验,也适合当作深度学习序列生成任务的落地参考。
2. 工程结构拆解:从压缩包布局看懂系统设计
一个源码项目拿到手,先别急着跑。花十分钟把目录过一遍,往往比直接看代码更能理解作者的架构意图,这套系统的目录划分属于典型的「数据层—模型层—评估层—可视化层」四段式。
2.1 根目录布局与模块职责
解压后能看到data、src、code、script、figure、profile这几个核心目录,外加config配置目录和version版本目录。其中data下面有sample_data.py,说明系统支持用脚本自动生成实验用样例数据,不必一上来就找真实数据集,这对跑通流程很友好。
| 目录/文件 | 职责 |
|---|---|
| data | 存放或生成密码样本 CSV,含sample_data.py |
| src/baseline | 基线模型代码,如seq2seq_model.py、tokenizer.py |
| src/code | 训练器与评估器,如pitg_trainer.py、pitg_evaluator.py |
| src/code/targuess | 用户名猜解相关实验,含crack_with_username.py |
| analysis | 用户行为分析脚本,如user_edit_behavior.py |
| figure | 可视化输出,含 RNN 架构图和攻击路径图 |
| script/profile | shell 脚本,批量跑实验用 |
重点提一下structure_analysis.txt和edit_analysis.txt这两个分析产物文件。它们不是代码,而是作者跑完数据统计后落盘的中间结果。前者记录密码结构分布,后者记录编辑距离分析结果。我一般会建议二次开发者保留这种「分析结果落盘」的习惯,因为它让实验过程可追溯,后面调参时能回看每一次数据体检的结论。
2.2 数据读取与预处理链路
数据层是整个系统的地基。user_pwd_reader.py负责从 CSV 里读用户名和密码,dataset_analyze.py做分布统计,tokenizer.py把字符串切成模型能吃的 token 序列。核心逻辑大致长这样:
# tokenizer.py 核心逻辑提炼 import re def build_tokenizer(sequences, max_vocab_size=5000): counter = {} for seq in sequences: for char in seq: counter[char] = counter.get(char, 0) + 1 # 按频率截断词表,低频字符统一映射为 <unk> sorted_items = sorted(counter.items(), key=lambda x: -x[1])[:max_vocab_size] vocab = {char: idx + 4 for idx, (char, _) in enumerate(sorted_items)} vocab['<pad>'] = 0 vocab['<sos>'] = 1 vocab['<eos>'] = 2 vocab['<unk>'] = 3 return vocab def encode(text, vocab, max_len=32): tokens = [vocab.get(c, vocab['<unk>']) for c in text[:max_len]] tokens = [vocab['<sos>']] + tokens + [vocab['<eos>']] return tokens这套 tokenizer 的做法是字符级编码,不是词级。原因很简单:密码没有天然的「词」边界,字符级能捕捉到@、#、数字串这类高频片段,而且词表量级可控。encode函数里强制给序列加上<sos>和<eos>,这是序列生成模型的标准做法——解码器需要知道从哪里开始、在哪里结束。参数max_len控制最长输入,超过部分直接截断,避免个别超长密码拖慢训练。
数据集切分在dataset_analyze.py里做,通常按 8:1:1 划分训练、验证、测试。一个值得注意的细节是:划分前必须打乱样本顺序,因为从泄露源导出的 CSV 往往按网站或时间聚合,不打乱会让同分布的样本扎堆,训练集和验证集的分布偏差会被低估。
2.3 用户名与密码的关联行为建模
username_edit_behavior.py和user_pwd_behavior.py这两个文件很有意思,它们在干一件传统密码工具不常做的事:分析用户怎么把用户名「变形」成密码。常见变形包括首字母大写、追加出生年份、把a替换成@,这些规则本质上就是字符串编辑操作。
# username_edit_behavior.py 核心思路 def extract_edit_ops(username, password): # 把密码拆成前缀、编辑区、后缀 # 例如: username = "alice", password = "Alice123" # 编辑区 = "lice" -> "lice123"? 实际按最长公共子序列对齐 lcs = longest_common_subsequence(username.lower(), password.lower()) ops = [] for i, j in trace_lcs(username.lower(), password.lower()): if username[i] != password[j]: ops.append(('substitute', username[i], password[j])) # 记录追加的后缀 suffix = password[len(lcs):] if suffix.isdigit(): ops.append(('append_digits', suffix)) return ops这类分析代码的产出不是模型参数,而是一份编辑行为统计表:数字后缀长度分布、大小写变换占比、特殊符号替换频率。这份统计对后续模型设计有直接指导意义。我在实际项目里看过类似数据的统计结果,append_digits这一项通常占到用户名相关密码的 40% 以上,后缀集中在 1-4 位数字。这说明用户对「在用户名后面加数字」有极强偏好,是猜解时的优先尝试空间。
3. 核心模型链路:Seq2Seq、PITG 与路径生成
这套系统最核心的部分都堆在src/code和src/baseline里。模型不是单一结构,而是三套递进方案:基线 Seq2Seq、PITG 区域/主机训练、pass2path 路径生成。理解这三者的关系,基本就理解了整个系统的设计脉络。
3.1 基线模型:LSTM 编码器—解码器结构
seq2seq_model.py是整套系统的起点。它解决的任务是:给定一个已破解的密码(或者用户名),生成候选密码。在密码猜解场景里,输入输出都是字符串,天然适合 Seq2Seq。
# seq2seq_model.py 核心结构 import torch import torch.nn as nn class Seq2Seq(nn.Module): def __init__(self, vocab_size, embedding_dim=64, hidden_dim=128, dropout=0.3): super().__init__() self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0) self.encoder = nn.LSTM(embedding_dim, hidden_dim, batch_first=True) self.decoder = nn.LSTM(embedding_dim, hidden_dim, batch_first=True) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, src, trg, teacher_forcing_ratio=0.5): embedded_src = self.embedding(src) # [B, T_src, E] _, hidden = self.encoder(embedded_src) # 编码器输出 embedded_trg = self.embedding(trg) outputs = [] for t in range(trg.size(1)): out, hidden = self.decoder(embedded_trg[:, t:t+1], hidden) pred = self.fc(out) # [B, 1, V] outputs.append(pred) # teacher forcing: 用真实 token 还是预测 token if torch.rand(1).item() < teacher_forcing_ratio: embedded_trg[:, t:t+1] = self.embedding(trg[:, t:t+1]) else: embedded_trg[:, t:t+1] = self.embedding(pred.argmax(dim=-1)) return torch.cat(outputs, dim=1)teacher_forcing_ratio是训练时的关键超参数。置为 1 时模型完全跟着真实序列走,收敛快但生成时容易累积误差;置为 0 时完全用自己的预测,训练慢但更贴近推理场景。这个项目取 0.5 属于折中方案。我一般习惯在训练前期用 0.8,后期衰减到 0.3,效果通常比固定 0.5 更稳定,但这属于经验调参,需要实验验证。
3.2 PITG 模型:从区域训练到主机级联合训练
pitg_model.py是三套方案里最重要的。PITG可以理解为一种面向个人信息的密码猜测模型,它的特点是同时吃进用户名、邮箱等个人信息和已有密码。pitg_region_trainer.py、pitg_trainer.py、pitg_host_trainer.py三个训练器对应了从粗到细的三个训练策略:
- 区域训练:按密码所属的网站/平台分组训练,学某个站的密码风格
- 全局训练:所有数据混在一起训练,学通用密码分布
- 主机级训练:在目标主机对应的数据集上微调,适配特定用户群体
第三个训练器逻辑上最实用。安全评估时如果知道目标系统是某个老牌论坛,用户群体年龄偏大,那直接用通用模型猜并不高效。先用全局数据预训练,再用该论坛的已知泄露密码微调,命中率会有可感知的提升。
# pitg_trainer.py 训练流程骨架 def train_pitg(model, train_loader, optimizer, device, epochs=20): model.train() for epoch in range(epochs): total_loss = 0.0 for batch in train_loader: username, email, password = [x.to(device) for x in batch] optimizer.zero_grad() loss = model.compute_loss(username, email, password) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪 optimizer.step() total_loss += loss.item() print(f"epoch={epoch}, loss={total_loss / len(train_loader):.4f}")梯度裁剪是训练 RNN 类模型的标准操作,密码序列虽然不长,但偶尔会出现个别样本让梯度过大,不裁剪很容易 loss 突然变成 NaN。clip_grad_norm_的阈值一般取 1.0 到 5.0,取值过小会拖慢收敛,过小则起不到保护作用。
3.3 password-to-path:把密码生成变成路径生成
pass2path模块是这套系统里设计最巧妙的部分。它的思路是:不直接生成密码,而是生成一条「从已知密码变换到目标密码的编辑路径」。比如已知密码是admin123,候选密码是Admin@123,路径就是「首字母大写 → 在 3 后面插入 @」。
这种建模方式在学术上有一个直观优势:变换路径比终态字符串更容易学习,因为路径中的每一步都是局部操作,不像字符串整体那样高维。实际表现上,它能更好地处理「已知老密码,预测新密码」的场景,这也是generate_inplace_edits.py和edit_path.py存在的意义。
# edit_path.py 路径生成核心逻辑 def generate_edit_path(src_pwd, tgt_pwd, tokenizer): # 将两个字符串对齐,提取编辑操作序列 # 操作集: keep / delete / insert(char) / substitute(char) ops = align_and_diff(src_pwd, tgt_pwd) path_tokens = [tokenizer.op_to_index(op) for op in ops] # 加入起始符和结束符 return [tokenizer.sos_index] + path_tokens + [tokenizer.eos_index]generate_path.py在推理阶段做的事情是把模型输出的路径 token 序列解码回具体密码。这里有个细节:路径生成模型推理时允许中间状态「非法」,比如对已经删除的字符再执行替换,解码器需要在生成过程中维护一个虚拟字符串状态,遇到非法操作就跳过。这种容错设计很重要,因为集束搜索产生的候选路径未必逐条合法,直接崩溃的话评估流程就断了。
4. 评估与可视化:命中率曲线背后的实验体系
模型训练完只能算完成一半。密码猜解是个「看排名」的任务——不是猜中就行,而是要在尽可能少的猜测次数内命中。这个项目里命名为targuess的相关模块,以及一堆draw_开头的可视化脚本,都是围绕「猜测次数 vs 累计命中率」这个核心指标展开的。
4.1 贪婪搜索与集束搜索的评估差异
pitg_evaluator.py和pass2path_evaluator.py分别实现了两套评估器。评估时模型按概率逐 token 生成候选密码,但生成策略不同,结果差异很大。
贪婪搜索每步只取概率最大的 token,简单快速,但一旦在某个位置选了次优 token,后面全被带偏,没有回头路。集束搜索保留beam_size个当前最优候选,每一步从所有候选的后续扩展里挑 Top-k,搜索空间更大,生成质量更高,但推理时间也成倍增长。
# 集束搜索评估伪代码 def beam_search_decode(model, src, beam_size=5, max_len=32): # 初始候选: [([sos], score=0.0)] candidates = [([sos], 0.0)] for step in range(max_len): all_candidates = [] for seq, score in candidates: if seq[-1] == eos: all_candidates.append((seq, score)) continue logits = model.decode_step(src, seq) top_k = logits.topk(beam_size) for token, log_prob in zip(top_k.indices, top_k.values): new_seq = seq + [token] all_candidates.append((new_seq, score + log_prob.item())) # 保留全局 Top-beam_size,按累计分数排序 candidates = sorted(all_candidates, key=lambda x: -x[1])[:beam_size] return candidates累积分数的做法是取 log 概率相加,防止 beam 搜索偏向短序列——因为每个 token 的概率都小于 1,log 后为负数,序列越长分数越低。不少初学者踩过这个坑,直接用概率相乘会导致长度惩罚失控,长密码几乎不可能出现在 beam 里。
4.2 猜测率曲线:三组对比实验的读法
draw_targuess_guessrate.py、draw_targuess_guessrate_comparison.py、draw_targuess_guessrate_inner.py三套绘图脚本分别对应不同评估维度:单模型评估、跨模型对比、同模型内部不同配置对比。
画出来的图通常是双对数坐标下的阶梯曲线,横轴是猜测次数(1、10、100、1000、1e5),纵轴是累计命中率。读这种图有三个重点。第一,看曲线左端起步高度,代表模型在极少次数下的命中能力。第二,看中段斜率,斜率大说明每增加一个数量级的猜测次数,命中率提升明显。第三,看尾段是否饱和——有的模型天花板低,猜 10 万次和猜 100 万次命中率几乎不变,说明模型容量不够,不是搜索策略的问题。
analyze_cracked_pwds_ri.py和analyze_cracked_pwds_ed.py这两套分析脚本分别从规则命中(rule-based)和编辑距离(edit distance)两个维度对「已破解密码」做归因分析。它能回答一个问题:被模型猜中的密码,到底是和训练集中密码结构高度相似,还是真的靠编辑路径推理出来的?前者说明模型在记忆,后者才说明模型在泛化。
4.3 可视化资产:从 SVG 架构图反推设计
figure目录下的三个 SVG 图值得单独看:rnn_architecture.svg画模型结构,email.svg画邮箱相关特征的处理流程,credential_tweaking_attack.svg画凭证变换攻击的路径示意。我拆项目时有个习惯,先看图再读代码。架构图能帮你建立全局认知,避免钻到代码细节里出不来。
profile目录下的line.sh等脚本是跑批量的实验入口。这类脚本一般是循环调起训练和评估,把不同超参数的结果输出到不同目录。实际项目里这种方式比手动一条条跑命令高效得多,改一个变量就能重跑一组实验。
5. 避坑指南:训练密码模型时最常犯的五个错误
这套系统的代码结构完整,但真要跑起来,有几个坑几乎是必踩的。以下按「现象 → 原因 → 解决」列出我拆解过程中印象最深的五条。
坑一:loss 一直在降,但命中率曲线纹丝不动
现象:训练 loss 从 2.3 降到 0.7,看起来很漂亮,但拿targuess数据集一测,猜 1 万次的命中率还不到 5%。
原因:loss 降不代表模型学到了有效的密码结构。密码数据集类别极不均衡,高频弱口令占了大部分,模型学到「怎么把高频结构生成得又稳又准」,但对长尾的个性化密码毫无办法。训练 loss 主要被高频类别主导,而命中率看重长尾覆盖。
解决:在训练时对低频样本做上采样,或者改用 focal loss 类别的损失函数。同时观察验证集上的命中率曲线,不要只看 loss。我一般会在训练循环里每 5 个 epoch 强制跑一次快速评估,宁可多花几分钟,也要保证指标对齐。
坑二:用户名和密码没对齐,数据泄漏进了验证集
现象:验证集命中率到了 40%,一上真实样本数据立刻掉到 15%。
原因:CSV 文件里用户名和密码的对应关系是同一行。如果划分训练/验证集时按行随机切,同一个用户名可能同时出现在训练集和验证集里。模型在验证集上记住了用户名对应的密码组合,而不是学泛化规律。
解决:按用户名分组后划分。把同一个用户的所有样本放进同一侧,保证验证集里的用户名在训练集中从未出现。dataset_analyze.py里建议显式传入「groupby 字段」,用用户名而不是行索引做切分。
坑三:显存直接爆掉,batch_size 调小也救不回来
现象:训练刚开始就 OOM,batch_size 已经降到 16 还是炸。
原因:密码序列虽然短,但集束搜索在评估阶段会把每个 beam 的不等式展开,如果 beam_size 设到 32 甚至 64,显存占用是指数级增长的。训练没问题,评估跑一半挂掉。
解决:评估时把 beam_size 调小,或者用 batch 维度展开 beam 的逻辑。修改pitg_evaluator.py的推理部分,把「batch 内每个样本展开 beam」改成「循环逐样本评估」。速度慢一些,但显存占用可控,属于典型的以时间换空间。
坑四:matplotlib 画出来的图中文全是方块
现象:跑draw_targuess_guessrate.py生成的图上,汉字标注全部显示为方框。
原因:Linux 服务器上没装中文字体,matplotlib 默认字体不支持中文。这是个几乎每个项目都会遇到的问题,和代码逻辑无关。
解决:检查系统是否装了中文字体,没有就安装一个,在绘图脚本开头加两行字体设置。另外一个更省事的习惯是图内标签全用英文,只有论文或报告里才需要中文标注。
坑五:beam_size 的变化导致命中率曲线不可比
现象:调大 beam_size 后命中率曲线明显上移,但不清楚是模型变强了还是搜索空间变大沾了光。
原因:beam_size 提高本质上是多枚举了候选密码,命中率自然上升。如果对比实验里 A 模型用 beam=5、B 模型用 beam=50,画在同一个图上是逻辑不严谨的。
解决:所有对比实验统一 beam_size,或者同一张图里同时画同一模型的 beam=1 和 beam=5 两条曲线,明确标出「同一模型在不同搜索强度下的表现差异」。
6. 二次开发技巧:把密码分析系统改造成你自己的工具
前面五章拆完了,最后一章聊改造。这个项目的代码组织方式属于「科研原型」风格,不是生产级工程,但正因如此,它的改动空间非常大。
拿到手先做的一件事,是换掉训练数据。sample_data.py生成的样例数据只有千条级别,跑通流程可以,但训出来的模型做不了正经评估。我建议替换成一份 10 万级别以上的密码样本集,注意先跑一遍dataset_analyze.py看结构分布,确认数据质量。密码样本的清洗和普通文本不太一样:要过滤纯空格、超长字符串、非 ASCII 乱码,同时保留大小写和特殊符号——这是密码特征的核心,不能像 NLP 那样一律 lower。
第二个可以做的是加规则引擎前置层。这套系统的纯机器学习模型对强口令生成有天然劣势,但结合规则引擎可以互补。在src/baseline里加一个规则猜解模块,优先尝试用户名+数字后缀、键盘相邻字符替换等高频模式,再把这些命中样本送入模型训练。我的经验是「规则跑量、模型跑长尾」的混合架构比单用模型命中率提高 10 到 15 个百分点。
第三个改动方向是把评估指标做实。现在只有命中率曲线,太单薄。可以加一个按密码长度分层计算的命中率矩阵,看看模型在 8-12 位、含特殊符号的密码上是强是弱。这一步能帮助定位模型盲区,决定下一步给哪些样本加权重。
改完代码后的验证方法也有讲究。先固定一组「验证集密码」,确保和训练数据不同源。然后跑三组实验:规则引擎单独跑、模型单独跑、混合跑。每组实验在相同 beam_size 下对比曲线,确认混合方案确实优于单模型。这个流程走完,你做的改动是否有效就有了量化结论,而不是凭感觉说「效果不错」。
最后多说一句我的血泪经验:改这种科研型项目的代码,千万不要边跑边改模型结构。先原样跑通一次完整流程,把所有中间产物(tokenizer 词表、预训练权重、首次评估曲线)存下来作为基线。后续每次改动只动一个变量,对比要拿同一个基线说话。这套系统涉及的模型种类多、评估脚本也多,改动一旦交叉,出了问题很难定位是哪一环引入的。从那以后我每次接手新项目,都强制走一遍「先跑基线 → 单变量改动 → 对比验证」这个流程,省下的返工时间远超多花的前期成本。以上这些折腾思路如果对你有用,希望帮到你。
本文还有配套的精品资源,点击获取