
最近在带几个刚入门的朋友刷题正好把 ctfshow 上的 JiaJia-CP-1 到 JiaJia-CP-3 这一组密码学题目从头到尾捋了一遍。CP 这个前缀在这里基本就是 Crypto 的缩写也就是 CTF 里常说的密码学方向。说实话密码学题目的门槛和 web、misc 不太一样它不需要你去构造复杂的请求也不需要你熟悉各种中间件更多时候拼的是对数学原理和编码规则的理解程度。很多 ctf入门 的朋友一看到一串大整数、一段 base64 或者一个压缩包就发懵不知道从哪下手。这一组题目的好处是难度是递进的三道题分别覆盖了不同的知识面很适合当成 ctf密码学 的入门练习。我打算把自己做题时踩过的坑、验证过的脚本、以及那些文档里不会写的经验整理出来不管你是刚接触 CTF 的新手还是想找一组题复习基础的老手应该都能从里面捞到点东西。1. 密码学题目的整体认知与解题思路设计1.1 从标题拆解这类题目的信息量拿到一组题我习惯先把标题读三遍。JiaJia-CP-1、CP-2、CP-3 这个命名方式本身就透露了不少信息。JiaJia 大概率是出题人的昵称或者题目系列的标识CP 是方向标记后面的数字是难度序号。在 ctfshow 这类平台上带数字后缀的系列题通常意味着知识点是平行或者递进的也就是说 CP-1 可能考的是编码识别CP-2 考的是异或或者古典密码CP-3 才上到 RSA 这类公钥密码。为什么要先做这个判断因为 CTF 时间宝贵你提前知道题目大概是哪个层次就能在心理上做好准备不至于拿着一道编码题去套 RSA 的脚本白白浪费十几分钟。这种命名规律并不是 ctfshow 独有的很多 ctf比赛 的密码学题都沿用类似的体系。我的经验是当题目标题里出现 CP、Crypto、crypto 这类字样时优先往数学和编码方向想出现 web 就去看注入和逻辑出现 misc 就去检查文件本身。方向判断对了后面的时间就是纯收益。反过来方向判断错了你写再多脚本也是南辕北辙。注意不要因为一道题的名字里有数字就认定难度一定递增有些出题人只是随手编号。真正的难度判断要看附件内容数字只是参考。1.2 密码学题目的通用解题框架做题多了以后我脑子里会有一个固定的检查顺序相当于一套排查清单。这套顺序是看附件格式 → 看数据特征 → 判断密码体制 → 选攻击方法 → 写脚本验证 → 提交 flag。前两步是纯观察不需要动脑但恰恰是很多新手跳过的步骤。他们一看是密码题就直接开始猜结果猜了半天发现附件里其实藏了一个 key.txt 或者注释。看附件格式这一步你要搞清楚拿到的是什么。是一个 .py 加密脚本是一个 .txt 里塞了一堆大整数是一个压缩包还是一个图片不同的格式对应完全不同的处理路径。加密脚本最友好因为它直接把加密逻辑告诉你了你只要逆着来就行纯数据文件最难因为你要靠经验去猜它用了什么算法。看数据特征这一步你要观察数据的长度、字符集和结构。全是 0-9 和 a-f那可能是十六进制全是大小写字母加数字和等号那可能是 base64是一长串没有换行的字母那可能是古典密码是几个几百位的大整数那基本锁定 RSA。判断密码体制就是根据前两步的观察结果缩小范围。这一步我建议大家建立一个自己的对照表把常见的编码和密码的特征记牢。表格这种东西在密码学里特别管用因为特征识别是模式匹配人脑对表格的记忆效率远高于对文字描述的记忆。数据特征可能的编码/密码第一步验证方法只有 0-9 和 a-f十六进制直接 hex 解码字母加数字加 号Base64长度是 4 的倍数时优先试只有 A-Z 和 2-7Base32长度是 8 的倍数一行无空格字母凯撒/维吉尼亚/栅栏试位移、看频率两个或多个大整数RSA看是否有一对 n、e长度相同的二进制串异或尝试单字节或重复密钥选攻击方法是最考验经验的一步也是最能拉开差距的地方。同样一道 RSA 题有人十分钟就出 flag有人卡一天差距往往不在脚本能力而在知不知道该用哪种攻击。这就像医生看病同样的症状有经验的医生一眼就知道是什么病新手要做一堆检查才敢下结论。经验从哪来从做的题和读的 writeup 里来。我建议大家在每个平台的密码学专题里都刷完至少一组系列题把常见的攻击手法过一遍形成肌肉记忆。1.3 环境准备与工具选型密码学题的实操离不开工具。我的主力环境是 Python核心库是 pycryptodome 和 gmpy2。pycryptodome 用来处理 AES、DES 这类分组密码gmpy2 用来做大整数运算和数论计算比如求逆元、开高次方、判断素性。这两个库装好百分之八十的 RSA 题都能对付。安装命令很简单pip install pycryptodome gmpy2如果你要处理更复杂的数论比如需要做格基规约或者大整数分解的进阶题那得考虑 SageMath。SageMath 内置了非常强的数论和代数学函数但它的安装体积大启动慢普通入门题没必要上。我的建议是先把 Python 加两个库用熟遇到确实搞不定的题再考虑上 Sage。在线工具方面CyberChef 是一个必推的东西。它把编码、解码、异或、进制转换、哈希、加密解密等几十种操作做成了可拖拽的流程非常适合快速试探和验证。很多题你在本地写脚本之前用 CyberChef 拖两下就能看出方向对不对效率极高。不过要注意CyberChef 适合做短数据的快速验证真到了需要批量处理或者写逻辑的时候还是得回到 Python。除了工具还有一个容易被忽略的准备项题目上下文。有些系列题的第一题本身没难度但它会给你一个提示比如一个自定义编码规则后面几题都靠这个规则。所以做系列题时一定要把第一题吃透别做出来就扔了。我见过有人把 CP-1 做出来后直接跳去做 CP-3结果卡住了回头才发现 CP-3 用到了 CP-1 里定义的编码表。2. 核心题型细节解析与实操要点2.1 编码与古典密码的快速识别CP-1 这个位置按照常见的出题习惯很可能是编码或者古典密码。这两类东西的特点是规则简单但变种多。编码类主要是 Base 家族和 URL、HTML 实体这类转义编码古典密码则是凯撒、维吉尼亚、栅栏、培根、猪圈这些。它们的共同点是靠特征就能识别难点只在于你是否见过那个变种。先说 Base 家族。Base64 的特征是字符集是大小写字母加数字再加 / 或者 -结尾常带 号补齐长度是 4 的倍数。Base32 的字符集只有大写字母和 2-7长度是 8 的倍数。Base16 就是十六进制。识别出来之后直接解码就行但要注意 Base64 有 URL 安全变体把 / 换成了 -遇到这种要手动替换后再解。我的习惯是写一个通用解码函数把常见的几种都覆盖进去import base64 def try_decode(s): # 尝试 base64 标准与 urlsafe 变体 for alt in [s, s.replace(-, ).replace(_, /)]: try: return base64.b64decode(alt * (-len(alt) % 4)) except Exception: continue return None这段脚本的价值在于自动补齐 号和尝试两种字符集能省掉大量手工试错。很多新手卡在解码上就是因为没处理 URL 安全变体和 padding 缺失这两个细节。古典密码这块凯撒是最基础的。它的本质是字母表上的循环位移位移量是 1 到 25。识别凯撒的方法是看字母频率分布如果一串字母的频率分布曲线整体移动了那大概率是凯撒。破解时不用一个个试写个循环遍历 26 个位移量把结果打印出来人工扫一眼找可读的就行了。维吉尼亚是凯撒的升级版用关键词控制位移频率分析会被打散破解要复杂一些通常需要用已知明文或者重复密钥长度分析来入手。栅栏密码是把明文按轨道数重新排列难度在于轨道数未知一般从 2 开始试。提示做古典密码题时先把有可能的明文语言确定下来。如果明文是英文那用英文频率表如果是拼音或者中文编码思路完全不同。别小看这一步语言判断错了频率分析再准也没用。2.2 异或类题目的处理技巧异或是我认为密码学入门里最值得吃透的一个操作因为它简单、常用而且 CTF 里出现的频率极高。异或有个非常关键的性质两次相同的异或会抵消也就是a ^ b ^ b a。这个性质决定了很多攻击思路比如已知明文的时候密文异或明文就能拿到密钥。CP-2 这个位置如果考密码异或的概率很大。异或题常见的形式有三种。第一种是单字节异或整段密文每个字节都异或同一个数。这种题的破解思路是爆破所有 256 种可能的密钥然后看哪个结果像可读文本。判断可读性可以用字符集过滤也可以用英文频率评分。第二种是重复密钥异或密钥长度大于一密文按密钥循环异或。这种题要先猜密钥长度可以用汉明距离法或者频差法猜出长度后再按单字节异或的方法逐位爆破密钥。第三种是已知明文攻击题目会给你一段已知的明文片段让你推出密钥再解出剩余部分。单字节异或爆破的脚本我写过很多遍核心就是暴力加评分def single_byte_xor_bruteforce(cipher): best None for key in range(256): plain bytes(b ^ key for b in cipher) score sum(chr(c).isalpha() or c 0x20 for c in plain) if best is None or score best[0]: best (score, key, plain) return best data bytes.fromhex(你的密文十六进制) score, key, plain single_byte_xor_bruteforce(data) print(key , key, plaintext , plain)这里的评分函数我用了最简单的字母加空格计数实际做题时如果你的密文里有大量数字或者标点可以把这个评分换成更细的英文频率表加权。评分函数的设计直接决定爆破的准确率这是异或题的一个隐形考点。重复密钥异或的密钥长度猜测我用的是汉明距离法。原理是当两个密文块用相同的密钥循环异或时把它们异或在一起密钥部分会抵消剩下的是两个明文块的异或。而正常英文文本的异或结果其汉明距离二进制下 1 的个数会明显低于随机数据。所以把密文按不同长度分组两两计算平均汉明距离距离最小的那个长度就是最可能的密钥长度。这个方法实测很好用密钥长度在 10 以内的题基本都能猜对。2.3 RSA 类题目的常见变种与识别特征CP-3 这种收尾位置上 RSA 是顺理成章的。RSA 是 CTF 密码学题里的重头戏可以说没有哪组系列题会绕开它。RSA 的核心参数是三个模数 n、公钥指数 e、密文 c。私钥由 p 和 q 两个大素数决定n 就等于 p 乘 q。基础的 RSA 题会直接给你 n、e、c然后 p 和 q 给的是小素数你直接分解 n 就行。但真正有意思的是那些参数有缺陷的变种这也是 CTF 里 RSA 题的主要出题思路。我把常见的 RSA 攻击手法和识别特征整理成了一张表这张表我建议每个做密码学的朋友都背下来。看到题目的参数特征直接对号入座选攻击能省掉大量试错时间。参数特征攻击方法核心原理p、q 相差很小费马分解相邻素数乘积可快速分解e 很小且明文很短低加密指数攻击直接开 e 次方多组 n 共用因子公约数攻击两组 n 求 gcd 得 p同一明文不同 n广播攻击中国剩余定理d 很小Wiener 攻击连分数逼近 e/ne 和 φ(n) 不互素有限域开方明文是 e 次幂费马分解的适用场景是 p 和 q 非常接近。原理是设 a 等于 p 和 q 的平均值的上取整那么 a 的平方减 n 就是一个完全平方数开出来就能得到 p 和 q。判断标准很简单如果你拿到的 n 不是特别大而且 p 和 q 的数量级接近先试费马分解。脚本大概是这样的from math import isqrt def fermat_factor(n): a isqrt(n) if a * a n: a 1 while True: b2 a * a - n b isqrt(b2) if b * b b2: return a - b, a b a 1 n 你的模数 p, q fermat_factor(n) print(p, q)公约数攻击是另一类高频题。它需要你手上有至少两组 n如果这两组 n 共用同一个素数因子那它们的最大公约数就是那个因子。这个攻击之所以常见是因为有些题目会故意生成一堆 n其中两个因为随机数生成缺陷共用了 p。检测方法就是把所有 n 两两求 gcd一旦结果大于 1 就说明中了from math import gcd def common_factor_attack(n_list): for i in range(len(n_list)): for j in range(i 1, len(n_list)): g gcd(n_list[i], n_list[j]) if g 1: return g return None低加密指数攻击针对的是 e 很小的情况比如 e 等于 3。如果明文很短满足 m 的 e 次方小于 n那密文 c 就等于 m 的 e 次方直接用整数开方就能还原明文。判断条件是比较 c 和 n 的大小关系如果 c 明显小于 n 且 e 很小先试这个。即使 m 的 e 次方略大于 n也可以尝试加上若干个 n 再开方这就是所谓的小指数爆破。注意RSA 攻击脚本里涉及大整数开方时一定要用 gmpy2.iroot 这样的精确开方函数别用浮点的幂运算浮点在几百位的数字上精度会直接崩掉导致结果差一点点就是错的。3. 实操过程与核心环节实现3.1 从附件到明文完整解题流程做题的完整流程我拆成六个阶段来讲。第一阶段是信息收集把拿到的所有东西列清楚题目描述、附件文件、可能的提示。第二阶段是数据预处理把附件里的数据提取成脚本能读的格式比如从文本里正则匹配出大整数从压缩包里解出文件。第三阶段是特征判断对照前面的表格确定密码体制。第四阶段是攻击实现写或者改脚本执行。第五阶段是结果验证把解出来的东西检查一遍是否可读、是否符合 flag 格式。第六阶段是提交。我拿一个典型场景举个例子。假设 CP-3 给的是一个 Python 加密脚本和一段密文脚本内容是标准 RSA 加密参数 n、e、c 都写在脚本里。第一步先读脚本看它的加密逻辑有没有额外处理比如有没有对明文做填充、有没有做 Base64 编码。第二步提取 n、e、c把它们转成整数。第三步看 n 的大小和 e 的值如果 e 是 65537那基本是标准题先尝试分解 n如果 e 是 3优先试低指数攻击如果发现脚本里给的 n 有两组那就试公约数攻击。分解 n 是基础题的核心。小素数直接试除几百位的则要看有没有特殊结构。题目给的小素数 n 一般用试除或者 Pollards rho 就能分解。我常用的是这个思路先用小素数表快速试除试不出再用 rho 算法。Pollards rho 的脚本网上很多但我要提醒一点rho 对大素数乘积的分解效率取决于因子的大小如果两个因子都很大rho 也会很慢这时候要考虑是不是该换攻击方法了。很多新手卡在分解上死磕其实问题根本不在分解而在没意识到这题该用别的攻击。拿到 p 和 q 之后就算私钥 d。d 是 e 关于 φ(n) 的模逆元φ(n) 等于 (p-1)(q-1)。这一步用 gmpy2.invert 一行搞定import gmpy2 p 你的 p q 你的 q e 65537 n p * q phi (p - 1) * (q - 1) d int(gmpy2.invert(e, phi)) c 你的密文 m pow(c, d, n) print(bytes.fromhex(hex(m)[2:]))最后一步把整数 m 转回字节。这里有个经典坑hex(m) 转出来的字符串可能奇数长度直接切片 [2:] 后长度不对解析会出错。稳妥的写法是用 gmpy2.digits 或者手动补零hex_str hex(m)[2:] if len(hex_str) % 2: hex_str 0 hex_str print(bytes.fromhex(hex_str).decode(errorsignore))这个补零的小细节我踩过不止一次坑。明明 d 算对了、明文 m 也解出来了但转字节时因为长度差一位一直报错排查半天才发现是十六进制字符串长度的问题。这种错误不会在文档里写但实操中非常常见。3.2 三道题之间的递进与知识串联JiaJia-CP 这个系列如果真是按难度递进设计的那三道题之间大概率存在知识串联。第一题可能是纯编码让你熟悉观察数据特征这个动作第二题引入异或或者简单密码考你写爆破脚本的能力第三题上 RSA考数学和攻击手法的组合。这种设计的好处是做完整组题你能把密码学入门的几个核心动作都练一遍。我做系列题有个习惯每做完一道就把用到的知识点、脚本、踩的坑记在一个单独的笔记里。三题做完笔记里就有了一套可复用的模板。下次遇到同类的题直接翻笔记调用脚本效率翻倍。这个习惯的长期价值特别大因为 CTF 里的很多题本质上是同一批知识点的排列组合你把模板攒够了新题只是换个壳。关于难度递进我要提醒一点不要因为第一题简单就轻视它。很多新手做完第一题觉得没意思直接跳最后一题结果发现最后一题用到了第一题的某些结论或者编码规则。系列题的魅力就在于此它考察的不只是单题能力还有你能不能把前面的收获迁移到后面。做第一题时多问一句这个规则出题人为什么要放在第一题往往能提前拿到后面题的钥匙。3.3 参数计算的手算验证方法写脚本之前我强烈建议你先手算一遍小规模的例子确认自己的理解是对的。比如学 RSA 的时候别再拿几百位的数去试先拿 p3、q11 这种小数字走一遍完整流程自己算 n、φ(n)、d、加密、解密。手算一遍你对每一步的作用会理解得非常透彻。这就像学开车之前先在模拟器上熟悉操作真上路时心里有底。我拿 p3、q11 举个例子。n3×1133φ(n)(3-1)×(11-1)20。取 e3因为 3 和 20 互素。求 d 满足 3d ≡ 1 (mod 20)算出来 d7因为 3×721≡1。现在要加密明文 m4密文 c 4^3 mod 33 64 mod 33 31。解密验证31^7 mod 33这个算起来麻烦但可以一步步来31 ≡ -2 (mod 33)(-2)^7 -128-128 mod 33 -128 4×33 4正好还原成 m4。手算一遍RSA 的加解密对称性就刻在脑子里了。再比如异或先拿一字节练手。假设密文是 0x2A你知道明文是字符 A0x41那密钥就是 0x2A ^ 0x41 0x6B。用这个密钥去解其他字节就能验证思路对不对。小规模手算的好处是一旦你在小数据上验证通过大规模脚本跑出来的结果你就敢信了。反过来如果你不手算直接上脚本脚本出错时你根本分不清是思路错了还是代码错了。提示手算验证是排查问题的利器。当大脚本报错时把参数缩到最小重跑一遍能快速定位是逻辑问题还是实现问题。4. 常见问题与排查技巧实录4.1 那些年踩过的解码坑密码学题里最容易卡住新手的不是数学而是各种编码细节。我把自己和身边朋友遇到过的高频坑整理一下。第一个坑是 Base64 解码报错却不报原因。Python 的 base64 库在遇到非法字符或者 padding 不对时抛的异常信息很模糊很多时候你以为密文有问题其实是多了个换行或者少了等号。解决方法是解码前先清洗字符串去掉空白字符补齐 padding。第二个坑是十六进制字符串长度奇数。这个前面提过bytes.fromhex 要求偶数长度遇到奇数长度直接报错。补零的时候要注意补在前面还是后面整数转换补在前面字节流哈希补在后面搞错了结果完全不同。第三个坑是字符集识别错误把 Base32 当成 Base64 去解怎么都解不出东西。这种时候最好的办法是用 CyberChef 这种工具快速试它会自动判断可能的编码类型。第四个坑是权限和路径问题。有些题的附件是压缩包解压出来有密码密码可能在题目描述里、在文件名里、或者就是前一道题的答案。解压工具报密码错误时先检查是不是复制密码时带了空格。这些看起来是小事但在紧张的比赛环境里一个空格能让你浪费好几分钟。4.2 脚本报错的排查思路脚本报错在密码学题里太常见了我总结了一套从外到内的排查顺序。先看报错类型是语法错误、类型错误还是值错误。语法错误是低级问题检查括号和缩进类型错误多半是把字符串当整数用了检查 int 和 bytes 的转换值错误通常是参数不合法比如模逆元不存在e 和 φ(n) 不互素、开方开不出来等。再看数据本身。把输入的 n、e、c 打印出来确认它们的值和你预期的一致。很多时候报错是因为你的正则匹配出错了把别的数字也匹配进来了导致 n 不是真正的 n。这一步用 len(str(n)) 检查位数如果位数和你估计的差很多那一定是提取出了问题。最后看逻辑。如果语法和数据类型都没问题那就是算法思路错了。这时候回到特征判断那一步重新审视你用攻击方法是否匹配。RSA 题里最常见的逻辑错误是选错了攻击比如该用共模攻击的题你用了低指数攻击脚本跑得飞快但结果就是错的而且不报错这种最隐蔽。排查方法是把中间结果打印出来看 p、q 是否合理都应该是素数φ(n) 和 d 是否符合预期。报错现象可能原因排查动作invert 报错e 与 φ(n) 不互素检查 gcd考虑有限域开方hex 解析报错字符串长度奇数补零后再解析解密结果是乱码攻击方法选错或没有正确填充回看加密脚本逻辑程序不报错但结果不对参数提取错误打印并核对 n、e、c4.3 独家避坑技巧与效率提升做了这么多题我攒了一些别人不太会讲的经验。第一个技巧是建立自己的编码字典。把常见的编码、密码、攻击手法做成一个可检索的本地文档做题时直接查比翻网页快得多。这个字典不用写得多详细关键词加一句话描述就够关键是要自己的话写的记忆深。第二个技巧是善用已知明文。很多题会在描述里给一个已知的明文片段或者格式提示比如flag 格式是 ctfshow{xxx}。有了这个已知明文你可以直接推出大部分密钥信息甚至绕过整个攻击过程。新手经常忽略题目描述里的这些暗示埋头硬算结果时间都浪费了。第三个技巧是保存中间量。比如 RSA 题算出了 p、q、d一定要保存下来因为系列题很可能后面还要用。我见过有人做 CP-2 时算出了某个密钥做 CP-3 时忘了又重新算一遍其实两题的密钥是同一个。在系列题里前一道题的中间结果经常就是后一道题的输入。第四个技巧是验证而非假设。解出来的东西一定要检查别假设它是对的。检查方法包括看是否可读、是否符合 flag 格式、是否能用另一种方法复算一遍验证。特别是用暴力破解得到的密钥很可能因为评分函数不够好而选出错误的候选多检查一个候选结果能避免提交错误答案后浪费提交机会。最后再分享一个关于时间分配的经验。密码学题的特点是想通了十分钟就能出想不通能卡一天。所以比赛或者刷题时如果一道题卡了超过二十分钟还没思路先去做别的题回头换个角度再看。很多时候脑子放松之后灵感反而来了。我自己的记录是有至少三成的密码学题是在搁置一段时间后突然想通的而不是硬磕出来的。密码学这方向思路比蛮力重要得多。