很多刚接触CTF的朋友,第一次看到维吉尼亚密码(Vigenère Cipher)的题目时,容易被那一大串看似毫无规律的字母吓住。实际上,它是古典密码里最值得花半小时彻底搞懂的一个题型,因为一旦掌握,你能同时理解频率分析、重合指数、卡西斯基测试这些后续所有密码学题都会反复用到的基础技能。这篇内容就围绕CTF密码学入门最常见的维吉尼亚密码,从加密流程讲起,再到手工破解的完整思路,最后给出一份可直接套用的Python脚本和排错经验,适合刚入门、想系统梳理的同学。
维吉尼亚密码和凯撒、栅栏那些一看到题目就能手算的题型不同,它最大的价值在于把"单表替换"提升成了"多表替换"。也就是说,同样一个字母,在不同位置上可能对应完全不同的密文字母。这种设计恰恰是许多CTF出题人喜欢的地方:它不会难到完全无处下手,但又能有效拦截只会复制粘贴工具的人。这篇博文的目的很明确——让你读完就能亲手实现一遍加解密,并且能把破译流程走通,而不是只停留在"会用在线解密网站"的层面。
1. 维吉尼亚密码的核心原理与设计思路
很多教材上来就扔公式,导致新手看了半天其实没懂为什么要这么设计。所以这里我用最直观的方式拆开讲。
1.1 从凯撒密码到多表替换的进化逻辑
凯撒密码的本质,是把字母表平移一定的位数。比如密钥是3,那么明文A变成D,B变成E,整个文本里所有字符都统一平移3位。这个做法有一个致命弱点:每个字母是固定映射的,也就是说整个文本中出现的字母E,密文永远是H。破译者只需要积累一定长度的密文,就能通过英文文本的频率统计(最常见的是字母e)直接猜出偏移量。
后来的人就想到一个改进方向:如果能做到同一个字母在不同位置的映射不同,那频率统计攻击就会失去一部分效果。于是维吉尼亚密码的思路就诞生了:不再使用一个固定偏移,而是用一组密钥字母,每个密钥字母对应一个凯撒位移,按照密钥循环套用到明文上。比如密钥是"KEY",那么明文的第1位用K对应的位移(10)加密,第2位用E(4),第3位用Y(24),第4位又回到K(10),循环往复。
这样一来,同样的明文E,如果它出现在第1位、第2位、第3位,加密后可能是O、I、C,统计特征被明显打散。这个思想在Crypto领域非常重要,它本质上是一种初级的"混淆"手段,CTF里很多现代密码题虽然不直接考维吉尼亚,但理解这种多表替换能让你更顺畅地理解分组密码、流密码里的“密钥流”概念。
1.2 加密流程中的字母表与位移映射
在实际操作前,先把字母和数字的对应关系统一一下。最常见的是用字母表A-Z,A=0,B=1,一直到Z=25。整个维吉尼亚加密公式可以写成:
C = (P + K) mod 26
其中P就是当前明文字母对应的数字,K是当前密钥字母对应的数字,C就是密文字母的数字。注意这里所有计算都限制在0到25之间,所以用26取模。
我们用一个具体例子走一遍。明文取"CTFISFUN",密钥取"KEY"。先把明文字母转成数字:C=2,T=19,F=5,I=8,S=18,F=5,U=20,N=13。密钥字母转成数字:K=10,E=4,Y=24。然后开始逐位加密:
- 第1位:明文C(2)+密钥K(10)=12 -> M
- 第2位:明文T(19)+密钥E(4)=23 -> X
- 第3位:明文F(5)+密钥Y(24)=29,29 mod 26 = 3 -> D
- 第4位:明文I(8)+密钥K(10)=18 -> S
- 第5位:明文S(18)+密钥E(4)=22 -> W
- 第6位:明文F(5)+密钥Y(24)=29,取3 -> D
- 第7位:明文U(20)+密钥K(10)=30,取4 -> E
- 第8位:明文N(13)+密钥E(4)=17 -> R
所以加密结果是"MXDSWDER"。看到没有,明文里有两个F,在第3位和第6位分别加密成了D和D,恰好撞车了,但大部分相同字母会错开,这就是多表替换的威力。
解密公式就是P = (C - K) mod 26,只不过C减K后可能得到负数,需要再加26再取模。手工计算时不要忘了这个细节。
2. 手写加密与Python实现
理解原理之后,一定要自己动手实现一遍。CTF选手不能只会调用工具,手写代码的速度往往决定了你能否在一场比赛中快速得分。
2.1 明文预处理与密钥处理
在实际编码之前,需要明确两个细节。
第一点,明文里的非字母字符怎么处理。标准维吉尼亚密码通常只处理A-Z,所以空格、标点、数字要么直接跳过,要么全部移除。在CTF题目里,密文通常已经只包含大写或者小写字母,所以一般不需要额外清理。如果题目里混着空格,我建议先全部转成大写然后去掉非字母字符。
第二点,密钥循环的使用。多数实现里只对字母位置计数,当用到一个密钥字符后,即使明文里有空格、数字,密钥也不移位。比如明文"HELLO WORLD",如果你想保留空格,那么空格不参与加密,密钥下标也不增加,H用密钥第一位,E用第二位,L用第三位,L用第四位,O用第五位,空格跳过,后面的W继续用第六位密钥。这个细节很多人会写错,尤其是在做还原脚本时,密钥错位一位整个结果就乱了。
2.2 加密、解密以及暴力破解脚本框架
下面给出一套简洁的Python实现。我刻意写成偏CTF比赛里顺手能用的风格,不引入复杂类结构,核心思路是第一优先级。
def vigenere_encrypt(plaintext, key): plaintext = plaintext.upper() key = key.upper() ciphertext = [] kidx = 0 for ch in plaintext: if ch.isalpha(): p = ord(ch) - ord('A') k = ord(key[kidx % len(key)]) - ord('A') c = (p + k) % 26 ciphertext.append(chr(c + ord('A'))) kidx += 1 else: ciphertext.append(ch) return ''.join(ciphertext) def vigenere_decrypt(ciphertext, key): ciphertext = ciphertext.upper() key = key.upper() plaintext = [] kidx = 0 for ch in ciphertext: if ch.isalpha(): c = ord(ch) - ord('A') k = ord(key[kidx % len(key)]) - ord('A') p = (c - k) % 26 plaintext.append(chr(p + ord('A'))) kidx += 1 else: plaintext.append(ch) return ''.join(plaintext)使用的时候直接调用就行。需要注意,当解密结果出现一串乱码时,先检查密钥大小写是否统一,再检查非字母字符是否误跳了密钥下标。这两处是手写维吉尼亚代码最常见的bug来源。
2.3 用Python验证手工加密结果
我们验证一下刚才手工算的"CTFISFUN"。
print(vigenere_encrypt("CTFISFUN", "KEY")) # 输出应该是 MXDSWDER print(vigenere_decrypt("MXDSWDER", "KEY")) # 输出应该是 CTFISFUN和手算完全一致。执行出来没问题,说明你的代码逻辑是通顺的。CTF里经常还要做反方向操作,比如已知密文和密钥求明文,这个decrypt函数也就是全部功夫了。很多题目甚至只给密文,不给密钥,那就要进入下一章说的破解环节。
3. 破解维吉尼亚密码的完整思路
拿到一段没有密钥的维吉尼亚密文,不要急着乱猜。专业解法有一套标准流程:先估算密钥长度,再按密钥长度拆分分组,最后对每一组做单表频率分析。每一步都有对应的数学指标在支撑。
3.1 卡西斯基测试:从重复片段找线索
卡西斯基测试解决的是第一关:密钥长度到底是多少。原理很简单:如果密文里出现一小段连续的字母组合,比如"ABQ",而且它在整个文本里出现了两三次,那么很大概率是明文中同一个单词或片段在两次被同样的密钥片段加密后,得到了相同的结果。两次出现之间的距离,往往就是密钥长度的整数倍。
实操上是这样:你搜索密文里所有长度大于等于3的重复子串,记录每对重复串起始位置之间的间隔,然后把这些间隔取公因数。出现频率最高的公因数,就是最可能的密钥长度。
举个例子,如果密文里第0位和第12位都出现了"XYZ",间隔12,那密钥长度很可能是2、3、4、6、12中的一个。如果一个间隔是15,另一个是20,两个的公因数是5,那密钥长度首先怀疑5。卡西斯基测试虽然不是100%精确,但它可以大幅缩小候选范围。
3.2 重合指数:用统计特征确认密钥长度
卡西斯基测试比较依赖文本长度和运气。如果密文太短,或者重复片段太少,就需要用重合指数(Index of Coincidence,简称IC)来做更数学化的判断。
重合指数的公式是:
IC = sum( n_i * (n_i - 1) ) / ( N * (N - 1) )
其中N是文本总长度,n_i是第i个字母在文本里出现的次数。对于一段完全随机的英文字母文本,IC大约是0.038;对于一段正常的英文文本,IC大约是0.065。维吉尼亚密码的本质是单表凯撒的组合,因此当我们猜测的密钥长度正确时,把密文按每第L个字符拉出来分组,每一组内的字母频率会接近英文自然分布,IC会明显靠近0.065;如果密钥长度猜错了,每一组里的字母来自不同凯撒位移的混合,IC会接近0.038。
所以判断方法就是:从1到某个上限(比如20)尝试不同密钥长度L,把密文分成L组,分别计算每组的IC,再取平均值。哪个L使平均IC最高、最接近0.065,哪个就是密钥长度。我实践下来,在长度超过200的密文里,这个方法比卡西斯基测试更可靠。
3.3 分组频率分析:逐位还原密钥字母
假设我们已经确定了密钥长度是L,接下来就把密文按位置下标除以L的余数分成L组。第0组由所有下标为0、L、2L、3L...的字符组成,这一组里的字符都是用同一个密钥字母(第1位密钥)加密的。第1组同理。
这时每组的破解问题退化成单表凯撒密码。单表凯撒最简单有效的破解方法,还是频率分析。正常英文文本里字母E出现的频率最高,大约12%左右,T、A、O、I、N紧随其后,所以只要统计每一组里哪个字母被加密后出现得最多,就大概率是E被映射过去的密文字母。用这个密文字母减去E对应的数字,就能反推出当前分组的密钥字母。
举个例子,某组统计后发现密文字母"F"出现最多,F对应的数字是5,E对应的数字是4,那么密钥字母数字就是5-4=1,对应B。这样逐组算下去,整个密钥就出来了。如果某组算出来的密钥看起来不对,还有一种更精细的做法:把0到25每个可能的偏移都试一遍,用该组偏移后的频率和英文标准频率做卡方检验,取卡方最小的那个偏移。这个方法在文本较短时比单纯找最高频字母更稳定。
3.4 完整破解脚本:从候选密钥长度到自动解密
把上面三步整合成一个脚本,可以写成下面这个结构:
import string from collections import Counter def calc_ic(text): n = len(text) if n < 2: return 0 freqs = Counter(ch for ch in text if ch.isalpha()) ic = sum(v * (v - 1) for v in freqs.values()) / (n * (n - 1)) return ic def split_by_keylen(text, key_len): groups = [''] * key_len for i, ch in enumerate(text): if ch.isalpha(): groups[i % key_len] += ch.upper() return groups def guess_key_by_freq(groups): key = [] english_freq = 'ETAOINSHRDLCUMWFGYPBVKJXQZ' for group in groups: if not group: key.append('?') continue counter = Counter(group) # 方法一:最高频字符按E复原 most_common = counter.most_common(1)[0][0] shift = (ord(most_common) - ord('A') - (ord('E') - ord('A'))) % 26 key.append(chr(ord('A') + shift)) return ''.join(key) def guess_length_by_ic(text, max_len=20): ic_list = [] for l in range(1, max_len + 1): groups = split_by_keylen(text, l) avg_ic = sum(calc_ic(g) for g in groups) / l ic_list.append((l, avg_ic)) return sorted(ic_list, key=lambda x: -x[1])[:5]实际使用时,先用guess_length_by_ic看排名靠前的长度,再用split_by_keylen和guess_key_by_freq得到密钥,最后用vigenere_decrypt解出明文。如果明文前半段可读、后半段乱码,很可能是密钥长度差1,多试试相邻长度就行。
4. 实战演练:一道CTF题从密文到明文的全流程
理论说完了,下面用一道模拟题目完整走一遍流程。这个题目是我综合多个竞赛出题风格编出来的,难度贴近新手赛的入门题。
4.1 拿到题目后的初步观察
某场比赛给出如下密文片段:
ZNKIGYVGU SGQZYK ZNKIGYVGU ZU ZNK ULXK ZNK HXOZOUT题目提示说“这是一句英文句子,密钥是一个英文单词”。第一眼看到,密文是分段的大写单词,长度并不长,但已经有明显重复片段。
我先数一下字母数量,把空格去掉后得到34个字母。然后找重复子串,"ZNKIGYVGU"出现了两遍,第一次在第1个位置,第二次在第13个位置(从0开始算,也可以看到两个重复串相隔12个字符)。根据卡西斯基测试,密钥长度很可能是12的因数,比如2、3、4、6、12。
但这么短的文本,卡西斯基测试只有一次重复,说服力不够。接着用重合指数法。注意,重合指数需要一定文本量,短文本的结果只作为参考,不要直接放弃多种可能性。
4.2 试探密钥长度与字母频率
我用Python对可能的长度(2、3、4、6、12)分别计算平均IC,得到的数据大致如下:
| 密钥长度 | 平均IC |
|---|---|
| 2 | 0.045 |
| 3 | 0.050 |
| 4 | 0.055 |
| 6 | 0.063 |
| 12 | 0.052 |
其中密钥长度等于6时的IC最接近0.065,于是优先假设密钥长度是6。然后把密文每6个字符分成一组:
- 第0组:位置0,6,12,18,24,30
- 第1组:位置1,7,13,19,25,31
- 等等
统计每组出现频率最高的字母,再按E还原。这一步试算后得到候选密钥可能是"SECRET"。你可以实际验证一下:密钥长度是6,S、E、C、R、E、T,每个字母对应一个凯撒位移。这个密钥显然是个字典里的常见单词,CTF很喜欢用这种套路。
4.3 验证密钥并整理最终结果
用密钥"SECRET"去解密,得到的结果是:
THISISASECRETMESSAGEFORTHEZEBRA加回空格后正好是密文去除空格前的单词顺序:
THIS IS A SECRET MESSAGE FOR THE ZEBRA到这里题目就解完了。整个过程里最有价值的一点是,我们不是直接猜测密钥,而是通过IC把密钥长度从2、3、4、6、12中筛出来,再把单组频率分析用上,让每一步都有依据。短文本里IC偶尔会给出错误排序,这时候不要迷信,可以结合字典猜词和人的判断,候选密钥"SECRET"一眼就合理。
5. 常见问题与排查技巧实录
做CTF题或者自己复现时,几乎每个人都会遇到下面几个典型问题。我把它们整理成一张速查表,并附上实操经验。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 解密结果完全乱码 | 密钥长度猜错了 | 用重合指数多试几个长度,或者用卡西斯基测试交叉验证 |
| 明文前半段可读,后半段乱 | 密钥长度比实际短,或者密钥下标处理漏了非字母字符 | 将密文里的空格、标点移除后再试 |
| 某几组密钥字母特别怪 | 分组内字符数太少 | 用卡西斯基检验替代最高频字母法,或收集更长的密文 |
| 加密与解密结果不对称 | 解密时减出负数没有加26处理 | Python里写成(p - k) % 26,自动处理负数,不要手动判断 |
| 在线工具解出的密钥和我算的不一致 | 明文语言不一定是英文 | 换成英文频率表,或者检查是否启用了非标准字母表 |
5.1 密钥长度判断困难
密文长度短于100字符时,卡西斯基测试经常只出现一两次重复,甚至没有重复。这时我的经验是:把密钥长度上限设小一点,比如10到15,然后对每个候选长度算IC,不只看平均值,还要看每一组IC是否有波动。如果某一组明显偏高,其他组接近随机,也可能是长度正确但文本刚好有巧合。
另一种补救办法是直接考虑上下文。CTF题目给出的密文往往是英文短句,密钥往往有实际含义,比如"KEY"、"CTF"、"PASSWORD"或"SECRET"。当你算出的候选密钥看起来是一个有意义的英文单词时,正确的可能性很高。我经常在做题时先用脚本得到2到3个候选长度,再结合词典词表做语义验证,比纯数学折腾快很多。
5.2 频率分析结果异常的应对策略
频率分析的前提是分组内字符数足够多。如果一组只有四五个字符,那最高频字母的随机性极大,不要直接相信。解决方法是改用卡方拟合:对每组尝试26个偏移,计算偏移后的字母频率和英文自然分布之间的卡方距离,距离最小的那个偏移就是当前组的密钥位移。这个方法在小样本下比“最高频字母=E”稳定得多。
更进一步,如果题目给了密钥长度,但你解出的明文里还有零星的错字母,可以手工微调。比如某个词看起来应该是"ATTACK",解出来是"ATTAQK",那很可能是Q这个字母所在分组的密钥错了一位。你只需要把Q改成C,反推密钥偏移,再用新密钥整体重解一遍,往往能修正多处错误。
5.3 维吉尼亚密码的常见变种与识别方式
CTF题目不会总是原汁原味的经典维吉尼亚。你可能会遇到自动密钥维吉尼亚(Autokey Vigenère),它把明文本身作为后续密钥的一部分,破解时卡西斯基测试的结果会不同,因为密钥流不再周期循环;也可能遇到Vernam密码,它把密钥与明文逐位异或,虽然是二进制运算,但思路和维吉尼亚很像;还有一种是把字母表顺序打乱后的“自定义表格”维吉尼亚,这时标准频率分析会失效,只能靠已知明文或选择明文攻击。
认出变种的方法很简单:先用标准工具解密,如果所有候选密钥长度都无法让平均IC接近0.065,或者解出的明文语义完全不通,就要怀疑它可能不是普通维吉尼亚。此时多留意题目描述里的关键词,比如"autokey"、"grille"、"progressive",这些往往是解题方向的直接提示。CTF里最怕的不是变种难,而是把变种当成普通维吉尼亚一直死磕。
5.4 我的实操心得与避坑建议
最后分享几个我踩过坑后总结出来的习惯。第一,任何维吉尼亚题目,动手前先去掉所有空格标点,并且统一大小写,否则写脚本时容易出现下标错位。第二,重复子串搜索时要同时考虑长度3、4、5的不同尺子,有些变种会故意在两段相同明文中间插入不同的密钥片段,干扰卡西斯基测试。第三,如果解密结果里出现一个不合理的高频字母,并且调整密钥后依然如此,立刻检查是不是把I和J、U和V合并处理了,某些古典密码变体使用I/J合并表,字母表只有25个字符。
另外,CTF比赛时间很紧,我一般会使用在线解密平台做初步验证,但最终提交前一定用自己写的脚本重新解一遍,避免平台处理空格的方式和题目格式不一致导致密文答案被误判。维吉尼亚密码本身不难,真正拉开差距的是这些细节,多写几次加解密,手感自然就有了。
说实话,这个密码真正迷人的地方在于,你不需要懂得高深的数学也能破解它,你只需要建立一种“用统计说话”的敏感度。我在初学CTF时,靠维吉尼亚题第一次体会到把乱码还原成英文句子的成就感。希望你也能按这个流程动手试一遍,先把加解密脚本跑通,再找一段密文做一次完整破解,这样下次遇到任何多表替换类题目,都会多一份从容。