ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

口令破解大作业技术路线选型:统计规则、PCFG与机器学习对比实战

口令破解大作业技术路线选型:统计规则、PCFG与机器学习对比实战

简介:这份资源是国科大Web安全技术课程口令破解方向的大作业参考代码,面向正在修读该课程或自学口令安全与机器学习结合实践的学生。内容围绕口令特征提取、特征统计与暴力猜测三条主线展开,覆盖从数据预处理到模型训练、结果分析的完整流程,适合具备Python基础、希望理解真实口令数据集处理与攻击思路的中高级学习者。压缩包共155个文件,约170.54MB,以78个txt与16个csv承载原始口令及统计结果,15个py脚本实现特征分析与猜测逻辑,另有xlsx统计表、png图表及少量配置与版本控制文件,目录结构便于按模块查阅。目前已有362人学习下载。读者可从中获得可运行的口令特征工程代码、针对csdn与yahoo数据集的统计脚本、暴力猜测实现及配套分析图表,用于课程作业参考、实验复现与排错思路借鉴。

1. 口令破解大作业,为什么我劝你别一上来就上机器学习

口令破解这个大作业,我前后帮三届人看过代码,发现一个特别反直觉的现象:用机器学习的那批人,最后交出来的东西反而最单薄。不是机器学习不行,是大部分人把它当成了万能锤子,拿到数据集就train_test_split,跑完一个分类器,准确率 92%,然后呢?然后就没有然后了——老师问一句「你这 92% 到底比规则方法强在哪」,直接哑火。

口令破解这个题目,本质是「给定一批泄露口令,推断生成规律,再按规律造出能命中的新口令」。它有三条完全不同的技术路线:纯统计规则(HashCat 规则、John 的增量模式)、概率模型(PCFG、Markov)、机器学习(RNN、GAN、Transformer)。三条路线的工程量、可解释性、命中率曲线完全不一样。这篇就把这三条路线拆开对比,告诉你什么情况下该选哪条,以及每条路最容易翻车的地方在哪。如果你正在做这个作业,或者想真正搞懂口令猜测的底层逻辑,这篇能帮你省掉至少两周的无效试错。

2. 三条技术路线怎么选:从四个维度给结论

选路线之前,先明确一件事:这个作业的评分点通常不在「准确率」上,而在「你能不能解释清楚你的模型为什么能生成有效口令」。我见过太多人把重点放反了,花一周调参,结果报告里连 PCFG 的概率公式都写不明白。

2.1 先搞清楚你的数据集长什么样

不同数据集决定了你能走哪条路。常见的几类:

数据集类型典型规模特点适合的路线
小规模泄露集1万~10万条噪声大、重复多统计规则
中等规模10万~100万条有结构规律PCFG / Markov
大规模100万条以上覆盖广、长尾明显机器学习
带个人信息不定含用户名、生日等规则+模板混合

如果你拿到的数据集只有几万条,硬上 RNN,训练集都不够模型收敛,最后生成的口令全是训练集里的原样复制,这种「过拟合式抄袭」在答辩时一问就穿帮。我一般会先做一件事:统计数据集的口令长度分布、字符集分布、是否有明显的结构模式(比如「单词+数字」「拼音+特殊符号」)。这个统计结果直接决定路线选择。

2.2 四个维度的逐项对比

把三条路线拉到同一张表里比:

维度统计规则概率模型(PCFG/Markov)机器学习(RNN/GAN)
实现难度低,半天能跑通中,需要理解概率图高,调参+训练+生成
可解释性强,每条规则可追溯较强,概率可拆解弱,黑匣子
命中率(小数据集)中高低(欠拟合)
命中率(大数据集)低(规则覆盖不够)中高高
答辩友好度高,逻辑清晰高,有数学支撑看你能不能讲清楚
工程量小中大

这张表的核心结论是:数据集规模决定了你的上限,可解释性决定了你的答辩下限。小数据集选机器学习是自找麻烦,大数据集只用规则又浪费了数据。

2.3 我的选型建议:分场景给结论

