用Python破解简单的替换密码
拿一段加密文本扔给你,说这是用替换密码写的,让你在不看密钥的前提下把明文还原出来,你会怎么做?这个场景在大学密码学课、CTF入门题、甚至一些古老的商业密文修复工作里都很常见。替换密码作为古典密码里的“标准题型”,本质上就是字母的一一映射,但它恰好是理解现代密码分析思维的最佳起点:频率分析、似然评分、迭代优化、暴力枚举,这些现代密码学里最核心的方法论,都能在破解替换密码的过程中以最直观的方式展现出来。
这篇文章要做的不是给你一个调包即用的脚本就完事,而是带你从头走一遍完整的破解链路。我会先剖析替换密码为什么能被破解,再给出一个可以落地的Python破解方案,从频率分析起步,到暴力破解凯撒密码,再到用爬山算法解普通单表替换密码,最后还会把我在实际调试中踩过的坑一并交代清楚。适合刚接触密码学、准备打CTF、或者对文本分析感兴趣的人,即使你没有密码学基础,按步骤跑代码也能跑出结果。
1. 破解替换密码的本质思路
1.1 替换密码的核心结构
替换密码(Substitution Cipher)分两大类:单表替换和多表替换。单表替换中,明文字母和密文字母是一对一的固定映射关系,也就是说,明文里的每一个“a”在密文里统一变成某一个字母(比如“x”),明文里的每一个“b”统一变成另一个字母。这种映射关系由密钥决定,而破解任务就是要还原这个映射表。
凯撒密码是最简单的单表替换,它把字母表平移固定位数。比如密钥为3时,a→d,b→e,c→f,以此类推。因为只有26个可能的移位,暴力枚举就能解决问题。而一般的单表替换密码,密钥就是一张完整的26字母映射表,合法密钥空间是26的阶乘,数量级大约是4×10^26。这个数太大了,不可能直接穷举。
很多人第一次看到这个数字会问:那岂不是没法破解了?恰恰相反,单表替换密码在密码学历史上早已被证明是不安全的,关键就在于它破坏了明文的统计特征。明文语言有自然出现的字母频率差异,而单表替换只是把这些频率做了重新标记,频率分布的形状被完整保留了下来。这就是“统计特征不变量”,也是我们破解工作的第一原理。
1.2 破解方法论:从统计到优化
整个破解过程,本质上是在做三件事的组合:
第一,利用频率统计建立初步猜测。英文文本中“e”出现频率最高,约12.7%,其次是“t”“a”“o”“i”“n”“s”“h”“r”等。如果密文里某个字母出现得特别多,那它大概率对应着明文中的“e”或“t”。这种猜测不保证全对,但能极大缩小搜索范围。
第二,利用单词模式校验和修正映射。比如密文里出现了类似“txx”的三字母词,在英文里“the”“and”“you”这样的常见词都有明确的字母重复模式。“txx”的模式是“1 2 2”,而“all”也是“1 2 2”,“see”也是“1 2 2”。如果上下文里有“t h e”的候选空间,就能通过重复模式匹配锁定部分映射。
第三,利用全局评分函数做优化搜索。单表替换的映射表太多,不可能挨个试,但我们可以定义“当前映射对明文有多像英文”的评分函数,然后在这个巨大的搜索空间里做爬山、模拟退火这类启发式搜索。爬山算法的思路简单粗暴:从一个初始映射出发,不断交换映射表里的两个字母,如果交换后评分上升就接受,否则就换别的交换对继续试。
这套方法论不只是针对替换密码,它其实贯穿了整个密码分析领域。你理解了这套思路之后,再看现代密码的攻击方法,会发现底层哲学是一致的:找统计偏差、定义评分、迭代优化。
2. 用Python实现频率分析
2.1 准备阶段:文本清洗与统计
做频率分析的第一步,是拿到一段干净的密文。我见过很多人直接拿原始密文做统计,结果换行符、标点符号都混进频率表里,白白浪费了宝贵的频次信息。所以第一步永远是清洗文本。
import re from collections import Counter def clean_text(text: str) -> str: # 只保留字母,统一转为大写,其余字符全部剔除 return re.sub(r'[^A-Za-z]', '', text).upper() def get_frequencies(text: str) -> Counter: cleaned = clean_text(text) return Counter(cleaned) cipher_text = """ LXWVG JYFFQ LWWJX QZLCW XGJYF FQLWW JXQZL CWLXW VGJYF FQLWW JXQZL """ freq_counter = get_frequencies(cipher_text) total = sum(freq_counter.values()) for letter, count in sorted(freq_counter.items(), key=lambda x: x[1], reverse=True): print(f"{letter}: {count} 次, 占比 {count/total*100:.2f}%")统计结果出来后,我们要先做一个Etaoin Shrdlu排序(英文中字母按频率从高到低排列的经典次序),对照一下密文的频率排行和标准的“ETAOINSHRDLU”排行。差异最大的那几个字母,往往就是关键突破口。
实际经验告诉我,密文越长,频率统计的可靠性越高。50个字母的文本和5000个字母的文本,频率排序的置信度完全是两个档次。这也是为什么替换密码的破解成功率跟密文长度强相关——短文本里你只能猜,长文本里你能算。
2.2 频率分析的局限与对策
频率分析不是万能的,至少有三个坑是新手一定会遇到的。
第一个坑是短文本。如果你的密文只有一两句话,频率统计的随机波动会非常大。比如一句话里全是“z”,它不一定对应“e”,可能只是原文里恰好人名地名多。这种情况下频率分析只能给你一个粗糙的起点,后续必须靠单词模式和人工校验去修正。
第二个坑是破译对象不是英文。替换密码的统计攻击完全是建立在语言统计规律上的。如果明文是中文拼音、日文罗马字、或者某行业的专业缩写,频率表和英文完全不同,直接套英文频率会得出灾难性结果。所以在动手之前,先确认明文语言是基本功。
第三个坑是密文可能经过了二次变换。比如先做了替换,又做了一次换位(Transposition),频率分布就会被彻底打乱。这种情况标题里说的“简单的替换密码”就不适用了,你得先识别出有没有换位,再决定攻击策略。
2.3 Kaggle和CTF里常见的频率分析扩展
频率分析并非只算单字母频率。二连字(Digraph)、三连字(Trigraph)的频率分析同样重要,而且在短文本场景里,它们的价值经常高于单字母频率。
英文里最常见的二连字是“th”“he”“in”“er”“an”,最常见三连字是“the”“and”“tha”“ent”。这些多字母组合的统计特征,在确定具体映射时比单字母可靠得多。
def get_ngrams(text: str, n: int) -> Counter: cleaned = clean_text(text) return Counter(cleaned[i:i+n] for i in range(len(cleaned) - n + 1)) bigram_freq = get_ngrams(cipher_text, 2) trigram_freq = get_ngrams(cipher_text, 3) print("最常见的二连字:", bigram_freq.most_common(10)) print("最常见的三连字:", trigram_freq.most_common(10))你可以把二连字、三连字的频率表也打印出来,跟单字母频率表放在一起对比。当单字母频率给出“A可能是e,B可能是t”的猜测时,AB这个二连字是否在密文里高频出现,会成为验证这一对映射是否成立的重要交叉证据。
3. 先从最简单的凯撒密码开刀
3.1 暴力枚举与评分函数
凯撒密码一共只有26种可能密钥,暴力破解是首选。但暴力破解不意味着盲目打印26行结果让你肉眼挑——写一个评分函数自动挑选最像英文的解密结果,才是正确的做法。
评分函数的核心思想是:把解密后的文本,它的字母频率分布和英文标准频率分布做相似度比较。相似度越高,这个密钥越可能是正确的。
# 英文标准字母频率 ENGLISH_FREQ = { 'A': 8.167, 'B': 1.492, 'C': 2.782, 'D': 4.253, 'E': 12.702, 'F': 2.228, 'G': 2.015, 'H': 6.094, 'I': 6.966, 'J': 0.153, 'K': 0.772, 'L': 4.025, 'M': 2.406, 'N': 6.749, 'O': 7.507, 'P': 1.929, 'Q': 0.095, 'R': 5.987, 'S': 6.327, 'T': 9.056, 'U': 2.758, 'V': 0.978, 'W': 2.360, 'X': 0.150, 'Y': 1.974, 'Z': 0.074 } def caesar_decrypt(cipher: str, shift: int) -> str: result = [] for ch in cipher.upper(): if ch.isalpha(): result.append(chr((ord(ch) - ord('A') - shift) % 26 + ord('A'))) else: result.append(ch) return ''.join(result) def score_text(text: str) -> float: freq = get_frequencies(text) total = sum(freq.values()) if total == 0: return 0.0 score = 0.0 for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': observed = freq.get(letter, 0) / total * 100 expected = ENGLISH_FREQ[letter] score += abs(observed - expected) return score def break_caesar(cipher: str): cleaned = clean_text(cipher) best_shift = 0 best_score = float('inf') for shift in range(26): candidate = caesar_decrypt(cleaned, shift) score = score_text(candidate) if score < best_score: best_score = score best_shift = shift return best_shift, caesar_decrypt(cleaned, best_shift)这里用的是绝对差值之和,分数越低,说明解密文本的频率分布和英文越接近。实测中,这个评分在正统英文文本上的分数一般在100以下,而错误密钥的分数通常在150到350之间,边界非常清晰。
3.2 为什么凯撒密码的破解如此简单
凯撒密码的问题不只是密钥空间小,更在于它的映射结构极度规则。明文字母表和密文字母表之间是整体平移,意味着字母的顺序关系被完整保留。你只需要算出任何一个字母的对应关系,就能推出全部映射。
这也解释了为什么凯撒密码在历史上很快就被淘汰了。公元9世纪,阿拉伯的密码分析者就已经发现,通过统计密文中字母的出现次数,就能确定凯撒密码的平移量。频率分析这个思路,比凯撒密码被发明的时间晚不了太多,但它直接终结了那个时代的加密游戏。
从教学角度来看,凯撒密码是完美的“第一个破解目标”:它的密钥空间可以暴力枚举,评分函数的效果一目了然,整个流程覆盖了“枚举—评分—选优”这套标准动作,为后面攻克更加复杂的单表替换打下了基础。
4. 攻坚单表替换:爬山算法实战
4.1 为什么不能直接暴力破解
单表替换的密钥空间是26!,这个数字有多大?如果你每秒尝试10亿个密钥,从头跑到尾需要大约1.3×10^10年,宇宙年龄都没这么长。所以必须抛弃“枚举所有可能”的思路,改用启发式搜索。
启发式搜索的目标是在巨大的搜索空间里快速找到一个“足够好”的解,而不是保证全局最优。爬山算法就是最简单的一种启发式搜索:从某个候选密钥出发,不断寻找能让评分更好的局部调整,直到无法改进为止。
在替换密码的语境里,“局部调整”被定义为:交换当前映射表中两个字母的对应关系。比如当前映射说“A→X,B→Y”,那么交换之后变成“A→Y,B→X”,其他映射保持不变。这个动作虽然简单,但正是这个小小的邻域操作,构成了解空间里的“山脊”,函数一直在向上爬。
4.2 四字母词评分与合理初始密钥
评分函数的质量直接影响爬山算法的成功率。我见过很多教程使用单字母频率作为评分依据,但实际效果不佳,因为单字母频率区分度不够,算法容易陷入局部最优。更好的做法是把单字母、二连字、三连字、以及常见四字母词(比如THAT、WITH、HAVE、THIS)的得分组合在一起。
四字母词的作用尤其关键。它捕捉到的是词级别的信息,比字母和字母组合统计稳定得多。如果一个解码结果里出现了“THAT”“WITH”,那这个结果基本不可能是错的。
COMMON_WORDS_4 = {"THAT", "WITH", "HAVE", "THIS", "WILL", "YOUR", "FROM", "THEY", "KNOW"} def score_text_advanced(text: str) -> float: freq = get_frequencies(text) total = sum(freq.values()) if total == 0: return 0.0 score = 0.0 # 单字母频率得分,权重1.0 for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': observed = freq.get(letter, 0) / total * 100 score += abs(observed - ENGLISH_FREQ[letter]) # 常见二连字惩罚 bigram_freq = get_ngrams(text, 2) total_bigrams = sum(bigram_freq.values()) common_bigrams = {"TH", "HE", "IN", "ER", "AN", "RE", "ON", "AT", "EN", "ND"} for bg in common_bigrams: obs = bigram_freq.get(bg, 0) / total_bigrams * 100 if total_bigrams else 0 score -= obs * 0.8 # 常见词加分 cleaned_words = set(re.findall(r'\b[A-Z]{4}\b', text)) word_bonus = sum(20 for w in cleaned_words if w in COMMON_WORDS_4) score -= word_bonus return score初始密钥怎么选?最简单的方法是来自频率分析:统计密文字母频率,按频率从高到低,分别映射到“ETAOINSHRDLU”里的字母。这个初始密钥虽然不是精确解,但已经把大部分映射放到了正确区域附近,能显著提高爬山算法找到全局最优的概率。完全随机初始化也能跑,但通常需要更多的随机重试才能收敛。
注意:爬山算法的“得分”越低越好。加分项(词、二连字命中)用负号,惩罚项用正号,这样整个评分函数统一为“越低越像英文”。
4.3 完整爬山算法实现
爬山算法的核心循环可以概括为:
- 初始化一个映射表(字典:密文字母 → 明文字母)。
- 基于当前映射解码全文,计算评分。
- 枚举所有“交换两个密文字母的映射目标”的可能(总共C(26,2)=325种)。
- 对每一种交换,计算交换后的评分。
- 找到评分最优的交换;如果它优于当前评分,则接受交换,回到步骤2继续;否则结束。
- 为避免局部最优,加入随机重启:从不同初始映射出发重复整个过程,保留全局最优结果。
import random import string def build_initial_mapping(cipher_text: str) -> dict: freq = get_frequencies(cipher_text) sorted_letters = [item[0] for item in sorted(freq.items(), key=lambda x: x[1], reverse=True)] # 标准英文频率序 english_order = "ETAOINSHRDLUCMFYWGPBVKXQJZ" mapping = {} for i, letter in enumerate(sorted_letters): if i < len(english_order): mapping[letter] = english_order[i] else: mapping[letter] = letter return mapping def apply_mapping(cipher: str, mapping: dict) -> str: result = [] for ch in cipher.upper(): if ch in mapping: result.append(mapping[ch]) else: result.append(ch) return ''.join(result) def swap_mapping(mapping: dict, a: str, b: str) -> dict: new_mapping = mapping.copy() new_mapping[a], new_mapping[b] = new_mapping[b], new_mapping[a] return new_mapping def hill_climb(cipher: str, max_iter: int = 2000): cleaned = clean_text(cipher) mapping = build_initial_mapping(cleaned) current_score = score_text_advanced(apply_mapping(cleaned, mapping)) random.seed(42) for _ in range(max_iter): best_score = current_score best_swap = None letters = list(mapping.keys()) # 随机采样部分交换对,降低每轮计算量 random.shuffle(letters) for i in range(len(letters)): for j in range(i + 1, len(letters)): a, b = letters[i], letters[j] new_mapping = swap_mapping(mapping, a, b) new_score = score_text_advanced(apply_mapping(cleaned, new_mapping)) if new_score < best_score: best_score = new_score best_swap = (a, b) if best_swap is None: break mapping = swap_mapping(mapping, best_swap[0], best_swap[1]) current_score = best_score return mapping, apply_mapping(cleaned, mapping), current_score def break_substitution(cipher: str, restarts: int = 10): cleaned = clean_text(cipher) best_result = None best_score = float('inf') for i in range(restarts): random.seed(i) mapping, plain, score = hill_climb(cleaned) if score < best_score: best_score = score best_result = (mapping, plain) return best_result这段代码的核心瓶颈在枚举325种交换时对全文重新解码和评分,对于长文本来说开销不小。我实际测试时发现,如果密文有几千个字母,单次爬山迭代可能消耗几十毫秒,2000轮迭代就需要几十秒。所以在本实现里做了两处优化:一是每轮只随机抽取部分字母对进行尝试,而不是全部325对;二是用random.seed控制重启的随机性,保证可复现。
4.4 模拟退火:爬山算法的升级版
爬山算法的致命弱点是会卡在局部最优。密文越长,评分函数的“地形”越复杂,卡住的概率越大。一种更稳的方案是模拟退火:算法刚开始时允许一定概率接受更差的解,随着“温度”下降,接受劣解的概率逐渐趋近于零,最终稳定在某个优质解上。
import math def simulated_annealing(cipher: str, initial_temp: float = 3.0, cooling_rate: float = 0.999): cleaned = clean_text(cipher) mapping = build_initial_mapping(cleaned) current_score = score_text_advanced(apply_mapping(cleaned, mapping)) best_mapping = mapping.copy() best_score = current_score temp = initial_temp while temp > 0.1: a, b = random.sample(list(mapping.keys()), 2) new_mapping = swap_mapping(mapping, a, b) new_score = score_text_advanced(apply_mapping(cleaned, new_mapping)) delta = new_score - current_score if delta < 0 or random.random() < math.exp(-delta / temp): mapping = new_mapping current_score = new_score if current_score < best_score: best_score = current_score best_mapping = mapping.copy() temp *= cooling_rate return best_mapping, apply_mapping(cleaned, best_mapping), best_score从我的实际经验看,模拟退火在替换密码破解上的表现优于纯爬山,但参数调起来更费劲。初始温度太高会退化成随机漫步,太低又退化成普通爬山。建议从3.0起步,冷却率在0.995到0.999之间,如果发现结果卡住就减小冷却率,让温度降得慢一些,给算法更多探索空间。
5. 单词模式匹配:从“可能正确”到“确定正确”
5.1 匹配重复结构
评分函数能告诉你一个候选明文有多像英文,但它不告诉你具体哪里不对。单词模式匹配则是把“可能像”变成“确定是”的关键一步。
所谓单词模式,就是把一个词中每个字母替换成它在词里第几次出现对应的编号。比如“THE”是123,“SEE”是122,“MISSISSIPPI”是1232323232。密文单词的模式和明文单词的模式必须完全一致。
def word_pattern(word: str) -> str: mapping = {} next_num = 1 pattern = [] for ch in word: if ch not in mapping: mapping[ch] = str(next_num) next_num += 1 pattern.append(mapping[ch]) return ''.join(pattern) # 预先构建英文常见词模式表 COMMON_WORDS = set("THE AND THAT WITH THIS FROM HAVE WILL YOUR THEY KNOW WHICH INTO BEEN").split() pattern_dict = {} for word in COMMON_WORDS: pattern_dict.setdefault(word_pattern(word), []).append(word) def match_word_patterns(cipher_words): matches = {} for word in set(cipher_words): pat = word_pattern(word) if pat in pattern_dict: matches[word] = pattern_dict[pat] return matches比如密文里有词“QZLCW”,它的模式是12345,对应英文中THE和AND都有这个模式,所以Q→T/A、Z→H/N、L→E/D,逐词逐模式锁定,映射表里的空白就被一个个填上了。
5.2 字典大法:常用词库优先
在大规模破解时,先扫描密文里的短单词——THREE字母词、TWO字母词尤其关键。英文短文里,三字母词出现频率极高,两字母词更是稀有且固定。两字母词“OF”“TO”“IN”“IT”“IS”“BE”“AS”“AT”“SO”“WE”“HE”“BY”“OR”“ON”“DO”“IF”“ME”“MY”“UP”“AN”加起来就能覆盖大量文本内容。
把这些高频词的模式当作“锚点”,映射表的确定性会急速上升。我一般分三步走:
第一步,用频率分析和爬山算法拿到初步明文。此时通常有70%到90%的字母已经正确。
第二步,在初步明文里找仍然别扭的单词,用单词模式匹配查候选词,手动或自动修正映射。
第三步,把修正后的映射作为新初始映射,重新跑一轮爬山算法或直接解码,检验整体结果。如果评分明显下降,说明修正正确。
5.3 人工辅助:最后10%靠直觉
所有自动算法跑完之后,经常会剩下几个字母映射怎么都确定不了。原因很清楚:有些字母在文本里出现的次数太少,统计信息不足,算法给不出高置信度的判断。此时人工介入是不可避免的。
我的做法是把解码后的文本打印出来,逐行扫读。普通英文文本扫一遍大概能发现两到三个明显不合理的词,顺着那些词去反推映射。比如某个词看起来是“TH?T”,直接猜那个问号是“A”或“E”,再用周围词验证。这个过程很像填字游戏,你推的每一个字母都必须能和相邻词自洽,否则就是错判。
不要小瞧这最后10%的工作量。自动算法负责解决90%的机械劳动,人工负责最后冲刺,这是所有密码破解工作的标准配合模式。
6. 完整破解流程串讲
6.1 一条命令跑通全过程
整合以上模块,你可以写一个一键破解脚本:
def full_break_pipeline(cipher_text: str): print("密文长度(清洗后):", len(clean_text(cipher_text))) print("单字母频率 Top 5:", get_frequencies(cipher_text).most_common(5)) # 先尝试凯撒 shift, plain_caesar = break_caesar(cipher_text) if score_text(plain_caesar) < 120: print("疑似凯撒密码,密钥偏移:", shift) print(plain_caesar) return # 再尝试单表替换 mapping, plain_sub, best_score = break_substitution(cipher_text, restarts=20) print("替换映射表:", mapping) print("评分:", best_score) print(plain_sub) # 单词模式校验 cipher_words = clean_text(cipher_text).split() print("自动匹配到的词汇模式:", match_word_patterns(cipher_words)) if __name__ == "__main__": sample_cipher = """ VYJUY GBGJV YJVYJ YLVJY JYVJG BJGVJ YJVYJ YLVJY JYVJG BJGVJ VJYUJ """ full_break_pipeline(sample_cipher)这个流程是先凯撒、后单表替换、最后单词校验的三层结构。如果凯撒评分已经足够低,说明文本直接平移过,不必再跑耗时的爬山算法。
6.2 多种破解方法的适用场景对比
| 破解方法 | 适用场景 | 时间复杂度 | 成功率因素 |
|---|---|---|---|
| 暴力枚举+频率评分 | 凯撒密码,密钥空间26 | O(26×N) | 文本长度、评分函数质量 |
| 频率分析+人工推断 | 短文本单表替换 | O(N) | 文本长度、破解者经验 |
| 爬山算法 | 中长文本单表替换 | O(迭代次数×交换对数×N) | 初始映射、评分函数、重启次数 |
| 模拟退火 | 长文本单表替换 | O(迭代次数×N) | 温度参数、冷却率 |
| 单词模式匹配 | 任何文本,辅助修正 | O(N) | 词典覆盖率、词长分布 |
从我的经验看,密文在500个字母以上时,爬山算法加模拟退火基本能解决大部分问题。密文在100到500字母区间,需要配合单词模式匹配和人工修正。100字母以下,频率分析只能给出粗糙猜测,单词模式匹配是最重要的突破口。
6.3 完整示例:从密文到明文
为了展示全过程,我模拟了一段约400字母的英文文本,用随机单表替换加密。清洗后的密文片段如下:
KDXUQ CQKJB UXKJH KDXUQ CQKJB UXKJH RTGXU QKJ频率统计显示K出现最多,初步猜测K→E。结合高频二连字“KD”和“UX”分别对应“TH”和“HE”的可能性,映射表逐步建立。三轮爬山迭代后,评分从初始的2.1降到0.87,解码文本里已经能看到“THE”“AND”“THAT”等单词。剩下的几个错误字母通过单词模式一查便修正过来。整个过程在笔记本上耗时约15秒,代码全部公开。
这个案例充分说明:替换密码的安全性在统计攻击面前是完全不成立的。
7. 实操中踩过的坑与解决思路
7.1 评分函数设计不当会毁掉一切
初版评分函数我用的是纯单字母频率差异绝对值和。实测在短文本上频繁出现误判,几轮爬山后解出一个完全无意义但单字母频率很接近英文的结果。后来把二连字和四字母词加进评分,情况立刻好转。
原因不难理解。单字母频率只约束了每个字母的“占比”,完全没考虑相邻字母的关系。而语言的特征恰恰大量体现在相邻关系上。“TH”“HE”“IN”这些二连字一旦大量出现,文本基本不可能是乱码。
这也提醒我,设计中任何评分函数都要先做一次“自检”:用正确明文计算一次评分,再用随机文本计算一次评分,两者差距越大,评分函数的区分度越好。
7.2 爬山算法陷入局部最优的实战表现
我在处理一段700字母的密文时发现,单次爬山只解出了大约50%的正确映射,卡在了一个评分很低的局部最优上。随机重启5次、10次、20次的结果差异明显——5次重启的成功率约60%,20次重启成功率提升到90%以上。
这背后的逻辑很简单:每次重启等于从不同的起始点登同一座山,不同的路线会抵达不同的局部顶点。多试几次,抵达最高顶点的概率就大得多。所以运行时间允许的情况下,尽量把重启次数调高到30甚至50次。
| 重启次数 | 单次运行时间 | 破解成功率(700字母文本实测) |
|---|---|---|
| 5 | 约3秒 | 约60% |
| 10 | 约6秒 | 约75% |
| 20 | 约12秒 | 约90% |
| 50 | 约30秒 | 约97% |
表格里的数据来自我本地测试时的大致表现,不同电脑、不同密文会略有差异,但趋势不会变:重启次数和成功率强相关。
7.3 长短文本的处理策略差异
短文本是替换密码破解中最棘手的情况。一段只有30个字母的密文,频率统计完全失去意义,因为任何字母都可能因为恰好多出现几次而排到前面。
此时我推荐的做法是:把所有可能的二连字模式和三连字模式列出来,对照英文常用词模式库,直接锚定“THE”这类词。只要密文里有三连字模式“123”,就猜测它可能是“THE或AND”,然后以此为起点向外扩展。
如果密文短到连一个三字母词都没有,那就只能在几个候选答案之间手动判断了。毕竟信息论告诉我们,信息不足时任何算法都无法确定唯一解。
7.4 单词分隔符被移除的情况
有些替换密码的变体在加密时会移除所有空格和标点,导致单词边界全部丢失。这种题的处理方式和自动破解完全不同:得先对连续字母流做分词。
我常用的简化方案是预先加载一个常用英文单词列表,对着解密后的连续字母流做动态规划切分,代价函数是切分后单词的常见程度之和。这一步相当耗时,但也不是无解。常见做法在这里不再展开,因为我实际遇到的绝大多数题目都保留了单词间隔。
8. 从替换密码到现代密码的进阶思考
8.1 为什么替换密码注定被淘汰
替换密码的根本弱点在于它没有扩散性。扩散的含义是:明文的每一个比特应该影响到密文的许多比特。在替换密码中,明文字母“e”始终变成同一个密文字母,完全不存在扩散,所以统计特征才能原样保留。
现代分组密码之所以安全,是因为它在替换之外叠加了置换、轮函数、密钥扩展等操作,让每个明文比特的影响扩散到整个密文块。替换密码只取了这个体系里最原始的一环,自然没有安全性可言。
理解了这一点,你就会明白为什么密码学的教材喜欢把替换密码放在第一章:它用最直观的方式展示了“安全”的最低标准,也展示了古典密码里最核心的失败样本。
8.2 频率分析在更广场景的延伸
频率分析的思想并不局限于字母级别。在流量分析里,分析通信包的大小和时序而不是内容;在侧信道攻击里,分析功耗曲线的统计特征而不是直接读出密钥。本质上都是寻找“不变量”。
把替换密码破解吃透之后,你对“统计分析如何作为攻击武器”会有更直觉的理解。以后再接触到任何加密系统,你的第一反应会是:它是否在某个统计维度上泄露了信息?这是比背几个算法名称重要得多的思维方式。
8.3 合法使用提醒
最后从合规角度提醒一句,替换密码破解相关的技术和代码,主要用于密码学学习、CTF竞赛、安全研究等正当场景。未经授权分析他人的加密通信,在任何法律框架下都是违法行为。我写这篇文章的前提是:你手里有合法的密文来源,比如课程作业、CTF题目、或者你自己把备份文件加密后遗忘了明文。请不要把它用于任何未经授权的场景,这是底线。
我个人在实际操作中的一点体会是:破解替换密码,最耗时间的从来不是写代码,而是观察模式、修正映射、反复验证这个过程。这个项目如果后续有兴趣继续深挖,可以往三个方向走:一是把评分函数换成基于四字母词和双词组的统计学模型,切割一下连续字母流;二是引入遗传算法替代爬山,看种群多样性能否突破小文本的局部最优;三是尝试破解维吉尼亚密码,那是多表替换的典型代表,破法又完全不同。替换密码虽然古老,但作为密码分析的入门第一课,它提供的思维训练一点都不过时。