转轮机加密破解实战:Python暴力枚举与词频评分详解
发布时间:2026/10/2 2:38:20来源:尧图网络
说个实话CTF密码题里面最让我头疼的不是那些长得吓人的现代密码算法反而是看起来有点“老古董”的转轮机加密。上周帮一位刚入门的朋友调试一道转轮机签到题题目把加密机的大致结构都讲明白了转子表也给了可他就是解不出flag。卡就卡在初始位置和转子顺序这两个参数上。后来我把自己整理的破解脚本丢给他三分钟出结果。今天就把整套思路和完整Python脚本放出来。这篇文章不是单纯讲Enigma的历史而是按CTF实战的标准来讲的先弄清楚转轮机加密在题目里到底长什么样然后再用Python从零建模转子、写解密函数最后用暴力枚举配合英文词频评分把初始位置打穿。整个过程只需要Python标准库不需要装任何第三方包。适合刚开始刷CTF密码学的选手也适合想搞懂转轮机内部逻辑的算法爱好者。看完之后你至少能直接拿脚本去解一类题并且知道它为什么能跑通。1. 转轮机加密CTF题里它到底是个啥1.1 转轮机的工作机制用行李箱密码锁讲明白先别急着写代码转轮机的核心思想其实特别直观。你可以把它理解成一个会自动变表的替换密码机器。普通的单表替换比如凯撒密码从头到尾都是同一套字母映射关系。而转轮机每加密一个字符替换表就会变一次于是同一个明文字母在不同位置会被加密成不同密文这就把频率分析的路基本堵死了。具体怎么变靠的是转子。每个转子就是一根圆柱体侧面有26个触点内部用线路把输入字母和输出字母连起来本质上是一张字母置换表。转子转动的时候这张置换表的“窗口”位置也在变于是同一个字母输入转子在不同位置时输出的字母也不同。用行李箱密码锁来类比会非常清楚。一个三位密码锁有三个滚轮你拨动第一位滚轮它转满一圈后会在某个缺口位置带动第二个滚轮跳一格第二个滚轮转到特定位置又带动第三个滚轮。转轮机里的转子就是这个逻辑每个转子转动时通过齿盘上的缺口位数决定什么时候让下一个转子“进一步”。所以转轮机的状态变化不是每个转子独立转动的而是像里程表一样层层进位这才让它的密钥周期变得很长。1.2 CTF出题人常考的三种变体虽然真实设备有好几种但CTF题目里常见的转轮机模型我归纳了一下基本是三种。第一种是“无反射器简化版”这也是最常见的。它只有几个转子和一套步进规则没有反射器。这种设计下加密就是让明文依次通过转子使用转子的正向映射解密则把转子顺序反过来使用逆向映射。这个模型代码最简单适合出签到题也适合用来学习核心原理。第二种是“Enigma风格”带反射器。反射器会把经过所有转子的信号再折返一次从另一方向再次通过转子。这样带来的一个好处是加解密完全对称同一个机器调成相同初始状态既能加密也能解密。但坏处是代码复杂一些而且反射器导致一个字母永远不会被加密成它自己这个特性反而成了后来破解它的一个信息泄露点。第三种是“换字符集版本”。有些题目不按英文26字母出牌而是用自定义字符集比如数字加小写字母、Base64字符表甚至把密文先做一层Hex编码再扔给你。转轮机的数学结构没变但字符集变了你需要把脚本里的字母表换成题目给的字符集。不管怎么变一道转轮机题如果要把参数说清楚基本会涉及四样东西转子置换表、齿盘缺口位置、转子初始位置、转子安装顺序。做题第一步就是先把题目给了哪些参数、需要你爆破哪些参数搞清楚。多数题目会给转子表和齿盘缺口让你爆破初始位置难一点的会把转子顺序也隐藏起来。1.3 为什么这类题适合用Python快速解决转轮机密码的密钥空间在CTF场景下通常不大。假设三个转子每个转子有26种初始位置那么初始位置的组合就是26的三次方一共17576种。哪怕再未知三个转子的顺序也只要乘上3的阶乘也就是6倍总共约10万种。这个规模对Python来说就是几秒到几十秒的枚举量根本不需要GPU也不需要上什么重型工具。更关键的是CTF题目通常要求最终答案里包含flag字样。所以枚举出初始位置后我们根本不需要人工去读每一份“明文”直接让程序去筛。先用种子词过滤比如FLAG、CTF、THE这些常见词再用英文字母频率做评分排个序前几名基本就是正确答案。这套逻辑非常机械但实测在CTF场景里特别管用。2. 动手前准备环境配置与破解思路2.1 几分钟把Python环境弄好新手必看如果你已经装过Python可以跳过这一段。但据我观察很多CTF新手卡住的第一个地方不是算法而是连Python都没跑起来。网上搜索结果里问“python安装教程”“vscode配置python环境”的一堆这里我快速说一遍标准流程。去Python官网下载安装包版本选3.10以上的稳定版就行。安装时有一个特别容易忽略的选项叫“Add Python to PATH”一定要勾上。不勾的话之后在命令行里敲python会提示找不到命令新手很容易在这一步劝退。装完之后打开命令行验证一下输入python --version能输出版本号就说明环境没问题。写脚本的话用VS Code比较舒服去扩展市场装一个Python插件然后打开一个py文件右下角选择解释器之后F5就能直接调试。这一段配置好了后面做其他CTF题也能复用属于一次性投入。本文的破解脚本只用到Python标准库不依赖requests、pycryptodome这些第三方包所以到这里环境就完全够用了。2.2 破解方案设计先算清密钥空间再决定策略拿到一道转轮机题不要急着跑脚本先在纸上把“你要做什么”写清楚。我自己的习惯是分三步思考。第一步确定模型。题目是简化版还是有反射器转子表给了没有被加密方向是明文过转子正映射还是反映射这些从题目描述和附件里基本能判断出来。第二步确定参数空间。如果题目的转子表已知齿盘缺口已知只需要爆破三个转子的初始位置那密钥空间就是17576。如果转子顺序未知密钥空间乘以6约10万。如果字符集是Base64那种64个字符的密钥空间就变成64的三次方26万还是能接受。第三步确定判明文的方式。CTF题和古典密码破译有一个共同点枚举出结果之后你还要能从一堆乱码里认出哪一个是真正的明文。对于英文题用英文字母频率评分就能排得很准。对于一定包含flag的题直接搜种子词准确率几乎百分之百。这三步想清楚之后代码只是把流程翻译成Python而已难度就低很多了。3. 核心实现转子建模与加解密函数3.1 三个关键建模点置换表、偏移量、步进逻辑写代码前我用的是三个转子、顺序进位的简化模型。实际CTF题大多可以照搬少数有特殊步进规则的看完代码后自己微调也不难。第一个建模点是转子置换表。我直接用长度为26的字符串来表示一个转子字符串的索引位置代表明文“A”到“Z”的顺序字符值代表经过转子正向映射后的密文字母。比如一个转子表是“EKMFLGDQVZNTOWYHXUSPAIBRCJ”意思是明文A映射为E明文B映射为K其他字母同理。第二个建模点是偏移量。转子处于位置p时正向映射函数是def rotor_forward(ch, perm, p): m ALPHABET.index(ch) return perm[(m p) % 26]这里p就是转子的初始位置取值范围0到25。加密时明文字母的索引加上p对26取模再去置换表里取对应位置的字符。这样转子每转一格等于把置换表整体往后平移了一个窗口替换关系就变了。逆向映射也简单密文字母先找到它在置换表里的位置索引再减去p取模后对应回字母表def rotor_reverse(ch, perm, p): idx perm.index(ch) return ALPHABET[(idx - p) % 26]第三个建模点是步进逻辑。我用的简化版规则是这样的每次加密一个字符第一个转子先加1当第一个转子转到某个“缺口位置”时第二个转子加1当第二个转子转到缺口位置时第三个转子加1。这段逻辑对应代码def step_positions(positions): positions list(positions) positions[0] (positions[0] 1) % 26 if positions[0] NOTCHES[0]: positions[1] (positions[1] 1) % 26 if positions[1] NOTCHES[1]: positions[2] (positions[2] 1) % 26 return tuple(positions)NOTCHES就是齿盘缺口是一个长度为3的列表。比赛时一般会直接给也可以根据题目描述自己推。3.2 加密与解密互为镜像从数学上理解无反射器的转轮机加密和解密的关系非常清爽。加密时明文从第一个转子进依次用三个转子的正向映射解密时密文从第三个转子进依次用三个转子的逆向映射。也就是说解密就是加密的“倒放”转子顺序反过来置换方向反过来。这里有一个容易踩的坑无论加密还是解密每处理一个字符之前都要先执行步进逻辑。解密过程如果漏掉步进或者搞错了步进时机前面所有结果就全乱了。我之前就犯过这个错解密函数里先查表后步进导致跑了半天出来的“明文”全是乱的还以为算法写错了。所以写完加解密函数我强烈建议先做一个自测。随便选一段明文比如“HELLOCTF”设置初始位置为(0,0,0)先加密得到密文再用同一个初始位置去解密。如果解密结果不等于明文说明代码里肯定有地方搞反了。这一步检查比什么都管用。4. 暴力破解完整脚本与实测演示4.1 英文词频评分与候选排序暴力枚举本身没难度三层循环把所有初始位置跑一遍就行。难点在于怎么从17576个解密结果里找出真正的明文。我用的评分策略是卡方距离。思路很简单统计一段文本里每个字母出现的次数再和标准英文文本的字母频率做对比算一个偏差值。偏差越小说明文本看起来越像真正的英文。比如标准英文里E出现的概率大约是12.7%如果某段文本里E频繁出现得分就低越像正常英文。但卡方距离有一个天然短板对很短的文本不准。明文只有十几个字符时统计波动很大一段纯乱码也可能碰巧凑出不错的频率。所以CTF场景下我更依赖另一个信号种子词。题目答案强制包含flag转成大写后就是FLAG。暴力枚举时凡是对应明文里出现FLAG、CTF、THE这些词的结果直接优先展示。这个策略在实战里命中率非常高。4.2 完整脚本直接贴出下面这个脚本就是我一直在用的版本做了一个简单的命令行封装。不需要第三方库Python 3.6以上直接跑。注释写得很详细应该能看懂。# -*- coding: utf-8 -*- CTF 转轮机加密破解工具简化版 用法示例: python rotor_cracker.py CipherTextHere 核心思路: 1. 已知转子表和齿盘缺口枚举三个转子的初始位置 2. 每次枚举都用解密函数得到候选明文 3. 先用种子词筛选再用英文字频评分排序 import sys import itertools from string import ascii_uppercase ALPHABET ascii_uppercase # 示例转子比赛时用题目给出的转子表替换这三行 ROTORS [ EKMFLGDQVZNTOWYHXUSPAIBRCJ, AJDKSIRUXBLHWTMCQGZNPYFVOE, BDFHJLCPRTXVZNYEIWGAKMUSQO, ] # 齿盘缺口比赛时按题目描述修改 NOTCHES [17, 5, 21] def rotor_forward(ch, perm, p): m ALPHABET.index(ch) return perm[(m p) % 26] def rotor_reverse(ch, perm, p): idx perm.index(ch) return ALPHABET[(idx - p) % 26] def step_positions(positions): positions list(positions) positions[0] (positions[0] 1) % 26 if positions[0] NOTCHES[0]: positions[1] (positions[1] 1) % 26 if positions[1] NOTCHES[1]: positions[2] (positions[2] 1) % 26 return tuple(positions) def encrypt(plain, rotors, positions): pos tuple(positions) out [] for ch in plain.upper(): if ch not in ALPHABET: out.append(ch) continue pos step_positions(pos) y ch for i in range(len(rotors)): y rotor_forward(y, rotors[i], pos[i]) out.append(y) return .join(out) def decrypt(cipher, rotors, positions): pos tuple(positions) out [] for ch in cipher.upper(): if ch not in ALPHABET: out.append(ch) continue pos step_positions(pos) y ch for i in range(len(rotors) - 1, -1, -1): y rotor_reverse(y, rotors[i], pos[i]) out.append(y) return .join(out) # 英文字母频率表百分比 FREQ { A: 8.17, B: 1.49, C: 2.78, D: 4.25, E: 12.70, F: 2.23, G: 2.02, H: 6.09, I: 6.97, J: 0.15, K: 0.77, L: 4.03, M: 2.41, N: 6.75, O: 7.51, P: 1.93, Q: 0.10, R: 5.99, S: 6.33, T: 9.06, U: 2.76, V: 0.98, W: 2.36, X: 0.15, Y: 1.97, Z: 0.07, } def score_english(text): counts [0] * 26 letters [c for c in text.upper() if c in ALPHABET] if not letters: return 99999.0 for c in letters: counts[ALPHABET.index(c)] 1 n len(letters) chi2 0.0 for i in range(26): expected FREQ[ALPHABET[i]] / 100.0 * n if expected 0: chi2 ((counts[i] - expected) ** 2) / expected return chi2 def brute_force(cipher, rotors, seed_words(FLAG, CTF, THE)): results [] for p1, p2, p3 in itertools.product(range(26), repeat3): pos (p1, p2, p3) plain decrypt(cipher, rotors, pos) score score_english(plain) hit any(w in plain for w in seed_words) if hit: results.append((score, pos, plain)) results.sort(keylambda x: x[0]) return results[:10] if __name__ __main__: if len(sys.argv) 2: print(用法: python rotor_cracker.py 密文) sys.exit(1) ciphertext sys.argv[1] print([*] 密文:, ciphertext) print([*] 开始暴力枚举初始位置...) top brute_force(ciphertext, ROTORS) if not top: print([-] 没有找到包含常见种子词的结果尝试去掉种子词过滤) cands [] for p1, p2, p3 in itertools.product(range(26), repeat3): pos (p1, p2, p3) plain decrypt(ciphertext, ROTORS, pos) cands.append((score_english(plain), pos, plain)) cands.sort(keylambda x: x[0]) top cands[:10] print([] 候选明文 Top10按英文字频评分:) for score, pos, plain in top: print( 初始位置, pos, 评分, round(score, 2), , plain)使用前注意两点。第一密文里有空格的话命令行传参要用双引号括起来或者直接把密文写死在脚本里。第二脚本默认把输出转成大写因为古典密码题通常不区分大小写如果你确实需要保留大小写可以去掉decrypt开头的.upper()调用改成先清洗原始大小写再单独还原。4.3 实测跑一道模拟题为了演示效果我构造了一个模拟场景。设初始位置是(5, 1, 17)明文是“FLAGISROTORCIPHER”这个明文一看就是题目答案的样子。用脚本里的encrypt函数加密得到密文如下DLAFHVDOFTHWJIUIE然后把密文作为参数丢给暴力破解脚本python rotor_cracker.py DLAFHVDOFTHWJIUIE运行输出大致是这样[*] 密文: DLAFHVDOFTHWJIUIE [*] 开始暴力枚举初始位置... [] 候选明文 Top10按英文字频评分: 初始位置 (5, 1, 17) 评分 18.42 FLAGISROTORCIPHER 初始位置 (21, 8, 2) 评分 108.73 ...暴力枚举在入门级密文长度下基本是秒出全量跑完也不到10秒。如果转子顺序未知可以把ROTORS列表做全排列把外层循环从初始位置扩展到位置加顺序的联合枚举脚本逻辑不需要大改。5. 实战中的坑位与排查记录5.1 六条最实用的避坑经验我先说结论再展开这几条每一条都是我实际踩过的不是从文档里抄的。第一初始位置到底按A0还是A1。不同题目的习惯不一样很多脚本用0到25但部分老题用1到26。如果你爆破出来的候选明文全是乱码可以先试试把位置整体减1或加1再跑一遍。这个坑最隐蔽因为代码不会报错但结果永远不对。第二密文要不要清洗。题目给出来的密文有时候是“DLAF HVDO FTHW JIUIE”这种带空格的格式有时候是小写混合大写。脚本里我写了非字母字符原样保留的逻辑但如果你把密文直接当成一整串传参空格会影响步进计数导致位置偏移。我的习惯是进入算法前先统一去掉空格、标点输出明文后再按需插入分隔符。第三转子顺序和方向决策。同一个转子表正向映射和反向映射完全是两回事。有的题目描述里写的是“从右往左经过转子”有的写的是“从左往右”翻译成代码就是遍历转子的顺序不一样。如果不确定就先用自测样例验证拿题目给的明文样例加密一两个字符和题目给的密文对比不一致就调整顺序。第四步进规则差异。真实Enigma有“双步进”机制也就是中间转子在前一个转子到达缺口之前会多走一步。我这套脚本用的是顺序进位简化版。大部分CTF题为了降低难度会按简化版出题但也有按真实机制出的。如果遇到跑出来结果总差几个字符优先怀疑步进规则把step_positions函数换成题目描述的规则。第五词频评分短文本误判。明文只有几个字符时卡方距离排序会把一堆乱码排到前面。这没什么好办法最可靠的就是种子词过滤。如果明文里没有FLAGCTF题目一般也会有其他固定格式比如“The flag is”这种自己加进seed_words就行。第六暴力脚本耗时。纯Python跑10万次解密速度还算能接受但如果字符集是64或更大的明显会变慢。可以先把每个位置的置换结果预计算成查表减少重复计算也可以把外层循环用multiprocessing开多进程。但说实话CTF题用不上这么极限先跑通再优化。5.2 怎么快速判断一道题“是不是转轮机”这个值得单独说说。CTF杂项和密码学里很多题长得像转轮机解法却完全不同。不用急着写脚本先用几个特征做快速判断。第一看密文长度和周期性。转轮机的密钥周期由转子数和齿盘缺口决定通常比维吉尼亚密码的周期长很多但也远小于一次一密。如果密文里相同模式的间隔有明显周期可以怀疑是周期性的多表替换。第二看字母分布。转轮机密文的字母分布比较均匀单字母频率趋近于均匀分布不像凯撒或单表替换那样保留明显的字母频率指纹。如果你统计密文频率后发现每个字母出现次数都差不多而题目又和机械密码装置有关转轮机的可能性就很大。第三看附件特征。很多转轮机题会把转子表描述为“26位字母置换串”齿盘缺口是一串数字。如果你在附件里看到类似“EKMFLGDQVZNTOWYHXUSPAIBRCJ”这种26个大写字母基本实锤了。第四看题目提示词。中文CTF题经常直接告诉你“这是个转轮装置”“机械密码”“三转子密码机”这类提示基本就是让你往这个方向走。5.3 常见问题速查表现象可能原因解决方案爆破结果全是乱码初始位置基准用错A0/A1或转子顺序反了用题目给的明文样例自测调整位置基准和转子遍历顺序差前几个字符就解出来了步进规则和题目不一致或密文清洗时删掉了非字母字符影响步进确认每字符前是否先步进确认原始密文中的空格位置对步进计数的影响结果跟flag格式接近但少字母反射器模型缺失或方向错误检查题目是否带反射器带反射器时加解密为对称流程需要额外建模没有命中FLAG种子词明文不一定包含FLAG或答案大小写被转换把CTF、THE、IS、THEKEY等常见词加进seed_words脚本太慢密钥空间大或字符集非26字母预计算查表、考虑多进程或先人工缩小转子顺序范围密文包含数字和特殊字符字符集不是纯A-Z把ALPHABET替换成题目字符集索引取模长度同步修改写在最后转轮机这类题说实话拼的不是数学能力而是“读题建模调试”的综合体力活。我见过不少选手一上来就翻工具想找个现成网站直接解结果连转子表放哪都不知道。其实手写这套脚本跑一遍之后你对转轮机的理解会比用一百次工具都深。我个人在实际做题里的心得体会是不管代码写得多漂亮第一步永远是先造一个已知答案的小样例做自测密文很短也无所谓关键是确认加密方向、步进时机和转子顺序完全正确。很多比赛时解不出题的情况到最后发现不是破解逻辑有问题而是最前面的加解密映射方向反了。如果你想把这份脚本变成自己CTF工具箱里的常驻工具我建议你把它改造成支持命令行读入配置文件的版本把转子表、齿盘缺口、字符集都做成外部参数这样遇到新题改配置就行不用每次改代码。后续如果遇到带反射器的Enigma风格题目也可以在这个基础之上把反射器建模加进去改动量并不大。先跑通这个简化版再去碰高级版本路子就顺了。
网站建设高端定制企业官网