场景一:数据集小于 10 万条,时间紧。直接上 PCFG。PCFG(Probabilistic Context-Free Grammar)的核心思路是把口令拆成结构模板,比如「字母段+数字段+特殊符号段」,然后统计每段的概率分布。实现起来不复杂,而且每一步都能解释。你可以在报告里画出模板概率树,老师一看就懂。

场景二:数据集 10 万到 100 万条,想拿高分。PCFG + Markov 混合。PCFG 负责结构,Markov 负责段内字符生成。这个组合的命中率比纯 PCFG 高 15%~30%,而且实现难度可控。

场景三:数据集超过 100 万条,且你有 GPU 资源。可以上 RNN(LSTM 或 GRU),但必须做对比实验——用同样的测试集,跑一遍 PCFG 基线,再跑你的 RNN,证明 ML 确实更好。没有对比实验的 ML 方案,在答辩时基本等于没做。

提示:不管你选哪条路,都要留出 20% 的数据做测试集,而且测试集的口令不能出现在训练集里。我见过有人不小心把测试集混进训练集,命中率虚高到 80%,被老师当场拆穿。

2.4 一个容易被忽略的评判点:生成效率

很多人只关注「命中率」,忽略了「生成速度」。口令破解在实际场景里是有时间约束的——你不可能花三天生成一批候选口令。统计规则每秒能生成百万级候选,PCFG 大概十万级,RNN 生成速度取决于模型大小,通常只有千级到万级。

如果你的作业要求里提到了「在限定时间内命中尽可能多的口令」,那生成效率就是一个硬指标。这时候纯 ML 方案反而吃亏,因为它的推理速度慢。我一般会建议:用 ML 生成候选池,再用规则做优先级排序,这样兼顾命中率和效率。

3. PCFG 实战:从零搭一个能跑的口令生成器

PCFG 是我最推荐的入门路线,因为它把「概率」和「结构」结合得恰到好处,代码量不大,但能讲的东西很多。这一章从数据预处理讲到生成,每一步都给可复现的细节。

3.1 口令结构拆解:模板是怎么定义的

PCFG 的第一步是定义模板。一个口令比如abc123!,拆成L3D3S1,其中 L 代表字母段、D 代表数字段、S 代表特殊符号段,后面的数字是长度。password2024拆成L8D4。

拆解规则我一般这么定:

import re def split_password(pwd): """ 将口令拆分为 字母段/数字段/特殊符号段 的序列 返回模板字符串和对应的段内容列表 """ # 按字符类型分组,连续同类型字符合并为一段 segments = [] current = '' current_type = None for ch in pwd: if ch.isalpha(): ch_type = 'L' elif ch.isdigit(): ch_type = 'D' else: ch_type = 'S' if ch_type == current_type: current += ch else: if current: segments.append((current_type, current)) current = ch current_type = ch_type if current: segments.append((current_type, current)) # 生成模板,如 L3D3S1 template = ''.join(f"{t}{len(s)}" for t, s in segments) return template, segments # 测试 print(split_password("abc123!")) # ('L3D3S1', [('L','abc'), ('D','123'), ('S','!')]) print(split_password("password2024")) # ('L8D4', [('L','password'), ('D','2024')])

这段代码的关键在于连续同类型字符合并。有人会把abc拆成L1L1L1,那样模板空间会爆炸,概率统计也会失真。合并之后,模板数量会大幅减少,统计意义更强。

拆解完之后,你需要统计两样东西:每个模板出现的频率(结构概率),以及每个段内具体内容的频率(内容概率)。比如L8D4这个模板出现了 5000 次,其中L8段是password的有 800 次,那P(password|L8) = 800/5000。

3.2 概率统计与模型训练:几个必须处理的坑

统计阶段有几个坑,我一个个说。

坑一:低频模板怎么处理。有些模板只出现一两次,如果直接保留,生成时会产出大量奇怪结构。我一般设一个阈值,出现次数少于 5 次的模板归入「其他」类,或者直接丢弃。

