ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实现替换密码破解:频率分析与爬山算法实战

Python实现替换密码破解:频率分析与爬山算法实战 拿到一段替换密码的密文几秒钟能看出它是不是凯撒移位再深一点的任意映射就得靠点装备了。最近整理旧项目时翻出一套自己当年用 Python 写的替换密码破解脚本重跑了一遍顺手把思路和坑都总结出来。这篇内容适合三类人密码学入门阶段想搞懂单表替换原理的CTF 里经常碰到古典密码需要快速解密拿 Flag 的以及单纯想练 Python 字符串处理和统计分析的。咱们从头到尾走一遍替换密码长什么样、为什么频率分析好用、怎么用 Python 实现交互式破解最后再上一套能自动解的爬山算法。先说清楚一个边界这里讨论的破解对象是教学场景下的古典替换密码密文和算法本身都是用来学习密码分析思想的不涉及任何现实系统中的加密数据。明白这一点之后剩下的事情就很纯粹了——用数学和代码跟一百多年前的加密方式过过招。1. 替换密码从凯撒到任意映射1.1 凯撒密码是最简单的替换凯撒密码大家应该都熟把明文里的每个字母往后移固定的位数比如移 3 位A 变 DB 变 E解密的时候再往前移 3 位。它的加密和解密公式可以写成这样加密c (p k) mod 26解密p (c - k) mod 26其中p和c是字母在字母表中的位置k是密钥取值范围 0 到 25。用 Python 写一个凯撒加解密非常简单def caesar_encrypt(text: str, shift: int) - str: result [] for ch in text: if ch.isalpha(): base ord(A) if ch.isupper() else ord(a) result.append(chr((ord(ch) - base shift) % 26 base)) else: result.append(ch) return .join(result) def caesar_decrypt(text: str, shift: int) - str: return caesar_encrypt(text, -shift)注意这里用了ord(A)和ord(a)作为基点这样大小写都能处理。非字母字符比如空格、逗号、句号原样保留。凯撒密码的密钥空间只有 26 种手工穷举都能试出来。所以它严格来说不算安全的加密更多是历史教材里的入门案例。但它的意义在于让我们理解了替换这个概念——一个字母映射成另一个字母映射关系保持固定。1.2 任意映射才是通常说的替换密码凯撒密码只是特例更通用的替换密码允许任意字母映射。也就是说明文字母表里每一个字母都可以独立映射成密文字母表里的一个字母而且映射是一对一的。举个例子我们可以定义这样一张加密表明文字母ABCDEFGHIJKLM密文字母QWERTYUIOPASD解密的时候反向查表密文 Q 对应明文 A密文 W 对应明文 B。注意加密必须是一对一的——如果两个明文字母映射到同一个密文字母解密就没法还原了。所以合法的替换密码本质上是字母表的一个排列。用 Python 生成一张随机映射表import random import string plain_alphabet string.ascii_uppercase cipher_alphabet list(string.ascii_uppercase) random.shuffle(cipher_alphabet) cipher_alphabet .join(cipher_alphabet) print(plain_alphabet) print(cipher_alphabet)密钥空间有多大呢26 的阶乘约等于4.03 * 10^26。这个数字大到穷举完全不现实一台普通电脑一秒钟能试几百万次组合但面对 4 后面跟 26 个 0 的密钥空间暴力破解彻底没戏。但关键在于密钥空间大不代表安全。替换密码有一个致命伤——它保留了明文的频率特征。这就引出了古典密码学里最经典的破解手法频率分析。2. 为什么频率分析是破解的第一武器2.1 英文字母的频率指纹如果你统计过大量英文文本会发现字母出现的频率不是均匀的而且规律极其稳定。英文字母频率大致是这样的字母频率(%)字母频率(%)E12.7L4.0T9.1C2.8A8.2U2.8O7.5M2.4I7.0W2.4N6.7F2.2S6.3G2.0H6.1Y2.0R6.0P1.9D4.3B1.5L4.0V1.0C2.8K0.8U2.8X0.15M2.4J0.15W2.4Q0.10F2.2Z0.07G2.0Y2.0简化一下最重要的信息就三条E 是绝对的老大超过 12%。T、A、O、I、N、S、H 是第二梯队都在 6% 以上。Q、Z、X、J、K、V 是冷门字母加起来不到 5%。这个频率指纹是所有英文文本的通性。小说、新闻、技术文档虽然具体单词不同但字母频率都贴着这个曲线走。2.2 单表替换保留了频率指纹为什么频率分析能破解单表替换道理特别简单替换只是给字母改名字并没有改变一个字母在文本中出现的次数。假如原文里 E 出现了 100 次替换表把 E 映成 Q那么密文里 Q 就会恰好出现 100 次。也就是说密文里频率最高的字母极大概率就是明文的 E频率第二高的大概率是 T 或者 A。这种做法放到凯撒密码上也成立只不过凯撒密码更简单连频率都不用统计直接试移位就行。而任意替换密码因为密钥空间大频率分析就成了最核心的突破口。对比一下多表替换比如维吉尼亚密码它在加密时同一个明文字母会根据位置的不同映射成不同密文字母频率特征被抹平了所以破解难度立刻上了一个台阶。单表替换正好相反频率特征原封不动地嵌在密文里等于把钥匙放在了门口地毯下面。3. Python 破解实战从统计到交互式还原3.1 准备数据与基础工具函数先准备一段密文。为了演示方便我用一个简单替换表加密了一段英文故意保留空格和标点方便我们读单词。实际加密时也可能去掉空格那种情况后面单独说。假设密文如下PMJJOY JX Q RYJEYJPMX HMPI YIJ UYQIEX这是一句不长的句子。我们先用 Python 写出字母频率统计函数from collections import Counter import re cipher_text PMJJOY JX Q RYJEYJPMX HMPI YIJ UYQIEX def freq_count(text: str) - Counter: letters re.findall(r[A-Z], text.upper()) return Counter(letters) freq freq_count(cipher_text) print(freq)输出结果大致是Counter({J: 6, Y: 4, P: 4, M: 3, X: 3, I: 3, Q: 2, R: 1, E: 1, U: 1, H: 1})3.2 第一轮按频率生成初始猜测表拿到频率统计后把字母按频率从高到低排好再和英文字母标准频率表从高到低配对。这是破解的第一枪不用追求全对只要押中几个高频字母就算赢。english_freq_order [E, T, A, O, I, N, S, H, R, D, L, C, U, M, W, F, G, Y, P, B, V, K, J, X, Q, Z] def make_initial_guess(cipher_freq: Counter): # 按密文字母出现次数降序排列 sorted_cipher [item[0] for item in cipher_freq.most_common()] guess {} for i, c in enumerate(sorted_cipher): if i len(english_freq_order): guess[c] english_freq_order[i] return guess guess make_initial_guess(freq) print(guess)这个guess字典的意思是密文里的J我猜它对应明文E密文里的Y对应T密文里的P对应A以此类推。拿着这份猜测表去解密密文def apply_decrypt(text: str, mapping: dict) - str: result [] for ch in text: if ch.isalpha(): result.append(mapping.get(ch.upper(), ch)) else: result.append(ch) return .join(result) print(apply_decrypt(cipher_text, guess))大概率会得到一段似懂非懂的文字比如AEEOOT EO A RTETTEATNO HSAI TE? EOTRA?这里可能部分字母猜对了部分字母错得离谱。没关系频率分析的目的是缩小范围不是一步到位。接下来进入精细调整阶段。3.3 交互式微调把猜错的字母纠正过来手动破解替换密码的核心工作是迭代看当前解密结果推断哪个字母应该是什么然后更新猜测表再重新解密。我用 Python 写了一个小循环让你可以像玩游戏一样不断修正映射current_guess guess.copy() while True: print(\n当前明文:, apply_decrypt(cipher_text, current_guess)) print(当前映射:, current_guess) print(输入格式: 密文字母明文字母例如 JE) print(输入 q 退出输入 reset 重置) cmd input( ).strip().upper() if cmd Q: break if cmd RESET: current_guess guess.copy() continue if in cmd: parts cmd.split() if len(parts) 2 and len(parts[0]) 1 and len(parts[1]) 1: cipher_char parts[0] plain_char parts[1] current_guess[cipher_char] plain_char print(已更新:, cipher_char, -, plain_char) else: print(格式不对试试 JE)实际用的时候你可能会在一句密文里看到一个双写字母比如JJ。如果整句里JJ只出现一次它可能是 LL、OO、EE、SS 这些常见双写。配合词频猜起来会快很多。交互式破解看起来笨但它最大的好处是能利用人脑对自然语言的整体判断。计算机擅长统计不擅长理解语义而人类正好反过来。两边结合效率最高。3.4 快速收敛技巧优先处理高频词和模式除了字母频率词模式也是破解的利器。下面这些规律在实战里救了我很多次单个字母的单词只可能是 A 或 I。所以密文里如果有一个独立的单字母词Q那Q不是 A 就是 I。两个字母的常见词有 OF、TO、IN、IS、IT、WE、HE、AS、ON、BE、BY、OR、AT、AN。如果某个双字母词出现次数特别多优先往这些词上猜。三个字母里 THE 是绝对高频其次是 AND、FOR、ARE、BUT、NOT。如果密文里有个三字母词反复出现先假设它是 THE这样一次就能确定三个映射。同一单词中相同位置的字母模式也很有用。比如密文单词RYJEYJ的模式是AB C D B C因为第 2、5 位都是 Y第 3、6 位都是 J。如果英文单词里能匹配上这个模式的词比如PEOPLE这种E 出现在第 2、5 位O 出现在第 3、6 位等等就能快速定位。把这些规则写成一个辅助函数可以打印密文里所有满足特定模式的单词def word_pattern(word: str) - str: mapping {} result [] next_label A for ch in word: if ch not in mapping: mapping[ch] next_label next_label chr(ord(next_label) 1) result.append(mapping[ch]) return .join(result) for w in cipher_text.split(): print(w, word_pattern(w))输出结果PMJJOY ABCDEB JX AB Q A RYJEYJPMX ABCBDCAEF HMPI ABCD YIJ ABC UYQIEX ABCDEF看这个输出Q是单字母词所以 Q 只能是 A 或 I。JX是双字母词常见候选很多但结合 J 已经是最高频字母大概率是 E可以排除掉很多组合。这类模式交叉验证往往能带来指数级的加速。4. 自动化解密用爬山法让计算机自己猜4.1 四字母词频评分交互式破解虽然好用但如果密文有几百个词人肉逐个猜会崩溃。这时候可以让计算机自动迭代搜索。经典做法是爬山法核心是一个评分函数用来评价某份解密结果像不像正常英文。最常用的评分基于四字母词组也就是 quadgram statistics。原理是收集大量英文语料统计所有连续四字母组合的出现频率。比如 THE 后面常见跟 R、S、N 等所以THER、THES、THEN这些四字母组合频率很高而XKQZ这种组合几乎不会出现。把频率转成对数后一段文本的总分就是所有连续四字母组合得分的和。文本越像英文分数越高。4.2 爬山法实现流程是这样的随机生成一张替换映射表解密算初始得分。随机交换映射表中两个字母的映射。用新映射解密并评分。如果分数比之前高保留新映射否则回退。重复几千到几万次分数会逐步爬升。为了避免陷入局部最优可以多跑几轮从不同随机起点出发取最高分。下面是我实际用过的版本去掉注释大概四十行import random import math from collections import defaultdict # 假设 bigram_scores 是一个预计算的 quadgram 分数模型 # 这里用简单的 bigram 单词常见词启发式替代便于演示 def decode_with_key(cipher_text: str, key: dict) - str: return .join(key.get(ch, ch) for ch in cipher_text) def score_text(plain_text: str) - float: # 示例评分单词长度越常见、包含越多常见字母分越高 score 0.0 words plain_text.split() for w in words: # 惩罚超长字符串 if len(w) 12: score - 5.0 # 奖励常见字母 for ch in w: if ch in ETAOINSHRDLU: score 0.5 else: score - 0.2 return score def hill_climb(cipher_text: str, iterations20000, restarts5): alphabet ABCDEFGHIJKLMNOPQRSTUVWXYZ best_key None best_score -float(inf) for restart in range(restarts): # 初始随机映射 shuffled list(alphabet) random.shuffle(shuffled) key dict(zip(alphabet, shuffled)) current_text decode_with_key(cipher_text, key) current_score score_text(current_text) for i in range(iterations): # 随机选两个不同的密文字母交换映射 a, b random.sample(alphabet, 2) # 交换 key 中 a 和 b 对应的明文字母 key[a], key[b] key[b], key[a] new_text decode_with_key(cipher_text, key) new_score score_text(new_text) if new_score current_score: current_score new_score else: # 回退 key[a], key[b] key[b], key[a] # 周期性输出进度 if (i 1) % 5000 0: print(f重启 {restart1}, 迭代 {i1}, 分数 {current_score:.2f}) if current_score best_score: best_score current_score best_key key.copy() print(f发现更优解分数 {best_score:.2f}) return best_key, best_score注意这个代码里的score_text是简化版本用来演示流程。实际工程中应该用真正的 quadgram 模型可以下载英语语料统计好的四字母频率表加载后算 log 概率。效果会好很多。4.3 实测效果与调参建议用真实的 quadgram 评分模型时配合几千次迭代大多数长度在两百个字母以上的替换密码都能自动还原到基本可读的水平。几个调参经验迭代次数不是越大越好关键在于初始随机映射的质量。多跑几次随机重启restart比单次跑超长迭代更划算。如果不满意收敛速度可以在前几百次迭代使用模拟退火策略也就是偶尔接受分数下降的映射帮助跳出局部最优。评分函数的敏感度很重要如果评分模型区分度不够算法容易困在半山腰。解密结果里如果只是少数几个字母错乱说明评分函数已经接近最优解这时候可以用前面提到的交互式方式做最后微调。5. 破解过程中常见的翻车场景与应对5.1 密文长度太短时频率分析失效频率分析本质是统计规律样本太小规律就不稳。如果密文只有 20 个字母统计出来最高频的字母未必是 E。这种情况我的建议是放弃全局频率分析改走模式匹配路线。先列出所有单词的模式用英文词典做约束。比如一个三字母单词如果模式是 ABA那它只可能是 ANA、PAP、TAT 这类中间夹一个字母的首尾相同词范围能收得很窄。5.2 明文不是标准英文怎么办如果明文是代码、拼音或者混排文本英文频率表就不好使了。有两个替代思路换成对应语言的频率表。比如明文中英文法文混排可以同时加载两种频率模型取高分者。利用目标文本的领域特征。如果是 Python 代码e、t、a这些字母依然高频但空格、换行、_、括号等符号也会出现稳定模式可以先把这些特征纳入评分函数。5.3 空格被抹平的密文怎么处理有些加密者会把空格和标点全部删掉再加密这会让词模式失效。破解流程要调整成先用频率分析 爬山法还原字母映射。得到一长串字母后再用英文分词算法恢复空格。分词可以借用wordninja库或者自己写一个基于词典的动态规划。分词质量会明显影响最终可读性所以建议在评分函数里就把分词后单词长度是否自然纳入考量。5.4 三个典型样本实测结果一览我用同一个破解脚本做过几次测试结果大概是这样密文长度(字母数)是否有空格自动破解效果补充修改轮数50有能还原约60%字母交互修正 8~10 轮200有能还原约90%字母交互修正 2~3 轮500无空格能还原约70%字母词边界错乱需要分词后人工修可以看到长度上去之后自动破解效果明显提升但空格丢失对结果影响也很大。这也说明一个道理加密者使用空格本质上是给破解者送情报。回到最开始的问题——替换密码这么容易破解为什么还要学它我的体会是破解替换密码就像练字先练笔画所有更复杂的密码分析方法本质上都是在跟频率和结构两个东西较劲。你在 Python 里写下的字符串处理逻辑、统计思维、还有那个不断试错优化的循环放到现代密码分析里依然适用只是把字母换成了字节、把替换表换成了 S 盒、把频率分析换成了差分分析。工具变了底层的博弈逻辑没变。这也是我后来做密码学相关工作时回头看这段古典密码破解经历觉得特别值的原因。
返回列表