1. 从一道“看起来很简单”的题说起
如果你刚开始接触CTF(Capture The Flag)竞赛,密码学方向大概率是你最先碰到、也最容易卡住的一类题目。它不像Web题那样有直观的页面交互,也不像逆向题那样能直接丢进调试器里跑,密码学题目往往只给你一段密文、一个加密脚本,或者一个看似简单的数学问题,然后要求你找出明文或者密钥。很多人第一次遇到这类题的时候,反应基本是“这什么东西”,然后就开始漫无目的地搜索,最后要么放弃,要么抄了个脚本但完全不知道自己在干什么。
我写这篇东西的目的很直接:把CTF密码学题目从“看到就懵”变成“有一套可复用的分析流程”。不管你是刚打完第一场校内赛的新手,还是已经做过几道基础题但总觉得不成体系的进阶玩家,这里的内容都能帮你把零散的知识点串成一条线。核心关键词就几个:编码识别、古典密码、现代密码基础、工具链、解题思维。我会从最基础的编码与古典密码讲起,一路延伸到RSA、AES这些现代密码在CTF里的常见考法,每一步都配上我实际做题时用的思路和踩过的坑。
需要提前说明的是,CTF密码学题目和真实的密码学工程之间有相当大的差距。竞赛题目为了可解性,往往会刻意削弱安全性,比如用很小的素数、固定的IV、可预测的随机数种子。所以你在CTF里学到的“攻击方法”,放到真实生产环境里大概率是不成立的。但这不影响它的价值——通过解题,你能真正理解加密算法内部的运作机制,这种理解在后续做安全开发或者代码审计时非常有用。
2. 密码学题目的整体分类与识别思路
2.1 先搞清楚你面对的是哪一类题
CTF密码学题目大致可以分成这么几个层次,从易到难排列:
- 编码类:严格来说不算加密,但经常被归到密码学方向。Base64、Hex、URL编码、Unicode转义、摩斯电码这些都属于这一类。特征是给你一串看起来“有规律但读不懂”的字符。
- 古典密码类:凯撒、维吉尼亚、栅栏、培根、仿射、Playfair等。特征是加密脚本很短,或者题目描述里暗示了“移位”“替换”之类的词。
- 现代密码基础类:RSA、AES、DES、流密码。特征是给你公钥、密文、加密脚本,要求你恢复明文或私钥。
- 数学/数论类:离散对数、椭圆曲线、格密码。这类题目通常需要一定的数学基础,但CTF里出现的往往是简化版本。
识别题目类型是第一步,也是最关键的一步。我的习惯是拿到题目先看三个东西:题目描述、附件文件类型、密文长度。如果附件是一个Python脚本,先通读一遍,看它调用了哪些库、用了什么算法、有没有明显的漏洞(比如随机数种子固定、素数太小)。如果附件只有一段文本,先尝试常见的编码解码,再根据字符集特征判断是不是古典密码。
2.2 一个实用的判断流程
我一般会按下面的顺序快速过一遍:
- 看密文字符集。只有大小写字母和数字,可能是Base64或Base32;只有0-9和a-f,大概率是Hex;包含大量等号和加号斜杠,基本就是Base64。
- 看长度。Base64编码后的长度通常是4的倍数;Hex长度是偶数;古典密码的密文长度往往和明文长度一致或成倍数关系。
- 看题目给的提示。如果题目名叫“Caesar is my friend”,那不用想了,直接凯撒暴力枚举。如果叫“RSA beginner”,那就去看公钥和密文。
- 跑一遍自动化识别工具。我常用的是
CyberChef的Magic功能,或者本地的ciphey工具,它能自动尝试多种编码和古典密码。但注意,工具只是辅助,最终还是要你自己理解每一步在做什么。
提示:不要一上来就丢给自动化工具。先手动判断一遍,哪怕判断错了,这个过程本身就在训练你的直觉。工具跑出来的结果如果看不懂,那说明你还没真正解出来。
2.3 为什么分类能力比具体算法更重要
我见过很多新手,背了一堆RSA的攻击方法,但拿到题目连“这是RSA”都认不出来。原因就是缺乏分类意识。CTF密码学题目的变化非常多,但万变不离其宗——它一定是在某个环节上做了削弱。你要做的就是找到那个削弱的环节。
举个例子,同样是RSA题目,有的给你很小的e和很大的明文(低加密指数攻击),有的给你两个模数共享一个素数(共模攻击),有的给你多组密文对应同一个明文(广播攻击)。如果你不知道这些攻击的存在,你可能会对着一个大整数发呆;但如果你知道分类,你就能快速定位到“这题考的是哪种攻击”。
3. 编码与古典密码的实操拆解
3.1 编码类题目的快速处理
编码类题目在CTF里通常出现在签到题或者热身题的位置。它们的解法非常固定,我整理了一个速查表:
| 编码类型 | 特征 | 解码方式 |
|---|---|---|
| Base64 | 大小写字母+数字++/=,长度4的倍数 | base64 -d或Pythonbase64.b64decode |
| Base32 | 大写字母+数字2-7,末尾多个= | Pythonbase64.b32decode |
| Hex | 只有0-9和a-f | xxd -r -p或bytes.fromhex |
| URL编码 | 大量%后跟两位十六进制 | urllib.parse.unquote |
| Unicode转义 | \u开头后跟四位十六进制 | 直接print或unicode_escape |
| 摩斯电码 | 点和划,用空格或斜杠分隔 | 查表替换 |
实际操作中,最麻烦的不是单一编码,而是多层嵌套编码。比如先Base64,再Hex,再URL编码。我的做法是写一个小脚本,循环尝试解码,直到输出看起来像正常文本为止。但要注意,有些编码解码后可能还是乱码,这时候需要人工判断是不是还需要进一步处理。
import base64 import binascii from urllib.parse import unquote def auto_decode(s): for _ in range(10): try: # 尝试Base64 if len(s) % 4 == 0 and all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' for c in s): s = base64.b64decode(s).decode('utf-8', errors='ignore') continue # 尝试Hex if all(c in '0123456789abcdefABCDEF' for c in s) and len(s) % 2 == 0: s = bytes.fromhex(s).decode('utf-8', errors='ignore') continue # 尝试URL编码 if '%' in s: s = unquote(s) continue except: break return s这个脚本很粗糙,但应付大部分签到题足够了。实际比赛中,我建议直接开CyberChef,它的Magic功能能自动识别多层编码,省去很多手动尝试的时间。
3.2 古典密码的手工与脚本解法
古典密码是CTF密码学里最有“解谜感”的部分。我挑几个最常考的说说。
凯撒密码是最简单的移位密码,暴力枚举26个偏移量就行。但有时候题目会做变种,比如只对字母移位、大小写分别处理、或者结合其他编码。我的习惯是先写一个暴力脚本,把所有偏移的结果打印出来,肉眼找可读的文本。
def caesar_bruteforce(cipher): for shift in range(26): plain = '' for c in cipher: if c.isalpha(): base = ord('A') if c.isupper() else ord('a') plain += chr((ord(c) - base - shift) % 26 + base) else: plain += c print(f"shift={shift}: {plain}")维吉尼亚密码是多表替换密码,需要知道密钥才能解。CTF里常见的考法是给出足够长的密文,让你通过频率分析或者已知明文攻击来恢复密钥。如果题目给了密钥长度提示,可以用卡西斯基测试或者弗里德曼测试来估计。但说实话,CTF里的维吉尼亚题目往往不会让你做完整的频率分析,而是会给出一些额外提示,比如“密钥是一个英文单词”“密钥和题目名有关”之类的。
栅栏密码是换位密码,把明文按固定字数分组后按列读出。解法就是尝试不同的栏数,看哪个输出可读。我一般写个循环,栏数从2到密文长度的一半,逐个尝试。
培根密码用A和B两种字符表示二进制,每5个一组对应一个字母。CTF里经常把A和B替换成其他字符对,比如大小写、奇偶、元音辅音。识别方法是看密文是否只有两种字符,且长度是5的倍数。
注意:古典密码题目有时候会故意用错误的英文拼写或者夹杂数字来干扰你。这时候不要慌,先把能识别的部分解出来,剩下的可能是填充或者提示。
3.3 古典密码的实战心得
我做古典密码题有一个原则:先假设它是最简单的变种,再逐步增加复杂度。比如看到一串字母,先试凯撒,不行再试维吉尼亚,再不行考虑是不是仿射或者Playfair。不要一上来就想着“这题肯定很难”,很多时候答案就藏在最基础的解法里。
另外,古典密码题目经常和编码结合。比如先凯撒移位,再Base64编码。这时候你需要反向操作:先Base64解码,再凯撒暴力。我建议养成习惯,每解一层就把中间结果保存下来,方便回溯。
还有一个坑是大小写敏感。有些题目在凯撒移位时只移大写字母,小写字母保持不变;有些则统一处理。如果你暴力枚举的结果看起来“差一点”,检查一下是不是大小写处理方式不对。
4. 现代密码题目的核心攻击面
4.1 RSA题目的常见漏洞与利用
RSA是CTF密码学里出现频率最高的现代密码算法。它的安全性基于大整数分解的困难性,但CTF题目为了可解,往往会在参数上做手脚。我总结了几种最常见的攻击类型:
| 攻击类型 | 触发条件 | 利用方法 |
|---|---|---|
| 小素数分解 | n很小(比如小于512位) | 用factordb或yafu分解n |
| 低加密指数 | e很小(如e=3)且明文短 | 直接对密文开e次方 |
| 共模攻击 | 同一n,不同e加密同一明文 | 扩展欧几里得求明文 |
| 广播攻击 | 同一明文,不同n但相同e | 中国剩余定理 |
| 因数共享 | 多个n之间有公因数 | 两两求gcd |
| 私钥泄露 | 给了d或p、q之一 | 直接解密 |
小素数分解是最基础的。如果n只有一两百位,直接丢到factordb.com或者用sage的factor()函数就能分解。我遇到过一些题目,n甚至只有几十位,手算都能算出来。
低加密指数攻击的典型场景是e=3,明文很短,导致m^3 < n,这时候密文c = m^3,直接开三次方就行。如果m^3略大于n,可以尝试c + k*n再开方,k从0开始枚举。
import gmpy2 def low_exponent_attack(c, e, n): for k in range(100000): m, exact = gmpy2.iroot(c + k * n, e) if exact: return int(m) return None共模攻击是指同一明文用两个不同的e加密,但n相同。这时候可以用扩展欧几里得算法找到s1和s2使得e1s1 + e2s2 = 1,然后m = c1^s1 * c2^s2 mod n。
def common_modulus_attack(c1, e1, c2, e2, n): g, s1, s2 = gmpy2.gcdext(e1, e2) if s1 < 0: c1 = gmpy2.invert(c1, n) s1 = -s1 if s2 < 0: c2 = gmpy2.invert(c2, n) s2 = -s2 m = (pow(c1, s1, n) * pow(c2, s2, n)) % n return m广播攻击是同一明文用相同的e但不同的n加密多组,可以用中国剩余定理合并。条件是e组密文,且e个n两两互素。
这些攻击的共同点是:它们都利用了RSA参数选择不当导致的漏洞。在实际CTF中,你需要先读题目的加密脚本,看它生成了哪些参数,然后判断哪个参数“不正常”。
4.2 AES与流密码的考法
AES在CTF里通常以两种形式出现:ECB模式和CBC模式。
ECB模式的问题是相同的明文块会产生相同的密文块。如果题目允许你控制部分明文,你可以通过构造特定的输入来逐字节恢复未知的明文。这种攻击叫ECB字节翻转攻击或ECB逐字节恢复。核心思路是:把未知明文放在你控制的明文后面,通过观察密文块的变化来推断未知字节。
CBC模式的问题主要出在IV(初始化向量)上。如果IV可控或者可预测,可以实施CBC字节翻转攻击,通过修改前一个密文块来影响下一个明文块的解密结果。还有一种叫Padding Oracle攻击,利用服务器对填充错误的反馈来逐字节解密。这种攻击在CTF里出现得不多,因为需要服务器有交互接口。
流密码在CTF里最常见的是异或加密。如果密钥重复使用,可以通过已知明文攻击或者频率分析来恢复密钥。我遇到过一道题,密钥是一个短字符串,重复异或加密了一段英文文本。解法就是利用英文空格的高频特性,先恢复出密钥的一部分,再逐步扩展。
def xor_attack(cipher, known_plain): key = bytes([c ^ p for c, p in zip(cipher, known_plain)]) return key4.3 现代密码题目的解题节奏
我做现代密码题目的节奏是这样的:
- 读脚本:先看加密脚本,理解它做了什么。重点关注随机数生成、参数选择、模式选择。
- 找异常:哪个参数特别小?哪个参数重复使用了?哪个地方用了不安全的随机数?
- 选攻击:根据异常选择对应的攻击方法。
- 写脚本:用Python的
gmpy2、pycryptodome、sage等库实现攻击。 - 验证:解出来的明文通常是有意义的字符串或者flag格式,如果不是,检查步骤。
提示:
gmpy2是处理大整数运算的利器,sage在数论和椭圆曲线方面更强。如果本地没有sage,可以用在线的SageMathCell。
5. 工具链与自动化脚本的搭建
5.1 我常用的工具清单
CTF密码学题目不需要太复杂的工具链,但有几个工具能极大提升效率:
- CyberChef:浏览器端的“编码瑞士军刀”,支持几乎所有常见编码和古典密码,Magic功能能自动识别多层编码。
- ciphey:命令行的自动解码工具,适合快速处理签到题。
- gmpy2:Python的大整数运算库,RSA题目必备。
- pycryptodome:Python的密码学库,支持AES、DES、RSA等。
- SageMath:数学计算环境,适合处理椭圆曲线、格密码等高级题目。
- factordb:在线整数分解数据库,小n直接查。
- yafu:本地整数分解工具,比factordb更灵活。
5.2 搭建一个可复用的解题脚本框架
我建议每个打CTF的人都维护一个自己的密码学解题脚本库。不需要多复杂,但要把常用的函数封装好。比如:
# crypto_utils.py import gmpy2 from Crypto.Util.number import long_to_bytes, bytes_to_long def rsa_decrypt(c, d, n): m = pow(c, d, n) return long_to_bytes(m) def rsa_encrypt(m, e, n): return pow(bytes_to_long(m), e, n) def gcd(a, b): return gmpy2.gcd(a, b) def egcd(a, b): return gmpy2.gcdext(a, b) def crt(remainders, moduli): # 中国剩余定理 pass这个框架的好处是,下次遇到类似的题目,直接调用函数就行,不用重新写一遍。我自己的脚本库里还包含了常见的RSA攻击函数、异或分析函数、频率分析函数等。
5.3 自动化与手工的平衡
虽然工具很强大,但我强烈建议不要完全依赖自动化。原因有两个:第一,CTF题目经常有变种,自动化工具不一定能识别;第二,手工分析的过程能帮你真正理解算法的弱点。
我的做法是:签到题和编码题直接用工具秒掉,节省时间;古典密码和现代密码题目先手工分析一遍,确定攻击思路后再写脚本实现。这样既能保证速度,又能保证学到东西。
6. 常见问题与排查技巧实录
6.1 解题卡住了怎么办
卡住是CTF的常态。我遇到卡住的时候,会按下面的顺序排查:
- 重新读题:是不是漏看了题目描述里的提示?是不是附件里还有隐藏文件?
- 检查中间结果:把每一步的输出打印出来,看看哪一步开始变得“不对劲”。
- 换工具:用CyberChef再跑一遍,看看有没有遗漏的编码层。
- 搜索特征:把密文的前几个字符或者题目的关键参数拿去搜索,看看有没有类似的题目。
- 放一放:如果卡了超过半小时,先去做其他题,回头再看往往会有新思路。
6.2 常见报错与解决方法
| 问题 | 原因 | 解决方法 |
|---|---|---|
ValueError: invalid literal for int() | 输入字符串包含非数字字符 | 检查是否有多余空格或换行 |
gmpy2.invert报错 | 模数不互素 | 检查n和e是否互素,或者是否选错了参数 |
| 解密结果乱码 | 解密方式错误或编码问题 | 尝试不同的解码方式,检查是否需要用long_to_bytes |
| 脚本运行超时 | 暴力枚举范围太大 | 优化算法,或者检查是否有更简单的攻击方法 |
factordb查不到 | n太大或太特殊 | 尝试yafu或sage的factor() |
6.3 几个容易踩的坑
坑一:忽略大小写和空格。古典密码题目经常在大小写和空格上做文章。比如凯撒移位只移大写,或者空格被替换成其他字符。解出来的结果如果“差一点”,检查这些细节。
坑二:RSA的e和phi不互素。正常情况下e和phi(n)是互素的,但有些题目会故意让它们不互素,这时候需要特殊处理,不能直接求d。
坑三:Base64解码后是二进制。有些题目Base64解码后得到的是二进制数据,不是文本。这时候需要判断是不是图片、压缩包或者其他文件格式。我一般用file命令或者看文件头。
坑四:异或密钥长度判断错误。异或加密的密钥长度如果不确定,可以尝试用汉明距离或者卡西斯基测试来估计。不要盲目假设密钥长度。
提示:CTF密码学题目做多了之后,你会形成一种“直觉”——看到某种参数组合就知道大概考什么。这种直觉只能通过大量练习获得,没有捷径。
7. 从CTF到真实世界的思维迁移
CTF密码学题目和真实密码学工程之间有一条明显的鸿沟,但这条鸿沟并不是不可跨越的。我在实际工作中发现,CTF训练出来的几种能力非常有价值:
第一是参数敏感度。做过大量RSA题目后,你会对“小素数”“低指数”“重复模数”这些危险信号非常敏感。在代码审计时,你能快速发现不安全的加密实现。
第二是编码识别能力。CTF里经常需要处理各种编码嵌套,这种能力在分析日志、处理数据时非常有用。
第三是脚本化思维。CTF题目往往需要你快速写脚本验证想法,这种“想到就写”的习惯能极大提升工作效率。
当然,也要清醒地认识到CTF的局限性。CTF题目为了可解性,往往会简化问题,忽略真实环境中的很多因素(比如侧信道攻击、实现漏洞、协议交互)。所以不要把CTF里的攻击方法直接套用到真实系统上,而是要把它们当作理解密码学原理的练习。
我个人在实际操作中的体会是:CTF密码学题目最大的价值不在于“解出flag”,而在于“理解为什么这个flag能被解出”。每做完一道题,花几分钟想想“如果我是出题人,我会怎么防止这种攻击”,这种反向思考能帮你真正掌握密码学的核心逻辑。下次遇到类似的题目,你就不需要从头分析了,直接就能定位到关键点。