坑二:段内容的大小写问题。Password和password要不要合并?我的做法是:统计时保留原始大小写,但额外统计一个「首字母大写」的概率。生成时先按小写生成,再按概率决定是否首字母大写。这样既保留了信息,又避免了数据稀疏。

坑三:数字段的处理。数字段不是简单的「随机数字」,很多人用生日、年份、手机号后四位。我一般会把数字段进一步分类:纯年份(19xx/20xx)、纯生日(月日组合)、递增序列(123/1234)、重复数字(111/666)。分类之后分别统计概率,生成时按类别采样。

from collections import Counter, defaultdict class PCFGModel: def __init__(self): self.template_counter = Counter() # 模板频率 self.segment_counter = defaultdict(Counter) # 每个模板下各段的频率 self.total = 0 def train(self, passwords): for pwd in passwords: template, segments = split_password(pwd) self.template_counter[template] += 1 self.total += 1 for seg_type, seg_value in segments: # key 用 模板+段类型+段长度,保证对齐 key = f"{template}|{seg_type}{len(seg_value)}" self.segment_counter[key][seg_value] += 1 def get_template_prob(self, template): return self.template_counter[template] / self.total def get_segment_prob(self, template, seg_type, seg_len, value): key = f"{template}|{seg_type}{seg_len}" counter = self.segment_counter.get(key) if not counter: return 1e-10 # 平滑处理,避免零概率 total = sum(counter.values()) return counter[value] / total

这段代码里key的设计很关键。用「模板+段类型+段长度」做联合索引,保证同一个模板下同类型同长度的段在一起统计。平滑处理用了一个极小的1e-10,防止生成时出现零概率导致采样失败。

3.3 口令生成与优先级排序

训练完之后就是生成。生成逻辑是:先按模板概率采样一个模板,再按段内容概率逐段采样,拼起来就是一个候选口令。

import random def generate_password(model, num=1000): """按 PCFG 概率生成候选口令""" templates = list(model.template_counter.keys()) template_weights = [model.template_counter[t] for t in templates] results = [] for _ in range(num): # 按概率选模板 template = random.choices(templates, weights=template_weights, k=1)[0] # 解析模板,逐段生成 parts = re.findall(r'([LDS])(\d+)', template) pwd = '' for seg_type, seg_len in parts: seg_len = int(seg_len) key = f"{template}|{seg_type}{seg_len}" counter = model.segment_counter.get(key) if counter: values = list(counter.keys()) weights = list(counter.values()) pwd += random.choices(values, weights=weights, k=1)[0] else: # 没有统计数据的段,用默认字符集随机填充 charset = {'L': 'abcdefghijklmnopqrstuvwxyz', 'D': '0123456789', 'S': '!@#$%^&*'}[seg_type] pwd += ''.join(random.choices(charset, k=seg_len)) results.append(pwd) return results

生成之后,优先级排序是提分的关键。同样生成 10 万个候选,按概率从高到低排序,先试概率高的,命中率曲线会明显更陡。排序依据就是P(模板) × ∏P(段内容),取对数避免下溢。

注意:生成数量不是越多越好。我一般会生成 5 到 10 倍于测试集大小的候选量,然后看命中率曲线。如果曲线在某个点之后基本平了,说明模型覆盖能力到顶了,再加量也没用。

4. 机器学习路线:RNN 生成口令的正确打开方式

如果你数据集够大,或者作业明确要求用机器学习,那 RNN 是最稳妥的选择。但「能跑」和「能拿分」之间差了很多细节。

4.1 数据预处理:字符级还是 token 级

口令生成的 RNN 通常是字符级建模,因为口令长度短、字符集有限。字符集我一般控制在 95 个可打印 ASCII 字符以内,超出范围的直接过滤。

预处理步骤:

  1. 过滤掉长度小于 4 或大于 30 的口令(太短没规律,太长训练慢)
  2. 统一字符集,非 ASCII 字符转成占位符或直接丢弃
  3. 每条口令首尾加<start>和<end>标记
  4. 按 8:1:1 划分训练/验证/测试集
