没有 LLM 的时代,程序如何判断一段文字是"人话"?
基于 CrypTool-2 凯撒密码暴力破解的自然语言识别机制深度剖析

摘要
在大语言模型(LLM)出现之前,密码分析工具面临一个很实际的问题:暴力破解产生了一堆候选明文,程序怎么知道哪个是"人话",哪个是乱码? 本文通过阅读开源密码学教育平台 CrypTool-2 (Kopal, 2018) 的源代码,拆解它在凯撒密码暴力破解中使用的四种自然语言识别方法:重合指数(IoC)、信息熵(Entropy)、N-gram 对数概率评分、以及字典匹配法。每种方法背后的数学原理、数据结构和代码实现都会展开讨论。
1. 问题定义:暴力破解的"最后一公里"
凯撒密码的密钥空间仅有 25 个(移位值 1~25),暴力破解的计算量微不足道。真正的挑战在于:
给定 25 个解密结果,如何让程序自动选出那个"看起来像人话"的结果?
例如,对密文 KHOOR ZRUOG 暴力破解:
| 移位值 | 解密结果 | 是否为"人话" |
|---|---|---|
| 1 | JGNNQ YQTNF | 否 |
| 2 | IFMMP XPSME | 否 |
| 3 | HELLO WORLD | 是 |
| ... | ... | 否 |
人眼一扫就能挑出答案。但对程序来说,JGNNQ 和 HELLO 都只是五个 ASCII 字符——它需要一个量化标准来区分自然语言和随机噪声。
CrypTool-2 提供了四种做法,思路各不相同。
2. 方法一:重合指数(Index of Coincidence)
2.1 数学原理
重合指数(IoC)由 William Friedman 于 1922 年提出 (Friedman, 1922),基于一个简单的观察:
自然语言中字母分布极不均匀,随机文本中字母分布趋于均匀。
英语中 E 占比约 12.7%,而 Z 仅 0.074%。如果从文本中随机抽取两个字母,它们相同的概率在自然语言中远高于随机文本。IoC 正是度量这一概率的指标:
其中 为第 个字母的出现次数, 为文本总长度, 为字母表大小。
- 英语文本:IoC ≈ 0.0661
- 德语文本:IoC ≈ 0.0762
- 均匀随机分布:IoC ≈ 1/26 ≈ 0.0385
2.2 CrypTool-2 实现
源文件:CrypPlugins/CostFunction/CostFunction.cs,第 384-416 行。
1public double calculateFastIndexOfCoincidence(byte[] text, int bytesToUse)
2{
3 if (bytesToUse > text.Length)
4 {
5 bytesToUse = text.Length;
6 }
7
8 double[] n = new double[256]; // 256 长度的计数数组,覆盖全部 ASCII
9 //count all ASCII symbols
10 int counter = 0;
11 foreach (byte b in text)
12 {
13 n[b]++;
14 counter++;
15 if (counter == bytesToUse)
16 {
17 break;
18 }
19 }
20
21 double coindex = 0;
22 //sum them
23 for (int i = 0; i < n.Length; i++)
24 {
25 coindex = coindex + n[i] * (n[i] - 1); // Σ n_i * (n_i - 1)
26 }
27
28 coindex = coindex / (bytesToUse); // 除以 N
29 coindex = coindex / (bytesToUse - 1); // 除以 (N-1)
30
31 return coindex;
32}关系运算符:LargerThen(值越大,越像自然语言)。
见
CostFunctionControl.GetRelationOperator()方法(同文件第 567-580 行):csharp1case CostFunctionSettings.CostFunctionType.IOC: 2 return RelationOperator.LargerThen;

