简介:一套基于 Python 语言编写的密码破解分析系统源码,面向计算机安全方向的研究者与机器学习爱好者,聚焦如何借助深度学习模型剖析密码破解数据、发现解密规律并提升破解效率。压缩包内共 74 个文件,以 60 个 Python 脚本为主,同时包含 6 个可执行 shell 脚本、3 张 SVG 架构示意图、2 份文本分析记录、2 个 JSON 配置以及 1 个说明文档,整体仅 711 KB,目录划分清晰,便于按模块查阅。系统实现了数据清洗、分割与标准化,并集成 LSTM、Seq2Seq 两种常用序列模型完成训练;通过贪婪搜索和集束搜索算法计算命中率、召回率等指标,再借助 matplotlib 进行可视化对比。另包含用户编辑行为统计、猜测率变化曲线、密码相似度分析等脚本,可辅助深入理解密码破解的攻击特征。已有 62 人学习下载,适合有一定 Python 与 PyTorch 基础、希望将序列建模应用到安全分析场景的中高级学习者,通过源码可快速复现实验并扩展算法调优。
1. 密码破解分析系统:把弱口令的“被命中路径”变成可复现实验
某公司做内部弱口令自查时,安全组拿到一批脱敏的账号和密码样本,想回答一个很实际的问题:如果站在攻击者视角,用户的密码会不会在“猜前 1000 次”之内被命中?人工排序不现实,靠经验拍脑袋又不服众。这个基于 Python 的密码破解分析系统,核心价值就是把“密码被猜中”拆成可复现的实验:先做数据清洗和抽样,再用 LSTM、Seq2Seq、PITG 模型把密码变换建模成序列生成任务,通过贪婪搜索和 Beam Search 算出命中率与召回率,最后用 matplotlib 画成 guessrate 对比曲线。它适合做口令安全评估、风控策略验证,也适合想理解序列生成模型在真实数据集上怎么落地的后端或算法开发者。注意,这套代码是安全研究用的分析框架,不是让你拿真实目标测试攻击工具。
2. 目录结构梳理:从原始数据到模型训练,代码到底怎么摆放
2.1 顶层目录划分
我拿到压缩包后第一件事就是看目录。整个工程不是单脚本堆砌,而是按“数据读入 → 行为分析 → 模型训练 → 评估可视化 → 脚本编排”分层的。顶层大致长这样:
项目根目录/ ├── README.md ├── src/ │ ├── baseline/ │ │ └── targuess/ │ ├── das/ │ └── analysis/ │ ├── user_pwd_behavior.py │ ├── username_edit_behavior.py │ ├── user_edit_behavior.py │ ├── user_pwd_reader.py │ ├── user_edit_reader.py │ ├── dataset_analyze.py │ ├── structure_analysis.txt │ └── edit_analysis.txt ├── code/ │ ├── tokenizer.py │ ├── password_dataset.py │ ├── seq2seq_model.py │ ├── pitg_model.py │ ├── pitg_trainer.py │ ├── pitg_evaluator.py │ ├── pitg_cracker.py │ ├── pitg_region_trainer.py │ ├── pitg_host_trainer.py │ ├── pass2path_trainer.py │ ├── pass2path_evaluator.py │ ├── generate_path.py │ ├── generate_inplace_edits.py │ ├── edit_path.py │ ├── crack_with_username.py │ └── version/ ├── script/ │ ├── user_pwd_behavior.sh │ ├── user_pwd_behavior_demo.sh │ ├── user_behavior.sh │ ├── user_pwd_behavior2.sh │ └── user_behavior2.sh ├── profile/ └── figure/ ├── rnn_architecture.svg ├── email.svg └── credential_tweaking_attack.svg从结构能读出设计意图。src/analysis/放的是人对密码和用户名的行为分析脚本,目标是产出一批结构化统计文件,比如structure_analysis.txt、edit_analysis.txt。src/baseline/targuess/是统计基线,用来证明后面训练的模型比规则式猜测强多少。src/das/和analysis高度耦合,主要是分析模块的数据访问辅助,老代码里经常能看到两者互相 import。
真正的模型主战场在code/。tokenizer、dataset、seq2seq、pitg、pass2path 全在这里,version/保存历史 checkpoint 和冻结版本,方便回归对比。script/是 shell 编排,把分析和训练串起来保证换机器还能一键复现。figure/放输出图,profile/是运行环境相关的 profile 配置。
我习惯先把script/下的.sh读一遍,因为它们才是最接近“怎么运行”的答案。比如user_pwd_behavior.sh会依次调用dataset_analyze.py、user_pwd_behavior.py,再用user_behavior.sh汇总画图。只要 shell 脚本没坏,整个流程就能走通。
2.2 数据读入与抽样:这两个文件决定了实验能不能复现
src/analysis/user_pwd_reader.py是数据入口。它负责把不同格式的 CSV 统一成user和pwd两列。我看到的实现是按列名映射,缺失行直接按空字符串过滤。
import pandas as pd COL_MAP = { 'username': 'user', 'user': 'user', 'name': 'user', 'account': 'user', 'password': 'pwd', 'pass': 'pwd', 'pwd': 'pwd' } def read_user_pwd(path: str) -> pd.DataFrame: df = pd.read_csv(path, keep_default_na=False) df = df.rename(columns=COL_MAP) df = df[['user', 'pwd']] df = df[(df['user'].str.len() > 0) & (df['pwd'].str.len() > 0)] df = df.drop_duplicates(subset=['user', 'pwd']) return df.reset_index(drop=True)这段代码解决的是脏数据问题。参数说明:keep_default_na=False会把 CSV 里的空字符串保留下来而不是转成 NaN,便于后面按字符串长度过滤;drop_duplicates(subset=['user', 'pwd'])按“用户 + 密码”去重,避免同一个口令被统计两次。实际跑的时候,列名可能是account + passwd或phone + password,所以映射表必须能扩展。
抽样逻辑在src/utils/sample_data.py。我发现压缩包里专门放一个sample_data.py,说明作者对这种长尾分布考虑得很清楚:如果不限制每个用户最多保留多少密码,高频用户的习惯会在训练集里失衡,模型会偏向“人数多但习惯单一”的用户群体。
import random import pandas as pd def sample_by_user(df: pd.DataFrame, max_per_user: int = 20, seed: int = 42) -> pd.DataFrame: rng = random.Random(seed) outputs = [] for user, group in df.groupby('user', sort=False): if len(group) <= max_per_user: outputs.append(group) else: outputs.append(group.sample(max_per_user, random_state=rng)) return pd.concat(outputs, ignore_index=True)这里的关键参数是max_per_user:设太小,长尾用户学不到;设太大,高频用户主导 loss。我一般会先用dataset_analyze.py看每个用户的密码数量分布,再决定这个阈值。seed是复现命脉,跑实验之前先固定住,否则第二天改代码后结果对不上,很容易误判模型改进点。
2.3 行为分析:编辑距离、用户名编辑和结构特征
src/analysis/里有一批行为分析脚本,user_edit_behavior.py和username_edit_behavior.py分别研究“密码到密码的编辑”和“用户名到密码的编辑”。它们输出的edit_analysis.txt和structure_analysis.txt不是给人看的报表,而是后面模型挑选训练样本的依据。
比如structure_analysis.txt会统计每个密码的长度、字符种类、是否包含数字段、是否出现键盘连续序列。edit_analysis.txt则计算编辑操作分布:替换、插入、删除、大小写变换各自占多少比例。这些统计结果会直接告诉我们,PITG 模型的编辑路径到底应该侧重哪类操作。
user_edit_reader.py和user_pwd_reader.py的区别也在这里:前者读的是“用户名 + 密码”结构,后者读的是“用户 id + 密码”。前者更贴近真实攻击视角,因为攻击者通常会拿用户名做种子。这也是后面crack_with_username.py存在的意义。
3. 核心模型拆解:Seq2Seq、PITG 与 pass2path 的生成链路
3.1 字符级 tokenizer 和数据集的坑
code/tokenizer.py是所有模型的第一关。它只做字符级 token,不切词。密码是由数字、字母、符号组成的无空格字符串,词级 tokenizer 在密码场景基本不适用,字符级才是常见做法。
class CharTokenizer: def __init__(self, vocab: str): self.vocab = vocab self.stoi = {c: i for i, c in enumerate(vocab)} self.itos = {i: c for i, c in enumerate(vocab)} self.pad_idx = self.stoi['<pad>'] self.sos_idx = self.stoi['<sos>'] self.eos_idx = self.stoi['<eos>'] def encode(self, seq: str): ids = [self.sos_idx] for c in seq: if c in self.stoi: ids.append(self.stoi[c]) ids.append(self.eos_idx) return ids def decode(self, ids): chars = [self.itos[i] for i in ids] if self.eos_idx in chars: chars = chars[:chars.index(self.eos_idx)] return ''.join(c for c in chars if c != '<sos>')参数说明:vocab必须覆盖训练集里所有字符,否则推理时遇到out-of-vocab字符就会崩,或者在预处理阶段被静默丢掉。这里用 skip 而不是 unk,是因为密码里低频符号丢掉影响较小,但<pad>、<sos>、<eos>三个控制符必须保留且索引不能乱。
数据集实现是code/password_dataset.py,它负责把变长的 token id 补成定长 tensor。
from torch.utils.data import Dataset import torch class PasswordDataset(Dataset): def __init__(self, src_pwds, tgt_pwds, tokenizer, max_len=32): self.src_ids = [tokenizer.encode(p)[:max_len] for p in src_pwds] self.tgt_ids = [tokenizer.encode(p)[:max_len + 1] for p in tgt_pwds] def __len__(self): return len(self.src_ids) def __getitem__(self, idx): src = torch.tensor(self.src_ids[idx], dtype=torch.long) tgt = torch.tensor(self.tgt_ids[idx], dtype=torch.long) return src, tgt这段代码的坑在于:max_len截断发生在 tokenizer 输出之后,如果某个密码本来有 40 个字符,截断后<eos>会被切掉,训练时模型永远学不到终止符。我一般会在截断之后手动补<eos>,或者把max_len设到密码长度的 90 分位以上,宁可用显存换稳定性。
3.2 Seq2Seq 模型:LSTM 编码器解码器
code/seq2seq_model.py提供了序列生成的主干。编码器把源密码编码成隐状态,解码器逐个 token 输出目标密码。核心实现简化后长这样。
import torch import torch.nn as nn class Seq2Seq(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2, dropout=0.2): super().__init__() self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.encoder = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) self.decoder = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, src, tgt): src_emb = self.embed(src) enc_out, (h, c) = self.encoder(src_emb) tgt_emb = self.embed(tgt) dec_out, _ = self.decoder(tgt_emb, (h, c)) logits = self.fc(dec_out) return logits逻辑说明:这里把编码器最终隐状态(h, c)直接传给解码器,源密码信息从解码第一步开始参与生成。batch_first=True表示输入形状是(batch, seq_len, embed_dim),PyTorch LSTM 默认是seq_len在第一位,漏掉这个参数会导致维度错乱。padding_idx=0让<pad>在 embedding 层梯度恒为 0,避免补齐 token 污染训练。
参数说明:hidden_dim=256对密码这种长度不超过 20 的序列够用;embed_dim一般 128 起步,太小会让相似拼音、键盘序列之间没有可分性,太大在小数据集上过拟合。训练时如果用 teacher forcing,目标侧输入是真实目标密码而不是上一步预测,收敛快很多,但推理时就要换成自回归循环。
figure/rnn_architecture.svg画的就是这套结构。我二次开发时会把这张图跟seq2seq_model.py对照看,能快速理解(h, c)从哪一层传进去、哪一层输出接全连接层。
3.3 PITG 模型:把密码变换建模成“编辑路径”
code/pitg_model.py是这套工程里我认为最有价值的部分。传统 Seq2Seq 直接生成目标密码,而 PITG 的思路是先生成一条编辑路径:从原始密码出发,每一步是copy、replace、insert、delete中的一种。这样模型不用记住密码字符的具体分布,只需要学会“什么位置做什么操作”,数据量要求低,解释性也强。
code/edit_path.py负责把src和tgt对齐成编辑路径。下面是一个简化版实现。
def compute_edit_path(src: str, tgt: str): ops = [] i = j = 0 while i < len(src) or j < len(tgt): if i < len(src) and j < len(tgt) and src[i] == tgt[j]: ops.append(('copy', src[i])) i += 1 j += 1 elif j < len(tgt) and i < len(src): ops.append(('replace', src[i], tgt[j])) i += 1 j += 1 elif j < len(tgt): ops.append(('insert', tgt[j])) j += 1 else: ops.append(('delete', src[i])) i += 1 return ops这个贪心对齐只做参考,真正的训练样本来自generate_inplace_edits.py:它枚举“原地编辑”操作来构造正负样本,而不是完全依赖简单的对齐算法。pitg_model.py会把编辑操作序列 token 化,然后用 LSTM 或 Transformer 预测下一个操作。参数上要关注ops_vocab,如果操作类型不全,比如漏了大小写替换,模型就学不会zhangwei到ZhangWei这种常见变换。
pitg_region_trainer.py和pitg_host_trainer.py是训练器两种变体:前者按密码区域如数字段、字母段分别训练,强调局部模式;后者用数据集里所有用户信息一起训练,强调全局习惯。我在实际跑的时候发现,region 版在小数据集上更容易收敛,但泛化到没见过的用户名时会差一些。
3.4 把用户名卷进来的猜测入口
code/crack_with_username.py是面向效果的入口,它把“用户名”当成密码生成的条件。比如用户名叫zhangwei,密码很可能是zhangwei123、ZhangWei!@#、zw123456。这个逻辑单靠 Seq2Seq 也能学,但显式建模成 username-to-password 会让命中率更可解释。
src/baseline/targuess/提供的是统计基线。它会根据用户名、邮箱前缀、数字尾巴做模板猜测,比如检测到用户名后拼接年份和常见符号。训练模型的 guessrate 要和这套基线对比才有意义,否则你不知道模型提升是来自数据集的偶然性还是真的学到了编辑规律。
pitg_cracker.py则负责把训练好的模型应用到候选生成:给一批用户名和可能的原始密码,批量生成候选密码,再按概率排序输出。这个文件是评估脚本和真实猜测之间的桥梁。
4. 训练与评估:loss、Beam Search 和 guessrate 曲线背后
4.1 训练器:loss 计算和参数配置
pass2path_trainer.py和pitg_trainer.py的结构相似,区别在数据和模型。通用训练循环如下。
for epoch in range(epochs): for batch in train_loader: src, tgt = batch optimizer.zero_grad() logits = model(src, tgt[:, :-1]) loss = loss_fn(logits.reshape(-1, vocab_size), tgt[:, 1:].reshape(-1)) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step()逻辑说明:tgt[:, :-1]是解码器输入,tgt[:, 1:]是目标输出,这是标准 teacher forcing 写法。reshape(-1, vocab_size)合并 batch 和序列维度,方便计算交叉熵。max_norm=1.0的梯度裁剪很关键,LSTM 训练时偶尔出现 loss 突变成 NaN,大部分原因是梯度爆炸。
参数说明:epochs=10、lr=1e-3、batch_size=128是常见起点。日志里的 loss 如果前两个 epoch 没从 4.5 降到 3.0,就该检查是否用了错误的padding_idx,或者数据集的 tokenizer 没把<eos>算进 vocab。
pitg_evaluator.py和pass2path_evaluator.py是评估入口,它们不参与训练,而是加载 checkpoint 后跑候选生成。配置都在config/目录下,我改参数时主要改三个:模型结构配置、数据路径、评估的beam_size。
4.2 贪婪搜索与 Beam Search:命中率和召回率从哪来
代码里有draw_targuess_guessrate.py,说明模型评估的核心指标是 guessrate。guessrate 表示“生成前 N 个候选里,能覆盖多少真实密码”。贪婪搜索只取每个时刻概率最大的 token,表现通常很差;Beam Search 保留 top K 条部分序列,最后按累计对数概率排序。
def beam_search(model, src, tokenizer, beam_size=5, max_len=20): with torch.no_grad(): src_emb = model.embed(src.unsqueeze(0)) enc_out, (h, c) = model.encoder(src_emb) sequences = [[tokenizer.sos_idx, 0.0]] for _ in range(max_len): all_candidates = [] for seq, score in sequences: if seq[-1] == tokenizer.eos_idx: all_candidates.append((seq, score)) continue input_tok = torch.tensor([seq[-1]]).unsqueeze(0) logits = model.decode_step(input_tok, h, c) probs = torch.log_softmax(logits, dim=-1).squeeze(0) for idx, p in zip(*torch.topk(probs, beam_size)): all_candidates.append((seq + [idx.item()], score + p.item())) sequences = sorted(all_candidates, key=lambda x: x[1], reverse=True)[:beam_size] return sequences[0][0]这里需要注意torch.topk的返回值。probs是vocab_size的一维向量,zip(*torch.topk(probs, beam_size))会同时取出分数和索引。beam_size不是越大越好:调大 beam 能提升召回率,但会把生成时间放大 beam 倍,在小数据集上还会让同一模板的候选大量重复,guessrate 曲线反而变稀疏。
指标上,代码里的命中率一次只计算“当前这批候选是否包含目标密码”,和“全部历史候选合计命中”是两回事。我见过的翻车现场大多是直接把每次实验的 guessrate 相加,导致最终曲线超过 1.0,看着很强,实际是口径错了。
4.3 可视化脚本:对比曲线是给谁看的
draw_targuess_guessrate_comparison.py、draw_targuess_guessrate.py、draw_guessrate_via_editdistance.py都是画图。前者比较不同模型,后者按编辑距离分组看命中率,比如距离为 1、2、3 的密码分别命中多少。
import matplotlib.pyplot as plt def draw_guessrate(curves, labels, out_path): x = [i + 1 for i in range(len(curves[0]))] fig, ax = plt.subplots(figsize=(8, 5)) for curve, label in zip(curves, labels): ax.plot(x, curve, marker='.', label=label) ax.set_xscale('symlog') ax.set_xlabel('number of guesses (log)') ax.set_ylabel('guess rate') ax.legend() fig.savefig(out_path, dpi=150, bbox_inches='tight')注意这里用xscale('symlog')而不是log,因为 guessrate 前几名可能为 0,log会把 0 值直接画出空点。dpi=150是为论文或报告准备,bbox_inches='tight'避免 SVG 输出时裁掉图例。这个脚本本身不训练模型,只消费评估器生成的 CSV,所以我可以反复调样式而不污染实验结果。
5. 常见问题与避坑:把这套代码跑起来时我踩过的几个坑
5.1 tokenizer 字符表不一致,训练出来全是<unk>
现象:模型 loss 能降,但 decode 出来的密码大多是<sos><eos>或重复的单个字符。
原因:训练时用的 tokenizer 是从训练集生成的,评估时又加载了另一个 vocab 文件,两边stoi不一致,模型预测的索引在推理 vocab 里对应完全不同的字符。
解决:固定一个vocab.json文件。我会在code/version/里放一份构建好的字符表,并在启动脚本里用绝对路径引用,避免.py文件默认相对路径在不同工作目录下读到不同文件。
5.2 编辑路径生成出现死循环,候选密码长度失控
现象:PITG 模型生成的候选是abcabcabc...越来越长,或者同一个字符插入几百次,评估阶段内存被打满。
原因:generate_inplace_edits.py把 insert 操作放在任何位置,模型学到 insert 可以无限执行;如果解码时不强制终止,copy/insert会互相交替。
解决:给 decode 加长度上限和终止约束。我一般限制生成密码长度不超过原始长度的 1.5 倍,并在生成到<eos>时强制退出;处理 insert 时限制连续 insert 次数不超过 3 次。
5.3 script 里的相对路径找不到数据集
现象:在项目根目录跑sh script/user_pwd_behavior.sh时脚本报FileNotFoundError: data.csv,但文件明明存在。
原因:脚本开头如果用了cd $(dirname $0)/..,从非项目根目录调用时dirname $0拿到的是相对路径,切过去的目录不对。
解决:每个.sh开头加cd "$(dirname "$0")/.."并pwd验证,然后把数据路径通过环境变量DATA_DIR传入,不让脚本硬编码相对路径。
5.4 GPU 显存溢出,batch size 一调就炸
现象:训练跑几个 step 之后CUDA out of memory。
原因:Seq2Seq 的max_len=32,如果 batch 里有大量长密码,PyTorch 没做长度打包,所有序列都补齐到 32,embedding 和 LSTM 的显存占用比预期高很多。
解决:先确认torch.backends.cudnn.benchmark = False,排除 cuDNN 问题,然后按长度分桶。我会写一个简单函数按密码长度排序分组,让每个 batch 内长度接近,显存占用能明显下降。
5.5 guessrate 曲线累计方式搞错,结果看起来百发百中
现象:同一份模型,在draw_targuess_guessrate.py里曲线很漂亮,但换到analyze_cracked_behavior.py里命中率数值对不上。
原因:一个脚本算的是累计命中率,另一个脚本算的是单 batch 命中率。guessrate 严格来说是“在前 N 次候选内命中的比例”,必须把所有历史候选合起来算,而不是只算当前窗口。
解决:先明确指标定义。我在评估输出里同时打印cumulative和per_batch两个数值,画图只消费 cumulative 字段。
6. 二次开发建议:改成你自己的口令评估工具
6.1 接入新数据集要改哪几个文件
如果只是想在自己的脱敏数据上复现实验,我一般只改三个地方:user_pwd_reader.py里的COL_MAP、sample_data.py里的max_per_user、config/下的数据路径。其余脚本尽量不动。先跑通再优化,而不是一次改一堆。
6.2 把 LSTM 换成 Transformer 的改造思路
seq2seq_model.py当前是 LSTM。如果想换注意力机制,我会保留CharTokenizer和PasswordDataset,替换模型主体为 Transformer encoder-decoder,同时把crack_with_username.py里的 decoder 循环改成顺序生成。注意两点:Transformer 对位置编码很敏感,密码长度不固定,建议用正弦位置编码,不要直接训练绝对位置 embedding;批量推理时要用 mask 保证未来 token 不可见。
6.3 验证小技巧:先跑 baseline,再改模型
最后是验证方法。我每次改完代码不会直接上完整训练,而是先跑baseline/targuess和version/里保存的旧 checkpoint,确认draw_targuess_guessrate.py能画出和 README 一致的曲线。判断“这个改动有效”必须用同一条 guessrate 指标对比,而不是只看 loss。
以前我拿到新数据集总是急着调参,结果在structure_analysis.txt的统计口径没统一的前提下改了一晚上参数,第二天发现 baseline 的 guessrate 因为抽样方式变了也跟着涨。从那以后,我每次换数据集或改数据预处理,都强制走一遍version/里的回归脚本,确认统计口径没变,再动模型。希望帮到你。
本文还有配套的精品资源,点击获取