import numpy as np def build_vocab(passwords): """构建字符表""" chars = set() for pwd in passwords: chars.update(pwd) chars = sorted(chars) # 特殊标记 vocab = {'<pad>': 0, '<start>': 1, '<end>': 2} for i, ch in enumerate(chars): vocab[ch] = i + 3 return vocab def encode(pwd, vocab, max_len=30): """将口令编码为索引序列""" seq = [vocab['<start>']] + [vocab.get(ch, 0) for ch in pwd] + [vocab['<end>']] # 填充到固定长度 if len(seq) < max_len: seq += [vocab['<pad>']] * (max_len - len(seq)) else: seq = seq[:max_len] return seq

这里max_len的选择有讲究。设太小,长口令被截断;设太大,填充太多影响训练效率。我一般取数据集口令长度的 95 分位数。

4.2 模型结构与训练参数

模型用两层 LSTM 加一个全连接输出层就够了,不需要上 Transformer——口令生成的序列依赖没那么复杂,Transformer 反而容易过拟合。

import torch import torch.nn as nn class PasswordRNN(nn.Module): def __init__(self, vocab_size, embed_dim=64, hidden_dim=256, num_layers=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, dropout=0.3) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, x, hidden=None): emb = self.embedding(x) out, hidden = self.lstm(emb, hidden) logits = self.fc(out) return logits, hidden

训练参数:batch_size 128,学习率 1e-3,优化器 Adam,训练 20 到 30 个 epoch。关键点在于用验证集的 loss 做早停,不要死磕训练轮数。我见过有人训练到 loss 0.01,结果生成的口令全是训练集原样,这就是过拟合的典型表现。

4.3 生成策略:温度参数怎么调

RNN 生成时,温度参数(temperature)直接决定生成口令的多样性。温度低(0.5),生成的口令保守、接近训练集高频模式;温度高(1.5),多样性好但容易生成乱码。

我的做法是:生成多组不同温度的结果,分别评估命中率,选最优温度。通常 0.7 到 1.0 之间效果最好。

def generate_with_temperature(model, vocab, start_token, temperature=0.8, max_len=30): """按温度采样生成口令""" model.eval() idx2char = {v: k for k, v in vocab.items()} input_seq = torch.tensor([[vocab[start_token]]]) hidden = None result = [] with torch.no_grad(): for _ in range(max_len): logits, hidden = model(input_seq, hidden) logits = logits[:, -1, :] / temperature probs = torch.softmax(logits, dim=-1) next_idx = torch.multinomial(probs, 1).item() if next_idx == vocab['<end>']: break if next_idx > 2: # 跳过特殊标记 result.append(idx2char[next_idx]) input_seq = torch.tensor([[next_idx]]) return ''.join(result)

温度参数的本质是调整概率分布的平滑程度。温度趋近 0 时等价于贪心搜索,温度趋近无穷时等价于均匀采样。实际用的时候,我一般会跑 0.6、0.8、1.0 三组,每组生成 5 万条,看哪组的命中率最高。

5. 避坑指南:那些年我在口令破解作业里翻过的车

这一章全是血泪经验,每一条都是真实踩过的坑。如果你正在做这个作业,建议逐条对照检查。

5.1 命中率虚高:测试集泄露

现象:模型报告命中率 75%,答辩时老师让现场跑一遍,结果只有 30%。

原因:训练集和测试集有重叠。最常见的情况是数据集本身有大量重复口令,去重没做干净,同一条口令既在训练集又在测试集。

解决:划分数据集之前先做全局去重。用set()去重后再划分。另外,测试集的口令不能以任何形式出现在训练过程中,包括验证集调参时也不能偷看测试集。

5.2 生成的口令全是训练集原样

现象:RNN 生成 10 万条口令,去重后只剩 2 万条,而且大部分能在训练集里找到一模一样的。

原因:过拟合。模型记住了训练样本,没有学到泛化规律。温度参数太低也会加剧这个问题。

