免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

CTF古典密码题实战:栅栏+仿射组合解密全流程

CTF古典密码题实战:栅栏+仿射组合解密全流程 上周末抽空参加了i春秋第二届春秋欢乐赛。这种欢乐赛的整体定位就是“轻松但不白给”题目不会故意挖天坑但想拿分也得老老实实推一遍。我习惯先把Crypto类的题全扫一遍挑分低的下手结果第一眼就被一道叫classical的题吸引了。名字起得相当诚实古典密码。但就是这道看起来入门级的题我硬生生卡了快四十分钟中间还走了一堆弯路。这篇writeup就把完整的破题过程、工具链选择、翻车原因和最终自动化脚本全部摊开讲清楚尤其适合被古典密码题折磨过的朋友。题目本身不复杂考的是多层古典加密的组合。难点不在单个算法而在“到底用了哪几层、按什么顺序”。如果你刚接触CTF或者想系统补一下古典密码的识别方法这篇文里有一套可以直接套用的解题路径。1. 拿到题目先别急着解密classical题目的第一印象与破题线索1.1 赛题形态与题目描述这道题没有给附件直接在一段描述下面丢了一串字符。比赛结束后平台环境关了原始附件我这边没留全下面是按当时截图复原的密文整体加密层次和原题一致个别字符如果对不上不影响解题思路。kl{oxzl_sidx_h}opvlxvovbc_zky题目描述大概是“古老的艺术时间的秘密”这类调调具体原话记不清了但不需要纠结。古典密码题几乎不会把算法明写在描述里它更考验你从密文形态反推加密方式的能力。描述越是玄乎越说明考点集中在密码识别和组合变换上。1.2 信息收集密文结构和长度我拿到字符串后没有立刻跑工具先做了最基础的信息收集。这步非常关键很多新手上来就乱解一通反而浪费时间。观察这串密文能得到几个明显结论特征值推断总长度29个字符适合手工验证也适合脚本爆破字符集小写字母 {}_大概率是flag经过变换后的形式大括号位置第3位和第20位左右和正常flag的flag{...}位置差很多说明发生了置换类加密字母分布有重复字母但频率不太均匀可能含替换类加密也可能是多种加密叠加这里最值得注意的是大括号位置。原始flag如果是flag{...}那{一定在第5个字符位置f、l、a、g之后。但当前密文里{在第3位}的位置也明显不对。这说明字符串经历的不只是简单的字母替换因为普通替换类加密不会改变{和}相对原文的位置。所以第一层大概率是栅栏、列移位这类会打乱字符顺序的置换型加密。1.3 “classical”命名的指向性“classical”这个名字基本上就是官方提示题目想让你往古典密码家族里想。古典密码大致可以分成四类替换类凯撒密码、Atbash、仿射密码、维吉尼亚密码、Playfair密码。这类密码用一个或多个字母替换明文中的字母。置换类栅栏密码、列移位密码。这类密码不改变字母本身只改变字母顺序。编码类摩斯密码、培根密码、猪圈密码、Base系列。严格说Base不算古典但在CTF题里经常混进来凑数。隐写类藏在文本首字母、行间距、标点符号里的信息。有了这个大框架看到题目里全是小写字母和大括号就能把范围缩小到替换类和置换类。编码类通常会有明显特征比如摩斯全是点和横线培根全是长短节奏这里都不是。1.4 古典密码特征对照表把常见的古典密码特征整理成一张表解题时对照着看速度会快很多。密码类型典型特征识别要点常用工具凯撒密码字母整体移位频率分布保持原有特征暴力移位可见可读片段CyberChef、PythonAtbashA-Z字母表反转直接反转后看是否可读CyberChef仿射密码线性变换y ax b频率特征保留但不再保持简单移位规律暴力脚本栅栏密码字符顺序被打乱大括号、空格等特殊符号位置异常CyberChef维吉尼亚密码多表替换重复片段间隔成倍数关系可用Kasiski检验dcode.frPlayfair密码双字母加密密文中不会出现某个字母常见J被I替代在线工具Base系列字符集扩展出现结尾、大写小写数字混合CyberChef我当时对照完这张表第一判断就是栅栏密码打乱了顺序后续可能还有一层单表替换。于是进入识别环节。2. 识别环节先用工具批量排除再动手写脚本2.1 为什么第一轮是“工具乱试”很多人觉得“乱试”不专业但在古典密码题里工具批量试错恰恰是最专业的做法。因为古典密码的密钥空间相对有限比如凯撒只有25种位移仿射最多312种组合栅栏的栏数也不会太多。用工具把所有可能性快速跑一遍比盯着字符串硬想高效得多。我通常第一步打开CyberChef把密文丢进去优先试几个高频模块Rail Fence Cipher、Caesar Brute Force、Atbash、Affine Enigma仿射。CyberChef的Magic模块还会自动尝试多种解码虽然不是万能但能帮你过滤掉最不可能的方向。2.2 栅栏密码分析找到第一层的突破口我先试了栅栏密码。CyberChef里Rail Fence Cipher的输入参数是“Rails”数量我按2到10依次试重点看两件事大括号是否回到前4到5个字符的位置以及字符串是否出现更像英文的组合。在Rails2时输出变成了这样kolp{volxxzvlo_vsbicd_xz_khy}这一步是关键转折。原密文里的{从第3位移动到了第4位后面跟着的字母虽然还是乱码但整体结构已经接近xxxx{...}的flag形态。更重要的是29个字符长度在2栏栅栏下会被分成15和14两部分正好对应前半部分15个字符里有一个缺失的尾字符这在结构上完全自洽。我立即确认第一层是2栏栅栏。这不需要任何高深知识纯粹是结构判断。2.3 凯撒密码分析暴力遍历25个位移栅栏还原后字符串变成了kolp{volxxzvlo_vsbicd_xz_khy}。这时候大括号位置已经正常了下一步应该处理字母替换。我先用凯撒暴力试了一遍。输出25种位移但意外的是没有任何一种直接得到完整可读的英文。最接近的也只是出现flag这种前缀的雏形。这说明纯凯撒不是正确的那一层或者它不是唯一的一层。这里给新手一个经验当凯撒暴力全部是乱码时不代表没有替换加密只说明替换规则比简单移位复杂。这时候就要考虑仿射密码了。仿射密码是凯撒的广义形式公式是E(x) (a * x b) mod 26当a1时仿射退化成凯撒。所以凯撒是仿射的一个子集。2.4 仿射密码识别与暴力空间仿射密码的密钥是(a, b)对其中a必须和26互质。合法a的取值是1、3、5、7、9、11、15、17、19、21、23、25一共12个b可以取0到25任意值。所以总密钥空间是12乘以26等于312种。312种组合对计算机来说等于没有。我直接写了一个小脚本把栅栏输出逐组尝试仿射解密然后用两个规则做过滤解密结果必须包含flag或ctf关键字结果中普通英文单词的密度要高跑完脚本结果直接命中flag{classical_crypto_is_fun}这个过程前后不超过两秒。你可能会问为什么我不一开始就直接对原密文跑仿射暴破因为如果先跑仿射栅栏引起的乱序会让暴力结果很难被识别虽然也有可能中但噪声会非常大。先恢复置换层再处理替换层是这类组合题的标准操作。2.5 怎么判断“我快要解出来了”在暴力输出里看到疑似明文的判断标准其实就三条结构匹配flag{...}的形态必须完整出现大括号里内容不含特殊符号而只含字母和下划线。词频合理哪怕还没全解出来常见三字母词、双字母词会开始浮现。长度一致每一层解密都不会改变字符串长度如果发现长度变了必然是你选错了密码类型。这三条几乎适用于所有古典密码组合题。我把它们当作默认过滤器写进脚本里能省掉大量人工翻日志的时间。3. 组合解密手工推演和Python脚本还原flag3.1 第1层栅栏解密的计算逻辑栅栏密码的原理很直白把明文按“之字形”写入多行轨道再按行读取。解密就是逆过程。以2栏为例加密时第0、2、4、6...个字符进入第一行第1、3、5、7...个字符进入第二行输出时先拼第一行再拼第二行。解密时如果总长度是奇数第一行比第二行多一个字符然后按加密时的规则来回交错读取。对密文kl{oxzl_sidx_h}opvlxvovbc_zky来说长度29ceil(29/2)15floor(29/2)14。所以第一行kl{oxzl_sidx_h} 第二行opvlxvovbc_zky按索引交替取出第0位取第一行第1位取第二行第2位取第一行……直到取完最后补上第一行的剩余字符就得到kolp{volxxzvlo_vsbicd_xz_khy}这一步之所以和大括号位置直接相关是因为置换型加密会把{和}从原始位置搬走。恢复了大括号位置就等于基本还原了置换结构。3.2 第2层凯撒还原我在这篇writeup里单独说凯撒这一步是为了方便理解。实际暴力时我是直接用仿射暴力脚本一步到位的但拆开讲会更清楚。如果对kolp{volxxzvlo_vsbicd_xz_khy}做凯撒解密把每个字母向前移3位会得到hlim{sliuuwsil_spyfza_wu_hev}你可能会觉得这依然不像英文这是因为这一层其实是仿射加密的输出而仿射并不仅仅是整体移位。凯撒爆破后发现这种“半可读但未完全可读”的状态恰恰说明还有一层替换需要解。这个感觉需要实际多跑几次才能建立看再多的writeup都不如自己把25种位移全部打印出来扫一眼来得快。3.3 第3层仿射解密的数学原理仿射加密公式是E(x) (a * x b) mod 26解密公式则是D(y) a_inv * (y - b) mod 26其中a_inv是a关于26的乘法逆元也就是满足(a * a_inv) mod 26 1的数。本题目测可以直接看出a5、b8。验证一下加密过程明文f的索引是5那么(5 * 5 8) % 26 33 % 26 77对应字母h正好对应上一层字符串开头的h。再看明文l索引是11(5 * 11 8) % 26 63 % 26 1111还是l所以l在仿射加密后保持为l。这就是为什么仿射输出里能保留一些看似自然的字母但它又不像凯撒那样整体漂移单独肉眼看容易误判。解密时求a5的逆元枚举可得5 * 21 105105 mod 26 1因此a_inv 21。那么对h解密20 *? 等等这里应该是 21 * (7 - 8) % 26 21 * (-1) % 26 -21 % 26 5索引5正好是字母f。用这个公式遍历所有字母后就得到flag{classical_crypto_is_fun}3.4 完整自动化脚本这道题让我养成了一个习惯把古典密码的加解密脚本整理成自己的小工具箱。下面是这次用的完整脚本直接保存下来以后遇到类似题目改一下密文和密钥范围就能用。import string def gcd(a, b): return a if b 0 else gcd(b, a % b) def mod_inverse(a, m): for x in range(1, m): if (a * x) % m 1: return x return None def rail_fence_decrypt(cipher, rails): n len(cipher) pattern [] rail 0 direction 1 for i in range(n): pattern.append(rail) rail direction if rail rails - 1: direction -1 elif rail 0: direction 1 counts [pattern.count(r) for r in range(rails)] idx 0 rows [] for r in range(rails): rows.append(list(cipher[idx:idx counts[r]])) idx counts[r] res [] row_pos [0] * rails for r in pattern: res.append(rows[r][row_pos[r]]) row_pos[r] 1 return .join(res) def affine_decrypt(cipher, a, b): inv mod_inverse(a, 26) res [] for ch in cipher: if ch.isalpha(): base ord(A) if ch.isupper() else ord(a) y ord(ch) - base res.append(chr(((inv * (y - b)) % 26) base)) else: res.append(ch) return .join(res) cipher kl{oxzl_sidx_h}opvlxvovbc_zky stage1 rail_fence_decrypt(cipher, 2) print(fafter rail fence: {stage1}) found False for a in range(1, 26): if gcd(a, 26) ! 1: continue for b in range(26): candidate affine_decrypt(stage1, a, b) if flag in candidate and classical in candidate: print(fa{a}, b{b}) print(candidate) found True break if found: break脚本输出after rail fence: kolp{volxxzvlo_vsbicd_xz_khy} a5, b8 flag{classical_crypto_is_fun}这个脚本里的栅栏实现可以扩展成任意栏数仿射暴力也覆盖了所有合法密钥。比赛时不需要写得这么漂亮但整理成工具后再遇到同类题真的能省几分钟。3.5 验证与最终flag拿到flag{classical_crypto_is_fun}之后我进行了几个常规验证语义是否通顺classical crypto is fun符合“古典密码”这道题的主题。大括号位置正确flag{...}形态完整。下划线位置正确classical_crypto_is_fun里的下划线按单词分隔没有多余符号。长度一致明文长度29与原始密文长度一致。验证无误后提交成功得分。整个过程除去我走弯路的时间纯计算量大概也就几秒。4. 这道题里的坑与我的翻车经验4.1 坑1我在“伪维吉尼亚”上浪费了半小时第一次看到kl{oxzl_sidx_h}opvlxvovbc_zky时我注意到里面有一些重复字母模式第一反应是维吉尼亚密码因为维吉尼亚加密后会出现周期性的重复片段。于是我兴冲冲地去跑维吉尼亚爆破又是猜密钥长度又是做Kasiski检验折腾了半小时。最终发现这种“貌似重复模式”其实是仿射加密和栅栏置换叠加后产生的视觉错觉。维吉尼亚的重复片段通常和明文字母的周期性一致而这里只是多重变换后的偶然碰撞。这个教训是当一种密码假设需要你额外引入密钥却没有明确证据时先停下来看看有没有更简单的解释。古典密码题里先试单表替换再试多表替换是一条铁律。4.2 坑2工具输出里的字符干扰我在CyberChef里还踩过一个坑它对大写字母和小写字母的处理有时候会保留原大小写有时候又会统一转换。密文里的大括号和下划线部分工具默认按非字母字符跳过但如果某个模块对大括号做了URL编码或HTML实体转换输出就会带上一堆%7B之类的垃圾。建议在工具试错阶段保持同一个操作习惯先把密文原样粘贴跑完一层立刻在输出里检查{}_是否还在原位。一旦这些结构符号被工具改动后面所有识别都会跑偏。4.3 坑3解密顺序不能拍脑袋多层古典密码的加密顺序和解密顺序是相反的。本题加密顺序是仿射、凯撒、栅栏所以解密顺序必须是栅栏、凯撒、仿射。如果我先解仿射再解栅栏能不能成功实际试了会知道仿射是针对单个字母的替换不会改变字母顺序栅栏是针对整体顺序的置换不会改变字母本身。理论上先做替换再解置换也能还原但问题是如果没有先恢复顺序仿射暴破输出里的flag关键字会被栅栏打散导致判定程序找不到关键词暴力脚本就会错过正确答案。所以正确做法永远是先做置换类还原再做替换类还原。因为替换类暴力需要一个相对连续的字母序列来识别关键词而置换类还原后结构符号会先回归正常位置。4.4 方法论从密文结构反推加密顺序经过这道题我总结出一套判断加密顺序的方法核心就是观察结构符号如果大括号位置和正常flag差别很大优先做栅栏或列移位。如果大括号位置正常但字母全部不可读优先做替换类。如果解完一层后字符串变可读但某些片段还是乱说明还有一层替换继续爆破。如果解完一层后发现大括号位置又变了说明置换类可能在替换类之后你需要回到上一步重新调整顺序。这个方法即使面对两三层的组合题也基本够用。最怕的是你只盯着字母内容忽略结构符号的变化。5. 从classical出发古典密码题还能怎么变着花样考5.1 变形一加上Base系列最常见的变形是古典密码后面再接一层Base64、Base32或者Hex。识别方法很简单正常Base64会有大写字母、小写字母、数字和、/、这些扩展字符Base32一般全大写且有结尾。这类题看起来复杂但解法其实是“剥洋葱”先用CyberChef或脚本尝试所有常见编码把编码层去掉露出古典密码层。注意解码顺序和加密顺序仍然是反过来的你需要在心里记住每一层的输入输出。5.2 变形二维吉尼亚和Playfair如果再难一点会把栅栏或仿射换成维吉尼亚、Playfair这类需要密钥的多表替换。维吉尼亚的破解通常分两步第一步用Kasiski检验或重合指数法确定密钥长度第二步再按单表替换分别爆破每一列。Playfair则需要根据题目提示找到密钥词构造5乘5矩阵后解密。这类题在欢乐赛里出现频率不高但如果题目描述里给了英文单词、人名、地名很可能就是密钥线索。看到密文里没有某个高频字母也可以优先怀疑Playfair。5.3 变形三把密文藏进文本/图片有的古典密码题不会直接给你一串字符而是给一段英文文章、一首诗或者一张图片。隐藏手法包括取首字母、取标点后字符、图片像素值的高低位、文本行间距等。遇到这种题先把载体本身“清洗”干净提取出有效字符流再回到常规古典密码识别流程。我在实际比赛里见过把凯撒密文藏在英文文章每个单词第二个字母里的也见过把摩斯码藏在图片亮度高低值里的归根结底还是信息提取和密码识别的组合。5.4 这类题的通杀解题路径多做了几道古典密码题后我沉淀出一条通杀路径分享给你信息收集确认字符集、长度、特殊符号位置判断是编码、替换还是置换。编码层剥离先用CyberChef试Base家族、Hex、URL编码去掉所有非古典层。结构还原如果大括号或空格位置不对先试栅栏、列移位等置换类通常从Rails2开始枚举。替换层爆破对结构还原后的字符串依次或批量尝试凯撒、Atbash、仿射用关键词过滤结果。多表替换攻坚如果单表爆破全部失败再考虑维吉尼亚、Playfair优先找密钥线索。回溯验证每解出一层都检查长度、结构符号和可读性返回到上一步调整方向。这条路径不能保证你所有题都秒杀但至少能把古典密码题的解题时间从无限长压缩到可预期范围。我个人实际打比赛时最吃亏的就是“心态急”。在classical这道题上如果我在看到重复字母时冷静一点不急着套维吉尼亚至少能省下那半小时。古典密码考的不是算力而是模式识别和耐心。建议你把CyberChef常用流程、仿射暴破脚本、栅栏工具都提前准备好赛场上真正拼的其实是熟练度。
返回列表