2.3 局限性
IoC 的计算复杂度是 O(N),不需要任何预训练数据,跑起来非常快。但它只能区分"均匀分布"和"非均匀分布",粒度很粗——两种不同的自然语言它可能分不开。所以 IoC 更多是一个粗筛器:能快速排除明显的乱码,但面对两个都"看起来差不多"的候选项就力不从心了。它在密码分析中更常见的用途是 Friedman 测试——用来估算多表替换密码的密钥长度。
3. 方法二:信息熵(Shannon Entropy)
3.1 数学原理
Shannon (1948) 定义的信息熵衡量文本的"信息密度"或"不确定性":
其中 为第 个符号的出现概率。
- 自然语言:字母分布不均匀 → 熵值低(德语约 4.06 bits)
- 随机文本:26 个字母等概率出现 → 熵值高( bits)
- 单字符重复:如 "AAAA..." → 熵值为 0
3.2 CrypTool-2 实现
源文件:CrypPlugins/CostFunction/CostFunction.cs,第 422-497 行。
CrypTool-2 在这里做了一个预计算优化——预先建立 xlogx 查找表,避免运行时反复调用 Math.Log:
1// 预计算阶段:构建 xlogx 表(第 422-431 行)
2private void prepareEntropy(int size)
3{
4 xlogx = new float[size + 1];
5 //precomputations for fast entropy calculation
6 xlogx[0] = 0.0f;
7 for (int i = 1; i <= size; i++)
8 {
9 // xlogx[i] = -i * log2(i/size)
10 xlogx[i] = (float)(-1.0f * i * Math.Log(i / (double)size) / Math.Log(2.0));
11 }
12}1// 计算阶段(第 450-497 行):根据 EntropySelection 切换实现
2public double calculateEntropy(byte[] text, int bytesToUse)
3{
4 switch (settings.EntropySelection)
5 {
6 case 0: // C++/CLI 原生实现,用于高性能场景
7 return NativeCryptography.Crypto.calculateEntropy(text, bytesToUse);
8 case 1: // C# 托管实现(含 xlogx 查表优化)
9 // ... Mutex 保护的预计算检查 ...
10 int[] n = new int[256];
11 for (int counter = 0; counter < bytesToUse; counter++)
12 {
13 n[text[counter]]++; // 统计字符频率
14 }
15
16 float entropy = 0;
17 for (int i = 0; i < 256; i++)
18 {
19 entropy += xlogx[n[i]]; // 查表累加,避免实时 log 运算
20 }
21 return entropy / (double)bytesToUse;
22 default:
23 return NativeCryptography.Crypto.calculateEntropy(text, bytesToUse);
24 }
25}用户可通过 EntropySelection 设置在 C++/CLI 原生实现和 C# 托管实现之间切换。
关系运算符:LessThen(值越小,越像自然语言)。
3.3 IoC 与 Entropy 的等价性讨论
IoC 和 Entropy 其实度量的是同一件事——字母分布有多不均匀。IoC 越高,Entropy 就越低,两者近似单调关系。不过它们在边界情况和数值灵敏度上有些差别,所以 CrypTool-2 把两个都留着让用户选。
4. 方法三:N-gram 对数概率评分(核心方法)
4.1 从"字母频率"到"字母组合频率"
IoC 和 Entropy 都只看单个字母出现了几次,不关心字母的排列顺序。所以 ETAOIN SHRDLU(英语中最常见字母的排列)和 HELLO WORLD(一句合法英语)在它们眼里可能差不多——单字母频率分布太相似了。
要把"看似合理的乱码"和真正的自然语言区分开,得看字母的组合模式。英语中 TH 出现频率极高,QZ 几乎不出现;TION 是常见的四字母组合,XKZQ 不是。N-gram 统计就是用来捕获这种字母序列规律的。
4.2 数据生成:从语料库到概率表
CrypTool-2 通过 LanguageStatisticsGenerator(源文件:Util/LanguageStatisticsGenerator/Program.cs)从大规模语料库生成 N-gram 频率数据。支持两种数据源:
- Gutenberg 图书馆:Project Gutenberg 的公共领域图书 ZIP 文件集合
- Wikipedia XML Dump:维基百科的 XML 导出文件
数据源的选择很直接(第 71-78 行):
1if (path.ToUpper().EndsWith("XML"))
2{
3 ReadInWikipediaXML(path);
4}
5else
6{
7 ReadInGutenbergZips(path);
8}生成流程(以 5-gram 为例):
1Wikipedia/Gutenberg 语料库
2 ↓ 滑动窗口扫描
3 ↓ 8线程并行处理(WORKERS = 8)+ ConcurrentQueue + Lock合并
45维频率数组 uint[26,26,26,26,26]
5 ↓ 概率转换
6 ↓ Math.Log(count == 0 ? 1/sum : count/sum)
7对数概率表 float[26,26,26,26,26]
8 ↓ 序列化
9CTLS 格式 GZip 压缩文件 (.gz)关键的概率转换代码(Program.cs 第 542-545 行):
1private static IEnumerable<float> CalculateLogs(Array freq, ulong sum)
2{
3 return freq.Cast<uint>().Select(value =>
4 (float)Math.Log(value == 0 ? 1.0 / sum : value / (double)sum));
5}为什么用对数而不是原始概率?两个原因。第一,原始概率太小了——5-gram 的概率可能是 10^-8 量级,连续乘下去浮点数会下溢归零。取对数以后乘法就变成加法,数值稳定得多。第二,语料库中从未出现过的 N-gram(count=0)不能直接取 log(0),代码里用 1/sum 替代,相当于给了一个极小的概率,取对数后变成一个很大的负值——对罕见组合施加惩罚。
4.3 文件格式:CTLS 二进制协议
源文件:LibSource/LanguageStatisticsLib/LanguageStatisticsFile.cs
文件头结构:
1[Magic: 4 bytes "CTLS"] [语言代码: BinaryWriter.WriteString] [gram长度: int32]
2[字母表: BinaryWriter.WriteString] [频率数据: float[] 连续块]Magic number 定义(第 31 行):
1public const uint FileFormatMagicNumber = 'C' + ('T' << 8) + ('L' << 16) + ('S' << 24);整个文件经 GZip 压缩。加载时用 Buffer.BlockCopy 把频率数据一次性拷进多维数组(第 78-79 行),之后查表就是普通的数组下标访问:
1byte[] frequencyData = br.ReadBytes(sizeof(float) * frequencyEntries);
2Buffer.BlockCopy(frequencyData, 0, frequencyArray, 0, frequencyData.Length);以英语 4-gram 为例,数组大小为 26^4 = 456,976 个 float 值,约 1.7 MB(压缩前)。5-gram 则为 26^5 ≈ 1190 万个 float,约 45 MB。
4.4 评分算法:滑动窗口累加
以 Tetragrams(4-gram)为例,源文件:LibSource/LanguageStatisticsLib/Grams/Tetragrams.cs,第 44-84 行:
1public override double CalculateCost(int[] text)
2{
3 int end = text.Length - 3;
4 if (end <= 0)
5 {
6 return 0;
7 }
8
9 double value = 0;
10 int alphabetLength = Alphabet.Length;
11
12 for (int i = 0; i < end; i++)
13 {
14 int a = text[i];
15 int b = text[i + 1];
16 int c = text[i + 2];
17 int d = text[i + 3];
18
19 // addLetterIndicies: 字母表缩减映射(可选)
20 if (addLetterIndicies != null)
21 {
22 a += addLetterIndicies[a];
23 b += addLetterIndicies[b];
24 c += addLetterIndicies[c];
25 d += addLetterIndicies[d];
26 }
27
28 // 边界检查:跳过不在字母表中的字符
29 if (a >= alphabetLength ||
30 b >= alphabetLength ||
31 c >= alphabetLength ||
32 d >= alphabetLength ||
33 a < 0 || b < 0 || c < 0 || d < 0)
34 {
35 continue;
36 }
37 value += Frequencies[a, b, c, d]; // 查表:O(1) 的多维数组访问
38 }
39 return value / end; // 返回每个 N-gram 的平均对数概率
40}说白了就是一个滑动窗口:从头到尾扫一遍文本,每次取连续 4 个字母去频率表里查对数概率,全部加起来再除以窗口个数。
拿 "HELLO" 来算:
1H-E-L-L → Frequencies['H','E','L','L'] = log(P("HELL")) ≈ -8.2 (常见组合)
2E-L-L-O → Frequencies['E','L','L','O'] = log(P("ELLO")) ≈ -9.1 (较常见)
3平均值 ≈ -8.65对于随机乱码 "XKZQ":
1X-K-Z-Q → Frequencies['X','K','Z','Q'] = log(1/sum) ≈ -15.3 (极罕见)
2平均值 ≈ -15.3值越大(越接近 0),文本越像自然语言。差距一目了然。
4.5 CrypTool-2 中的调用链
当 CostFunction 插件被设置为 NGramsLog2 模式时,整个调用链为:
1CostFunction.CalculateCost(byte[] text)
2 → text → UTF8解码 → ToUpper() → 字符串
3 → LanguageStatistics.MapTextIntoNumberSpace(text, alphabet)
4 → 每个字符在字母表中查找索引,得到 int[]
5 → Grams.CalculateCost(int[] numbers)
6 → 滑动窗口,查频率表,累加,取平均
7 → return 平均对数概率值源文件 CostFunction.cs 第 222-224 行:
1case CostFunctionSettings.CostFunctionType.NGramsLog2:
2 return Grams.CalculateCost(LanguageStatistics.MapTextIntoNumberSpace(
3 Encoding.UTF8.GetString(text).ToUpper(),
4 LanguageStatistics.Alphabets[LanguageStatistics.LanguageCode(settings.Language)]));MapTextIntoNumberSpace 的实现(LanguageStatistics.cs 第 312-322 行)将每个字符映射为字母表中的索引:
1public static int[] MapTextIntoNumberSpace(string text, string alphabet)
2{
3 int[] numbers = new int[text.Length];
4 int position = 0;
5 foreach (char c in text)
6 {
7 numbers[position] = alphabet.IndexOf(c);
8 position++;
9 }
10 return numbers;
11}4.6 N-gram 大小的选择与权衡
CrypTool-2 通过 CostFunctionSettings.NGramSize 支持 1-gram 到 5-gram,LanguageStatisticsLib 库层面还支持 6-gram。默认使用 5-gram(Pentagrams)。
| N-gram 大小 | 数组维度 | 存储空间(26字母) | 语言感知粒度 | 典型用途 |
|---|---|---|---|---|
| 1-gram | 26 | 104 B | 单字母频率 | 等价于 IoC/Entropy |
| 2-gram | 26^2 = 676 | 2.6 KB | 字母对 | 快速粗筛 |
| 3-gram | 26^3 = 17,576 | 68 KB | 短模式 | Enigma 分析常用 |
| 4-gram | 26^4 ≈ 46 万 | 1.7 MB | 中等模式 | 通用分析 |
| 5-gram | 26^5 ≈ 1190 万 | 45 MB | 长模式 | CT2 默认,精度最高 |
| 6-gram | 26^6 ≈ 3.09 亿 | 1.2 GB | 极长模式 | 内存受限 |
N 越大判断越精确,但存储空间指数增长。5-gram 在精度和内存之间取了一个比较实际的平衡。
LanguageStatistics.cs 第 180-198 行的工厂方法可以看出这个设计:
1public static Grams CreateGrams(int languageId, string languageStatisticsDirectory,
2 GramsType gramsType, bool useSpaces)
3{
4 switch (gramsType)
5 {
6 case GramsType.Unigrams: return new Unigrams(...);
7 case GramsType.Bigrams: return new Bigrams(...);
8 case GramsType.Trigrams: return new Trigrams(...);
9 case GramsType.Tetragrams: return new Tetragrams(...);
10 case GramsType.Pentragrams: // CT2 的默认 ngram 大小
11 default: return new Pentagrams(...);
12 case GramsType.Hexagrams: return new Hexagrams(...);
13 }
14}4.7 多语言支持
LanguageStatistics.cs 定义了 15 种语言的字母表(第 142-159 行):
1public static Dictionary<string, string> Alphabets = new Dictionary<string, string>()
2{
3 {"en", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 英语:26 字母
4 {"de", "ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜß" }, // 德语:30 字母(含变音符号)
5 {"fr", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 法语
6 {"es", "ABCDEFGHIJKLMNOPQRSTUVWXYZÑ" }, // 西班牙语:27 字母
7 {"it", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 意大利语
8 {"hu", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 匈牙利语
9 {"ru", "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" }, // 俄语:33 西里尔字母
10 {"cs", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 捷克语
11 {"la", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, // 拉丁语
12 {"el", "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ" }, // 希腊语:24 字母
13 {"nl", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, // 荷兰语
14 {"sv", "ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ"}, // 瑞典语:29 字母
15 {"pt", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, // 葡萄牙语
16 {"pl", "AĄBCĆDEĘFGHIJKLŁMNŃOÓPQRSŚTUVWXYZŹŻ"}, // 波兰语:35 字母
17 {"tr", "ABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZ" } // 土耳其语:29 字母
18};每种语言都有独立的 N-gram 频率文件(如 en-5gram-nocs.bin、de-4gram-nocs-sp.bin),所以评分时可以指定目标语言。
代码里还硬编码了每种语言的 Unigram 频率(第 118-140 行),注释标注数据来源是 Wikipedia 和 practicalcryptography.com。
5. 方法四:字典匹配法
5.1 工程实现
前面三种方法都是在做概率性判断——算一个分数,看谁高。字典匹配法的思路更朴素:解密结果里能找到多少个真实存在的单词?找到得够多,那大概率就是正确明文。
CrypTool-2 的凯撒暴力破解模板(Templates/Cryptanalysis/Classic/Caesar_ExhaustiveKeySearch.xml)采用的工作流为:
1密文 → [Caesar 组件: 遍历移位值 1~26]
2 → 每个候选明文 → [Contains 组件]
3 ↑
4 [Dictionary 组件: 加载语言词典]
5
6Contains 组件判断:匹配词数 ≥ 阈值 → Result = true5.2 词典数据结构:WordTree(前缀树)
源文件:LibSource/LanguageStatisticsLib/WordTree.cs
词典以 Trie(前缀树) 结构存储,序列化为自定义二进制格式(魔数 CT2DIC),经 GZip 压缩保存为 .dic 文件。每种语言有独立的词典文件(如 Dictionary_en.dic、Dictionary_de.dic)。
反序列化逻辑(第 39-101 行)使用栈结构重建树:
1public static WordTree Deserialize(BinaryReader reader)
2{
3 WordTree tree = new WordTree();
4
5 // 1. 验证魔数
6 string magicNo = new string(reader.ReadChars(6));
7 if (magicNo != "CT2DIC")
8 {
9 throw new Exception("File does not start with the expected magic number for word tree.");
10 }
11
12 // 2. 读取语言代码和字母表(0-terminated strings)
13 // ...
14
15 // 3. 使用栈结构重建前缀树
16 Stack<Node> stack = new Stack<Node>();
17 stack.Push(tree);
18 int symbol;
19 while ((symbol = reader.Read()) != -1)
20 {
21 readChar = (char)symbol;
22 if (readChar == Node.WordEndSymbol) // 单词结束标记
23 {
24 stack.Peek().WordEndsHere = true;
25 tree.StoredWords++;
26 continue;
27 }
28 if (readChar == Node.TerminationSymbol) // 节点终止标记
29 {
30 stack.Pop();
31 continue;
32 }
33 Node newNode = new Node() { Value = readChar };
34 stack.Peek().ChildNodes.Add(newNode);
35 stack.Push(newNode);
36 }
37 return tree;
38}5.3 匹配算法:Aho-Corasick 多模式匹配
Contains 组件(源文件:CrypPlugins/Contains/Contains.cs)支持两种匹配策略:
策略 A:Aho-Corasick 算法(默认)
Aho-Corasick 是经典的多模式字符串匹配算法 (Aho & Corasick, 1975),可以在 O(N + M + Z) 时间内同时搜索文本中的所有词典单词(N=文本长度,M=模式总长度,Z=匹配数)。它特别适合"在一段文本中同时搜索数万个词典单词"的场景。
实现位于 CrypPlugins/Contains/Aho-Corasick/StringSearch.cs,核心接口:
1public class StringSearch : IStringSearchAlgorithm
2{
3 public bool ContainsAny(string text); // 检查文本是否包含任何关键词
4 public StringSearchResult[] FindAll(string text); // 查找所有匹配结果
5 public StringSearchResult FindFirst(string text); // 查找首个匹配结果
6}TreeNode 内部类实现了 Aho-Corasick 自动机的状态节点,包含:
_failure:失败链接指针(BFS 构建)_results:该节点匹配的关键词列表_transHash:转移函数(字符到子节点的映射)
策略 B:Hashtable 逐词查找
将词典单词存入哈希表,对输入文本按分隔符分词后逐词查找。适合输入已经是分词后的文本。
搜索结构的初始化逻辑(Contains.cs 第 161-215 行):
1private void SetSearchStructure()
2{
3 if (settings.Search == ContainsSettings.SearchType.AhoCorasick)
4 {
5 stringSearch = new StringSearch(
6 dictionaryInputString.Split(settings.DelimiterDictionary[0]));
7 }
8 else if (settings.Search == ContainsSettings.SearchType.Hashtable)
9 {
10 hashTable = new Hashtable();
11 string[] theWords = dictionaryInputString.Split(settings.DelimiterDictionary[0]);
12 foreach (string item in theWords)
13 {
14 if (!hashTable.ContainsKey(item))
15 {
16 hashTable.Add(item, null);
17 }
18 }
19 }
20}5.4 判定逻辑
Contains.cs 第 337-338 行:
1HitCount = listReturn.Count; // 匹配到的词典词汇数
2Result = (HitCount >= settings.Hits); // 超过阈值即判定为"人话"5.5 局限性
字典匹配法判断准确度很高——找到真实单词就是确定性证据,不像统计方法那样含糊。但代价也明显:每种语言都需要准备一份词典,碰到缩写、专有名词、拼写变体就容易漏判。Aho-Corasick 的时间复杂度是 O(N),速度不是问题。这种方法最适合密钥空间小的场景(比如凯撒的 25 个移位值),穷举的每个结果都过一遍词典就行。
6. 番外:CaesarAnalysisHelper 的启发式频率分析
CrypTool-2 还有一个专门针对凯撒密码的组件 CaesarAnalysisHelper(源文件:CrypPlugins/CaesarAnalysisHelper/CaesarAnalysisHelper.cs),走的是完全不同的路子——它不试解密、不评分,而是直接从密文的统计特征推断密钥。
对应的模板为 Templates/Cryptanalysis/Classic/Caesar_Analysis_Using-character-frequencies.xml。
6.1 算法步骤
Step 1: 单字母频率投票(CryptoAnalysis 方法,第 83-101 行)
1Dictionary<int, int> KeyList = new Dictionary<int, int>();
2int Counter = 0;
3foreach (int i in CountChars(frequencyList.ToLower()))
4{
5 if (Counter < 5)
6 {
7 if (!KeyList.ContainsKey(i))
8 {
9 KeyList.Add(i, 5 - Counter); // 加权投票:排名第 1 权重 5,...
10 }
11 else
12 {
13 KeyList[i] += 5 - Counter;
14 }
15 Counter++;
16 }
17}思路很直接:如果密文中出现最多的字母是 H,而英语中出现最多的字母是 E(由 settings.FrequentChar 配置),那密钥很可能是 H - E = 3。
CountChars 方法(第 135-188 行)解析频率列表,计算每个高频字母与目标语言最高频字母的偏移量。
Step 2: 二元组(Bigram)频率投票(CountBigrams 方法,第 190-257 行)
1// 硬编码的德语高频 bigram
2string[] Bigrams = new[] { "er", "en", "ch", "de" };关键在于:凯撒加密保持 bigram 内部两个字母的距离不变。例如 "er" 中 r - e = 13,加密后变为 "hu"(u - h = 13)。通过比对距离可以快速匹配:
1// 第 232 行:利用字符距离不变的特性进行匹配
2} while (!(CurrentBigramm[1] - CurrentBigramm[0] == s[1] - s[0]));找到匹配后,通过 s[0] - CurrentBigramm[0] 推算密钥(第 236 行)。
Step 3: 综合投票(第 120-132 行)
1IOrderedEnumerable<int> items = (from k in KeyList.Keys
2 orderby KeyList[k] descending
3 select k);
4List<int> ResultList = new List<int>();
5foreach (int i in items)
6{
7 ResultList.Add(i);
8}
9if (ResultList.Count > 0)
10{
11 key = ResultList[0]; // 最高分即为最可能的密钥
12}6.2 方法评价
这个方法不评估明文质量,而是跳过试解密直接猜密钥。好处是不需要遍历所有候选明文。坏处也很明显:密文太短的话频率统计不够稳定,猜出来的结果可能不靠谱。
另外值得注意的是,CountBigrams 里硬编码了德语的高频 bigram("er", "en", "ch", "de"),所以这个组件对德语有偏向。要分析其他语言的密文,得换成对应语言的高频 bigram。
7. 整体架构
把上面这些拼到一起看,CrypTool-2 的语言识别系统大致分四层:
1┌─────────────────────────────────────────────────┐
2│ 应用层(密码分析器) │
3│ Caesar Brute Force / Vigenere / Enigma / ... │
4├─────────────────────────────────────────────────┤
5│ 评估接口层 (IControlCost) │
6│ CostFunction (IOC | Entropy | NGramsLog2 | RegEx)│
7│ RelationOperator: LargerThen / LessThen │
8├─────────────────────────────────────────────────┤
9│ 语言统计引擎 (LanguageStatisticsLib) │
10│ Grams 类族 (1~6 gram) │ WordTree (Trie 词典) │
11│ LanguageStatistics │ CalculateIoC │
12├─────────────────────────────────────────────────┤
13│ 数据层 │
14│ CTLS 格式 N-gram 频率文件 (.bin.gz) │
15│ CT2DIC 格式词典文件 (.dic) │
16│ 硬编码 Unigram 频率表(15种语言,来源 Wikipedia) │
17└─────────────────────────────────────────────────┘接口设计
IControlCost 接口(CrypPluginBase/Control/IControlCost.cs)是代价函数体系的公共契约:
1public enum RelationOperator
2{
3 LessThen, LargerThen
4}
5
6public interface IControlCost : IControl
7{
8 RelationOperator GetRelationOperator(); // 值越大越好,还是越小越好?
9 double CalculateCost(byte[] text); // 计算代价值
10 int GetBytesToUse();
11 int GetBytesOffset();
12}密码分析器不直接依赖具体的评分算法,而是面向 IControlCost 接口编程。要换一种评分方式,改一下 CostFunctionSettings.CostFunctionType 的枚举值就行,分析器的代码不用动。
代价函数类型枚举
1// CostFunctionSettings.cs 第 27-33 行
2public enum CostFunctionType
3{
4 IOC = 0, // 重合指数
5 Entropy = 1, // 信息熵
6 NGramsLog2 = 2, // N-gram 对数概率(核心方法)
7 RegEx = 3 // 正则表达式匹配
8}8. 各方法比较
| 方法 | 数学基础 | 时间复杂度 | 额外数据需求 | 区分精度 | 最佳场景 |
|---|---|---|---|---|---|
| IoC | 离散概率论 | O(N) | 无 | 低 | 快速粗筛;密钥长度估计 |
| Entropy | 信息论 | O(N) | 无 | 低 | 快速粗筛 |
| N-gram | 统计语言模型 | O(N) | 大型 N-gram 频率表 | 高 | 所有密码分析场景的首选 |
| 字典匹配 | 精确字符串匹配 | O(N+M+Z) | 语言词典 | 极高 | 小密钥空间的穷举搜索 |
| 频率推断 | 频率分析启发式 | O(N) | 目标语言频率先验 | 中 | 凯撒/单表替换的快速破解 |
实际用哪种?
凯撒密码密钥空间只有 25 个,CrypTool-2 提供了两种现成模板:
- 字典匹配法(
Caesar_ExhaustiveKeySearch.xml):25 个移位值逐个试,每个结果过一遍 Aho-Corasick 字典检测,匹配到足够多的词就算找到了。判断结果是确定性的。 - 频率直接推断(
Caesar_Analysis_Using-character-frequencies.xml):用CaesarAnalysisHelper统计密文频率直接算出密钥,不需要遍历候选项,速度最快。
碰到更复杂的密码(Vigenere、Enigma、Playfair 之类),密钥空间大到没法穷举,这时候 N-gram 评分法就派上用场了——它充当爬山法、遗传算法、模拟退火等搜索算法的适应度函数(fitness function),引导搜索往"更像人话"的方向走。
9. 跟 LLM 比一下
| 维度 | 传统统计方法(CrypTool-2) | LLM 方法 |
|---|---|---|
| 判断依据 | 字母组合的频率统计规律 | 深层语义理解 |
| 数据需求 | MB 级频率表 + 词典 | GB 级模型参数 |
| 计算速度 | 微秒级(单次评分) | 毫秒~秒级 |
| 语义理解 | 无(纯统计) | 有(理解含义) |
| 可解释性 | 完全透明 | 黑盒 |
| 多语言扩展 | 需要每种语言的频率数据 | 训练数据覆盖即可 |
| 离线可用 | 完全离线 | 通常需要推理服务 |
传统方法不需要"理解"语言说了什么,只要抓住语言的统计指纹就够了。这个思路从 Friedman (1922) 和 Shannon (1948) 一直沿用到现在。
换个角度看,N-gram 方法其实就是一个马尔可夫链语言模型——假设当前字母出现的概率只取决于前面 N-1 个字母。Ravi 和 Knight (2008) 的工作已经表明,哪怕是低阶的字母 N-gram 模型也能有效破解替换密码。这跟现代 NLP 里的语言模型在数学上是同源的,只是规模差了几个数量级。CrypTool-2 的 5-gram 评分,说到底就是一个 5 阶马尔可夫链 LM——简陋,但在密码分析这个场景下够用了。
10. 结论
回过头来看,CrypTool-2 这套东西回答的问题其实很朴素:自然语言不是随机的,它的字母分布、字母组合、词汇构成都有规律可循。抓住这些规律,就能把"人话"和乱码分开。
几种方法各有各的粒度:IoC 和 Entropy 只看单个字母出现频率够不够"歪";N-gram 评分看的是字母序列的组合是否符合语言习惯;字典匹配最直接,找到真实单词就是铁证。
这些方法到今天也没过时。它们跑一次只要微秒级,完全确定性,不需要 GPU,可以塞进搜索循环里反复调用。密码分析动不动就要评估数十亿个候选密钥,这种场景下 N-gram 评分仍然是主流做法。
附录:关键源文件索引
| 文件路径 | 核心功能 |
|---|---|
CrypPlugins/CostFunction/CostFunction.cs | 四种代价函数的统一入口(IoC/Entropy/NGram/RegEx) |
CrypPlugins/CostFunction/CostFunctionSettings.cs | 代价函数配置(类型/语言/N-gram大小) |
CrypPluginBase/Control/IControlCost.cs | 代价函数接口定义(RelationOperator/CalculateCost) |
LibSource/LanguageStatisticsLib/Grams/Grams.cs | N-gram 评分抽象基类(模板方法模式) |
LibSource/LanguageStatisticsLib/Grams/Pentagrams.cs | 5-gram 评分实现(CT2 默认) |
LibSource/LanguageStatisticsLib/Grams/Tetragrams.cs | 4-gram 评分实现 |
LibSource/LanguageStatisticsLib/LanguageStatistics.cs | 语言统计门面类(15种语言字母表 + Unigram 频率) |
LibSource/LanguageStatisticsLib/LanguageStatisticsFile.cs | CTLS 文件格式解析(Magic: CTLS + GZip + BlockCopy) |
LibSource/LanguageStatisticsLib/WordTree.cs | 前缀树词典数据结构(Magic: CT2DIC) |
CrypPlugins/CaesarAnalysisHelper/CaesarAnalysisHelper.cs | 频率推断法直接推算密钥 |
CrypPlugins/Contains/Contains.cs | Aho-Corasick / Hashtable 词典匹配 |
CrypPlugins/Contains/Aho-Corasick/StringSearch.cs | Aho-Corasick 自动机实现 |
Util/LanguageStatisticsGenerator/Program.cs | N-gram 频率数据生成器(Gutenberg/Wikipedia) |
Templates/Cryptanalysis/Classic/Caesar_ExhaustiveKeySearch.xml | 凯撒穷举攻击模板(字典匹配法) |
Templates/Cryptanalysis/Classic/Caesar_Analysis_Using-character-frequencies.xml | 凯撒频率分析模板 |
参考文献
Aho, A. V., & Corasick, M. J. (1975). Efficient string matching: An aid to bibliographic search. Communications of the ACM, 18(6), 333–340. https://doi.org/10.1145/360825.360855
Friedman, W. F. (1922). The index of coincidence and its applications in cryptography (Riverbank Publication No. 22). Riverbank Laboratories.
Kopal, N. (2018). Solving classical ciphers with CrypTool 2. In Proceedings of the 1st International Conference on Historical Cryptology (HistoCrypt 2018) (Vol. 149, pp. 29–38). Linköping University Electronic Press.
Kopal, N., & Esslinger, B. (2022). New ciphers and cryptanalysis components in CrypTool 2. In Proceedings of the International Conference on Historical Cryptology (HistoCrypt 2022) (pp. 127–136). Linköping University Electronic Press.
Ravi, S., & Knight, K. (2008). Attacking decipherment problems optimally with low-order n-gram models. In Proceedings of the 2008 Conference on Empirical Methods in Natural Language Processing (pp. 812–819). Association for Computational Linguistics.
Shannon, C. E. (1948). A mathematical theory of communication. Bell System Technical Journal, 27(3), 379–423. https://doi.org/10.1002/j.1538-7305.1948.tb01338.x