解决:加 dropout(0.2 到 0.4),用早停,提高温度参数。另外可以在生成时加一点随机扰动,比如随机替换一个字符。判断是否过拟合的简单方法:看生成口令与训练集的重合率,超过 50% 就是过拟合了。

5.3 PCFG 模板爆炸

现象:统计出几万个模板,每个模板只有一两条口令,生成时完全随机,命中率极低。

原因:模板定义太细。比如把abc123拆成L1L1L1D1D1D1,而不是L3D3。

解决:连续同类型字符合并,低频模板归并。我一般会把出现次数少于 10 次的模板合并成「其他」类,或者直接丢弃。模板总数控制在 500 到 2000 之间比较合理。

5.4 数字段处理太粗糙

现象:生成的口令里数字段全是随机数字,但测试集里的数字大量是年份和生日,导致命中率上不去。

原因:数字段没有做子分类,统一按字符频率采样了。

解决:把数字段分成年份、生日、序列、重复、随机五类,分别统计概率。生成时先选类别,再按类别生成。这个改动通常能提升 10% 以上的命中率。

5.5 评估指标选错

现象:用准确率(accuracy)评估模型,报告里写「准确率 95%」,但实际命中率很低。

原因:口令生成不是分类问题,准确率没有意义。正确的指标是「命中率」——生成的候选口令中,有多少比例出现在测试集里。

解决:评估时用命中率曲线(hit rate curve),横轴是生成数量,纵轴是命中比例。另外可以报告「每命中一条口令需要尝试多少次」这个指标,更直观。

6. 最后说一个提分技巧:混合策略的优先级排序

如果你时间充裕,我强烈建议做混合策略:用 PCFG 生成候选池,用 RNN 补充长尾,最后按概率排序。具体做法是:

  1. PCFG 生成 5 万条高概率候选
  2. RNN 生成 5 万条多样性候选
  3. 合并去重,按 PCFG 概率打分排序
  4. 取前 1 万条作为最终输出

这个策略的好处是兼顾了 PCFG 的可解释性和 RNN 的覆盖能力。在报告里你可以分别展示三条曲线:纯 PCFG、纯 RNN、混合策略,然后说明混合策略在相同尝试次数下命中率最高。这种对比实验是答辩时最加分的部分。

def hybrid_generate(pcfg_model, rnn_model, vocab, num_pcfg=50000, num_rnn=50000): """混合生成:PCFG 候选 + RNN 候选,按概率排序""" # PCFG 生成 pcfg_candidates = generate_password(pcfg_model, num=num_pcfg) # RNN 生成 rnn_candidates = [] for _ in range(num_rnn): pwd = generate_with_temperature(rnn_model, vocab, '<start>', temperature=0.8) if len(pwd) >= 4: rnn_candidates.append(pwd) # 合并去重 all_candidates = list(set(pcfg_candidates + rnn_candidates)) # 按 PCFG 概率打分排序(RNN 候选如果没有对应模板,给一个基础分) def score(pwd): template, segments = split_password(pwd) p = pcfg_model.get_template_prob(template) for seg_type, seg_value in segments: p *= pcfg_model.get_segment_prob(template, seg_type, len(seg_value), seg_value) return p all_candidates.sort(key=score, reverse=True) return all_candidates

这段代码的核心是score函数——它给每条候选口令打一个概率分,然后按分排序。RNN 生成的候选如果不符合任何已知模板,概率分会很低,自然排在后面。这样既保留了 RNN 的多样性,又保证了高概率候选优先被尝试。

回过头看,这个作业最大的价值不是让你学会某个具体算法,而是让你理解「在资源受限的情况下,怎么用概率思维做优先级决策」。口令破解本质上是一个搜索问题,你的模型就是你的搜索策略。策略好不好,不看它多复杂,看它在有限尝试次数内能命中多少。

从那以后我每次做类似的数据驱动任务,都会先问自己三个问题:数据量够不够支撑复杂模型?可解释性重不重要?评估指标选对了吗?这三个问题想清楚,路线基本就不会选错。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进