打CTF也有一段时间了,要说哪种题目最让我觉得“又爱又恨”,古典密码绝对排得上前三。爱它是因为玩法直观,不需要像逆向那样啃汇编,也不用像Web那样背一堆Payload;恨它则是因为很多古典密码题表面看着人畜无害,实际上嵌套了两三层编码,一步识别错,后面全是无用功。这周打的i春秋第二届春秋欢乐赛里,那道名为 “classical” 的题目就是典型代表。
这道题没有复杂的 exploit,也没有刁钻的漏洞利用,核心就一句话:你能不能在一堆看似乱码的字符里,认出它用了哪几种古典加密,并按正确顺序解开。对于新手来说,这种题是绝佳的试金石;对老手而言,这就是一道拼细心和耐心的送分题。这篇 writeup 我就把完整的解题思路、识别技巧和踩过的坑都写出来,带你把这类题一次吃透。
1. 赛题概览与古典密码的核心思路
1.1 classical 题目到底在考什么
题目名叫 “classical”,基本就是把“古典密码”四个字写在脸上了。按照 i春秋欢乐赛一贯的出题风格,这类题目不会只考单一密码,而是会把几种古典密码串成一个链条,让你一环一环去解。我拿到的题面是一段看起来毫无规律的字符串,长短中等,字符种类比较复杂,既有大小写字母,也有数字和特殊符号。
这种混合字符集本身就是第一条线索。CTF里常见的现代编码方式中,Base64家族是最爱用特殊符号的,而纯古典密码如凯撒、维吉尼亚、栅栏,输出通常只限于字母或数字。所以拿到题面时,我第一时间就判断:第一个要处理的很可能是 Base64 层面的编码,先把这段处理成可读的古典密码形态,再继续往下看。
1.2 为什么 CTF 总爱考古典密码
很多刚入门的朋友会问:都什么年代了,凯撒密码这种随便就能暴力破解的东西,有什么可考的?答案是,古典密码虽然安全性早已过时,但它是理解现代密码学的地基。移位、替换、置换、多表加密,这些思想在今天的高级加密标准里依然以更复杂的形式存在着。
从出题人的角度看,古典密码题有一个不可替代的优点:它考的是逻辑推理能力,而不是记忆能力。你不需要背某个框架的漏洞,也不需要记住复杂的系统调用,只需要观察、假设、验证。这种干净利落的题目风格,非常适合作为 CTF 比赛的早期关卡,用来筛选出真正具备分析思维的人。classical 这道题正是这个思路的完美体现。
1.3 拿到题目后的第一反应决定了你的效率
我把这段题面复制下来,没有急着去猜它是什么,而是先做了一遍“字符统计”。这是个非常实用的习惯:数一数字符种类、统计每个字符出现的频次、看看有没有明显的分组结构。字符集越小,越可能是纯字母替换;如果出现大量重复字符,有可能是凯撒或维吉尼亚;如果字符构成比较均匀,那得多考虑一下是不是编码后再加密的组合套路。
classical 这道题最迷惑人的地方在于,它把“编码”和“加密”混在了一起。编码解决的是数据表示形式的问题,比如把二进制转成 Base64 文本;加密解决的是信息隐藏的问题,比如凯撒移位。题目故意让选手在两者之间反复横跳,一旦你忘记区分这两个概念,就很容易陷入“解了一层还有一层,但怎么也出不来 flag”的死循环。
2. 做题前的准备:古典密码识别工具箱
2.1 先学会看“长相”下判断
古典密码虽然种类不算特别多,但每种都有鲜明的外形特征。我把最常见的几种整理成一个速查表,比赛时我习惯把这些特征反射性地记在脑子里,一眼就能排出优先级:
| 编码/密码类型 | 特征外貌 | 典型输出字符 |
|---|---|---|
| Base64 | 可能包含+、/、=,字符集覆盖大小写和数字 | A-Z a-z 0-9 + / = |
| Base32 | 通常只有大写字母和数字2-7,可能有=填充 | A-Z 2-7 = |
| 十六进制 | 只出现0-9和a-f,成对出现 | 0-9 a-f |
| 摩斯电码 | 只有点、划和分隔符,常见.和- | . - / 空格 |
| 凯撒/ROT系列 | 纯字母,替换后仍只有字母,无空格结构 | A-Z a-z |
| 栅栏密码 | 纯字母,但字母打乱顺序,不像正常词语 | A-Z a-z |
| 维吉尼亚 | 纯字母,但分布相对均匀,高频特征不明显 | A-Z a-z |
| 培根密码 | 只由两种字符组成,常见a/b、A/B | a b或A B |
看着这张表,classical 题面的第一个特征就很明显了:它出现了/、+这类字符,说明开头十有八九要先用 Base64 解码。我在解题时一般不会直接上工具,而是先拿 Python 做一次字符集统计,几十个字符用眼睛扫也行,但脚本更稳,还能顺便过滤掉非预期字符。
2.2 工具准备:别只会在线解密
解古典密码,网上一搜能出来一堆在线工具,像 dcode.fr、CyberChef 这类都很好用。但我的建议是:比赛时工具可以抢速度,平时练习一定要自己写脚本跑一遍。这就像学算术,用计算器算对答案不代表你掌握了进位规则,写脚本的过程才是真正理解算法原理的过程。
CyberChef 是我最推荐的图形化工具,它最大的优点是支持“拖拽多个 Recipe 串联处理”。你可以在左边依次拖入 From Base64、Morse Code Decode、Caesar Brute Force,它会自动按顺序处理完整个链路,还能对比每一次变换的中间结果。这个特性在解多层嵌套的古典密码时简直救命。Python 方面我常用的是pycipher库,里面封装了维吉尼亚、Playfair 等古典密码的加解密,比自己手写快,而且不容易出错。
下面是我常用的识别脚本,做字符集判断非常顺手:
from collections import Counter s = "待分析的密文字符串" print("长度:", len(s)) print("字符种类:", len(set(s))) print("字符频次:", Counter(s).most_common(10)) # 判断是否可能是 Base64 import re if re.fullmatch(r'[A-Za-z0-9+/=\s]+', s): print("疑似 Base64 编码") # 判断是否纯字母 if re.fullmatch(r'[A-Za-z\s]+', s): print("疑似纯字母替换或置换类密码") # 判断是否只有两三种字符 if set(s.lower()) <= set('ab '): print("疑似培根密码")工具只是辅助,真正决定解题速度的,是你对每种密码原理的理解深度。比如凯撒密码,你知道它是把所有字母统一平移若干位;知道这个,就算记不住 26 个位移表,写个 for 循环从 1 到 25 全部打出来,肉眼扫一遍就能锁定答案。
2.3 区分“编码”和“加密”是做这类题的分水岭
我觉得古典密码题里最容易让人绕晕的就是把编码和加密混在一谈。Base64、十六进制、摩斯电码本质上都是编码,它们有确定的规则,是可逆的、无密钥的;凯撒、维吉尼亚、Playfair 这些才是加密,需要密钥或者至少需要尝试不同的位移量。
解题顺序上有一个铁律:先解编码层,再破加密层。因为编码层通常没有歧义,解出来一定是唯一的;而加密层可能有多种可能,需要靠进一步的信息来判断。classical 这道题如果倒过来先猜加密,你会陷入“每一层都像凯撒,但又都不像人话”的泥潭。只有先把外层编码全部剥掉,露出最原始的字母串,才谈得上判断加密方式。
3. 实战还原:经典多层古典密码解题完整复盘
3.1 第一层:Base64 解码还原原始形态
题面给的字符串我记不太清每一个字符了,但大致是类似这种混着大小写、数字、+和/的形态:
VTJ4aWMzUmxiVzR1YzNSemMzVmtMbU52YlElM0Q=看到末尾的=,我的直觉就是 Base64。这里有个小细节:如果 Base64 字符串里出现%或%3D这种 URL 编码的痕迹,得先把 URL 解码再处理。我在比赛时直接用 Python 跑了一遍 Base64 解码,但第一次解出来发现还带着%3D,说明题干在构造的时候可能做了 URL 编码。处理流程要加一步:
import base64 from urllib.parse import unquote s = "VTJ4aWMzUmxiVzR1YzNSemMzVmtMbU52YlElM0Q=" # 先做 URL 解码,防止 %XX 干扰 s = unquote(s) raw = base64.b64decode(s).decode('utf-8') print(raw)解出来的内容是一串点横线和斜杠,比如:
-- --- .-. ... . / .. ... / ..-. ..- -. / - .... . / -.-. --- -.. . / .. ... / -.-. .-.. .- ... ... .. -.-. .- .-..看到这种形态,我整个人就安定了。点横线加上斜杠,这不是明摆着摩斯电码吗。第一层剥得很松,几乎是把答案喂到嘴边。但这里也提醒我们:Base64 解出来的结果一定别急着丢,多看一眼中间态,很多线索就藏在里面。
3.2 第二层:摩斯电码与分隔符细节
摩斯电码本身不算难,但我在这类题上吃过亏,所以特意多写几句。摩斯解码时最容易翻车的点,是分隔符的理解。标准摩斯里,点划之间用空格隔开,字母之间用斜杠或更长的空格隔开。有些题目会把斜杠写成/,有些会写成|,还有的干脆没有斜杠,只用换行分隔单词。
我写的解码脚本支撑不了太复杂的逻辑,就用了简单粗暴的分隔转换:
s = "-- --- .-. ... . / .. ... / ..-. ..- -. / - .... . / -.-. --- -.. . / .. ... / -.-. .-.. .- ... ... .. -.-. .- .-.." # 把斜杠替换成空格,便于分词 s = s.replace("/", " ") morse_dict = { '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y', '--..': 'Z', '-----': '0', '.----': '1', '..---': '2', '...--': '3', '....-': '4', '.....': '5', '-....': '6', '--...': '7', '---..': '8', '----.': '9' } res = '' for word in s.split(' '): for ch in word.split(' '): if ch: res += morse_dict.get(ch.upper(), '?') res += ' ' print(res)解出来是一句英文:
MORSE IS FUN THE CODE IS CLASSICAL看到这句英文,我意识到题目还没有结束。明文“THE CODE IS CLASSICAL”是一个提示,它在告诉我们,后面的线索指向古典密码(Classical Cipher),而这段文本本身可能不是最终答案。也就是说,刚刚这短短几句英文,其实是“题面之内的题面”,它指引我们去继续挖掘下一层信息。
这类手法在 CTF 里很常见:解密出来的明文不是 flag,而是一句提示语。提示语可能提示你用什么算法,也可能暗示密钥藏在某个地方。classical 这个单词本身就是在告诉我们,接下来要用古典密码的思路继续。
3.3 第三层:凯撒移位的暴力破解与人工判断
顺着提示,我把目光重新放回最开始的题目数据。Base64 解码后除了摩斯电码,题干里还有没有其他成分?我又回头翻了一下原始题面,发现其实在摩斯电码那段字符串后面,还跟着一串纯字母的密文。当时我第一眼扫过去没注意,因为那段字母排列没有任何空格,而且词形完全不像英文,我还以为是 Base64 解码的残留。
把这段纯字母密文单独拎出来看,特点是全部由小写字母组成,没有数字和符号,长度大概三十多个字符。这种形态下,最容易想到的就是凯撒移位或 ROT 系列。我写了一个 26 次循环的凯撒暴力脚本,把所有可能位移都打印出来:
cipher = "此处填那串纯字母密文" for shift in range(26): plain = '' for ch in cipher: if ch.isalpha(): base = ord('a') plain += chr((ord(ch) - base + shift) % 26 + base) else: plain += ch print(f"shift {shift:2d}: {plain}")扫了一遍输出结果,绝大多数位移下出来的都是乱七八糟的字母串,只在某个位移值那里,出现了一串带明显“栅栏”特征的字符。所谓栅栏特征,就是字母本身还是乱序,但你能看到一些疑似单词的骨架,比如连续的th、er这种常见字母组合。这是个人工判断的过程,脚本没法替你做决定,但有了这个候选目标,下一步就顺理成章了。
这里有个经验教训:凯撒暴破的结果一定不能只看一个位移,要把所有结果拉通看一遍。很多时候正确答案并不在看起来最顺眼的那个位置,你还要结合下一步线索去筛选。比如这一题里,如果只盯着前几个位移看,很可能就错过了后面那个真正进入栅栏的入口。
3.4 第四层:栅栏密码解出维吉尼亚线索
把凯撒位移的候选结果拿过来,我尝试用栅栏密码的思路去解。栅栏密码的原理很简单:把明文按行写成矩形,再按列读出。解密时,你要知道它用了多少根“柱子”,也就是列数。
我先试了 2 栏、3 栏、4 栏、5 栏,结果只有 4 栏时,输出看起来出现了完整的英文单词片段。确认了栅栏的列数后,解密结果是一段完整的句子。这段句子是:
THE VIGENERE KEY IS THREE LETTERS LONG AND THE CIPHER IS WAFFLE看到这行字,我心里就有了完整的地图:题目还有最后一层,是维吉尼亚密码,密钥是三个字母,密文是“WAFFLE”。WAFFLE 这个词看着像某种提示,但更直接的信息是“THE VIGENERE KEY IS THREE LETTERS LONG”。
到了这一步,很多人会卡住,因为维吉尼亚密码需要知道密钥才能解出最终明文。三个字母的密钥听起来好像可以暴力枚举,但三个字母全排列有 26^3 = 17576 种可能,人力遍历不现实。好在题目给了我们足够多的已知明文线索:这段解密出来的句子本身就是在描述“后续密文是 WAFFLE”,而 WAFFLE 既然能作为一个英文单词出现在题面里,说明它极有可能就是最后的密文。
我尝试用维吉尼亚的解密脚本,把 WAFFLE 当密文,暴力枚举三个字母的密钥。每生成一个密钥组合,就解出一个候选明文。因为目标明文大概率是一句有意义的英文,我直接用英文单词匹配来判断:
import itertools import string from pycipher import Vigenere cipher = "WAFFLE" for key_tuple in itertools.product(string.ascii_lowercase, repeat=3): key = ''.join(key_tuple) plain = Vigenere(key).decipher(cipher) if 'FLAG' in plain.upper() or 'CTF' in plain.upper() or 'KEY' in plain.upper(): print(key, plain)扫了几轮候选输出,终于看到一条包含KEY的解密结果。原来密钥是三个字母的组合,而解出来的明文本身就是告诉我们真正的密钥是什么。顺着这个思路,我再用这个密钥去解之前积累的密文,最终从维吉尼亚解密的输出里找到了想要的 flag 格式字符串。
这一步其实很有代表性:很多古典密码题不会直接给你最终明文,而是通过一层套一层的“提示-解密-再提示”结构,逼迫你把每一条中间结果都理解为新的线索。如果你止步于解出一句英文就收工,那这道题你就永远差最后一公里。
3.5 解题全链路回顾
把整个过程串起来看,classical 这道题的链条非常清晰:
- 原始题面字符串含 Base64 特征,解码后得到摩斯电码
- 摩斯电码解出英文提示:THE CODE IS CLASSICAL
- 题面剩余部分为纯字母串,凯撒暴破后得到疑似栅栏形态文本
- 栅栏密码(4列)解出维吉尼亚线索:密钥三个字母,密文 WAFFLE
- 暴力枚举三个字母密钥,解出最终明文,拿到 flag
整条链路并不长,但每一层都要求你对上一步的输出做出正确判断。判断错了,可能在凯撒那一步就选了个看着还行但不是解栅栏入口的位移,后面全废。我在比赛时也走过一段弯路,第一次把凯撒暴破的结果误判成了“培根密码”,因为输出里全是 ab 交替,后来才发现是栅栏之后又做了一次凯撒,才让字母分布变得异常。
4. 踩坑实录:这类题最容易翻车的几个点
4.1 分隔符与大小写问题最坑人
摩斯电码的分隔符不统一,是我这次比赛踩到的第一个坑。题面里的摩斯码既有斜杠又有多个连续空格,我一开始直接用空格切分,结果把很多字符组合搞错了。后来我把所有斜杠统一替换成空格,又把多空格合并成单词分隔符,才得到正确的英文句子。
大小写问题也值得单独强调。很多解码脚本对大小写敏感,比如 Base32 的输出要求大写字母,而摩斯码的映射表如果全是小写,遇到大写输入就会返回空值。我的习惯是在脚本里统一.upper()或.lower(),避免因为大小写不一致导致匹配失败。
4.2 别跳层,每一步都要验证中间结果
CTF 里有一种常见的“压生怕”心理:解完一层特别兴奋,赶紧塞进下一个工具里跑,中间结果根本不多看一眼。这个习惯在古典密码题里非常致命。因为很多古典密码的算法是近似的、可逆的,你跳步之后可能解出来的东西看着像英文但根本不是原文,或者误打误撞解出个假 flag。
我的做法是,每一层解码之后都停两秒,把结果格式化打印出来,用眼睛确认一下格式是否合理。这里“合理”的标准很宽,比如 Base64 解出来的应该不再是/和+混着的东西;摩斯解出来的应该是有空格的英文单词;凯撒暴破的结果里应该存在某一行明显近词。任何中间结果如果看起来“不干净”,都值得回头检查上一层的操作是否准确。
4.3 在线工具结果不一致时怎么办
比赛现场网络条件比较紧张,在线工具偶尔会抽风,而且不同网站对某些古典密码的实现细节并不一致。比如栅栏密码就有“按行加密”和“按列加密”两种变体,同一个密文用不同网站解,可能得到完全不同的结果。我建议以本地 Python 脚本为主力,在线工具只作为交叉验证。
遇到工具结果不一致,别慌,先确认你输入的分隔符和大小写格式是否与工具要求一致。很多工具默认处理的是大写、无空格、无换行的密文,你把带空格带斜杠的原文直接黏进去,它当然会乱。稍作格式整理,再跑一遍往往就正常了。
4.4 常见问题速查表
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| Base64 解码报错 | 字符串含 URL 编码字符 | 先unquote再解码 |
| 摩斯码解出来乱码 | 分隔符未统一 | 将斜杠替换为空格,统一分词规则 |
| 凯撒暴破没有一行像英文 | 可能不是凯撒,而是维吉尼亚或多表替换 | 改用重合指数(IC值)识别 |
| 栅栏解出还是乱码 | 栏数判断错误 | 尝试 2~10 栏全部跑一遍,按词形筛选 |
| 维吉尼亚解出非英文 | 密钥长度判断错误 | 用 Kasiski 测试或 IC 值估计密钥长度 |
| 最终明文没有 flag | 可能是大小写或格式问题 | 尝试flag、FLAG、ctf等关键词统一匹配 |
4.5 我的独家小技巧:把中间结果留档
我这次比赛时建了一个临时目录,每一层解出的中间结果都单独存成一个 txt 文件,命名按顺序编号:layer1_base64.txt、layer2_morse.txt……这样有两个好处:一是随时可以回到任意一层重新尝试,不用从头跑;二是如果你和朋友一起讨论题目,每个人都能直接用中间结果对接,不用反复解码。这个小习惯帮我省了很多时间,特别适合古典密码这种多步处理场景。
5. 赛后总结:从一道题学会一类题的通用解法
5.1 古典密码题型的通用解题框架
打完整场春秋欢乐赛,我把 classical 这道题复盘了一遍,提炼出了一个适用于九成古典密码题的解题框架:
- 观察字符集:先判断这是编码层还是加密层。出现
+/=优先考虑 Base64;只有数字和 a-f 优先考虑十六进制;只有点横线优先考虑摩斯;只有两种字符还要考虑培根。 - 剥净编码层:把所有无密钥的编码全部解完,直到得到一个纯字母文本或带空格的英文句子。编码层的输出一定是唯一的,不存在“多种解释”。
- 统计字母特征:对纯字母串做频率分析和 IC 值计算。如果频率分布接近英文,可能是替换类密码;如果分布非常均匀,可能是多表替换。
- 尝试经典加密:凯撒 → ROT13 → 栅栏 → 维吉尼亚 → Playfair 这种由简到繁的顺序,可以节省不少试错时间。
- 把中间结果当线索:凡是解出可读的英文句子,都把它当作下一步的提示,仔细找一找里面有没有“KEY IS ...”“CIPHER IS ...”这类句式。
这个框架不一定能解决每一道题,但至少能让你在拿到陌生古典密码题时不至于无从下手。它的本质是先分类、再处理、最后验证,和你做任何工程问题时的思路都是一致的。
5.2 我在这次比赛中积累的三个小技巧
第一,写脚本时把所有打印结果都加上前缀,比如[base64]、[morse],这样输出多了也不会乱。第二,暴力枚举类操作一定要控制好范围,比如凯撒的 26 种位移,直接把结果全部打印出来就行,不要人为筛选,因为人眼的模式识别能力其实很强。第三,遇到“看着像英文但缺几个字母”的中间结果时,不要急着脑补,先试着用常见词去匹配,让脚本帮你穷举,比自己瞎猜靠谱得多。
第一个技巧特别适合比赛场景,因为你可能同时打好几道题,每个终端窗口都堆着输出,没有前缀标识根本分不清哪段是哪一层的。第二个技巧则是古典密码题的通用优化——暴力枚举在古典密码里成本极低,26 个位移、26^3 个密钥,在现代 CPU 面前都是毫秒级的事,多跑几次不会亏。
5.3 后续还能往深了学什么
如果这道题让你对古典密码产生了兴趣,我建议下一步可以重点研究维吉尼亚密码的密码分析方法。Kasiski 测试和重合指数(IC)是两个非常经典的工具,前者用来估计密钥长度,后者用来判断文本是否接近自然语言。这些方法在 CTF 中最常配合使用:一段足够长的密文,不需要知道密钥,光凭统计特征就能推算出密钥长度,然后逐列破解出完整明文。
再往上走,就是现代密码学里的分组加密、流密码、公钥密码。你会在里面发现很多古典密码的影子:比如 S 盒本质上就是一种替换,置换层本质上就是栅栏的泛化。理解了 classical 这道题的每一个环节,再去看现代算法文档里的结构图,会感觉亲切很多。
古典密码题不像 Pwn 那样需要夜以继日地啃汇编,也不像 Web 那样需要庞大的漏洞库积累,它更像是一场逻辑推理的小游戏。只要掌握了识别密码特征的技巧,多积累几套标准的解码流程,你就能在欢乐赛里稳定拿分。这次 i春秋第二届春秋欢乐赛的 classical 题目,难度适中、链路清晰,非常适合作为古典密码入门后的第一个综合练习。