
简介这是一份面向CTF竞赛密码学方向的RSA综合工具集聚焦RSA加密体系中常见漏洞的自动化检测与破解。资源共包含32个文件以18个Python攻击脚本为核心覆盖维纳攻击、低加密指数攻击、dp/dq泄露、共模攻击、Rabin算法等经典题型也有AMM算法、高位泄露等特殊场景脚本另有3个exe可执行程序含yafu及若干配置与日志文件便于在本地直接运行或二次修改。整个压缩包大小为26.83MB轻量且易于部署适合不同水平的CTF选手按需查阅。目前已有2077人学习获取该工具集整合了多类攻击脚本与配套说明能够帮助读者快速定位RSA题目突破口理解各类攻击方法的适用条件是一份实用性很强的密码学实战备查手册。工具内脚本分类清晰从基础脚本到特殊题型逐步进阶便于系统学习与排错。1. 轩禹CTF_RSA工具3.6.1把“赌哪个攻击”变成“跑哪条命令”CTF赛场上RSA题往往不是算不出来而是不知道往哪个方向算。拿到n2048bits、e3的一组密文新手还在翻论文找“低加密指数攻击”的公式老手已经把工具跑完、flag贴进提交框了。轩禹CTF_RSA工具3.6.1这类整合型工具就是为这个场景设计的把分解模数、低加密指数、共模、Wiener/连分数、Coppersmith等常见攻击路线封装成可调用的命令让你从“读代码”的节奏跳进“验证假设”的节奏。它适合两类人刚入门想用最小成本摸清RSA攻击面的人以及需要快速排除错误方向、给手工解题兜底的竞赛选手。这篇文章不替工具背书只讲清楚攻击适用边界、命令行参数和我踩过的坑。2. 攻击面拆解RSA题目六种常见死法与工具内置路线2.1 拿到题目先看这三个数n/e/c 的结构决定攻击方向动手跑工具之前先花三十秒做一次“参数体检”。我见过太多人把题目里的十六进制串当十进制喂给工具跑了两分钟没结果回头一看数据类型都错了。正确的第一步是把n、e、c全部转成十进制整数同时记录位数n是512bit还是2048bite是3、65537还是接近2^256有几组不同的c。这三个信息几乎能锁定攻击面。普通写法里e65537、n是大素数乘积唯一出路是分解n这种情况工具会优先尝试Fermat、Pollard p-1、Pollard rhoe3且c明显小于n时低加密指数攻击直接开三次方同一个n对应两套(e,c)时共模攻击登场e特别大接近n时往往对应解密指数d偏小Wiener连分数或其衍生算法更有效。这里建议先做一个快速脚本def inspect_params(n, e, c_list): print(n bits:, n.bit_length()) print(e bits:, e.bit_length()) print(e/n ratio:, e / n) print(c count:, len(c_list)) if e 3: print(hint: low exponent) elif e.bit_length() n.bit_length() * 0.8: print(hint: possible Wiener) if len(c_list) 2: same_n all(c[n] n for c in c_list) if same_n: print(hint: common modulus)这段脚本的逻辑很简单位数信息决定算法规模e/n的比值是Wiener攻击是否成立的早期信号c的数量则判断是否往共模方向走。参数说明n.bit_length()是判断RSA模数强度的标准口径小于512bit时可以期待工具直接分解e / n如果接近1说明e没有走65537常规路线大概率藏着低解密指数题目特征c_list长度大于等于2时要立刻检查它们是否共用同一个n。这一步的价值在于把“感觉是XX攻击”变成“数据特征指向XX攻击”避免拿着低指数攻击去解一个其实是Wiener的题白白浪费时间。我自己在赛场上吃过亏看到一个e3就兴奋结果n只有256bit直接分解更快低指数开方反而因为明文被分组而失败。先画像再动手这是RSA解题的第一条纪律。2.2 工具内置的攻击路线与适用边界以我对这类CTF RSA工具包的了解版本号到3.6.1的工具内部通常至少维护了五六条攻击线每条都不是银弹边界很清楚。第一条是分解类包含试除法、Fermat分解和Pollard系列适用条件是n小于512bit或者p、q选得太近导致Fermat分解生效这类攻击在128bit以下的n上几乎是秒出。第二条是低加密指数类针对e3或e5且密文没有做随机填充的情况直接把c开e次方注意如果出题人加了padding这里就会失败。第三条是Wiener及连分数类适用条件是私钥指数d满足d n^0.25参数特征表现为e的位数接近n。第四条是共模攻击条件是两组密文共用同一个n工具内部用扩展欧几里得求出两个系数然后组合出明文。第五条是Coppersmith相关的小根求解通常用于明确知道明文的一部分比如flag头是已知的时恢复完整消息。第六条是gift类场景题目额外给出p或q其中一个大素数的部分位工具尝试用已知高位恢复完整因子。常见误用是把所有题目无脑丢给“分解”命令结果n有2048bit分解跑一天都没结果——这不是工具不行而是这一步根本不该分解。我用一个表格来说明选型参数方便照着自己的题目参数对号入座特征组合推荐路线成功率预期典型耗时n 512bit分解类Pollard rho / Fermat高秒级到分钟级e3, c n低加密指数开方高秒级同一n, 两组(e,c)共模攻击高秒级e接近n, d偏小Wiener / 连分数中秒级到分钟级已知flag前缀或明文部分位Coppersmith小根中分钟级已知p或q的部分位因子恢复常规求d中分钟级这个表不是工具说明书里的承诺而是我自己的经验区间。实操时如果第一条路线在几秒内没反应不要死等直接换另一条。这里有个分辨技巧你的n只要超过1024bit且没有额外信息任何分解类攻击都不可能有“快速成功”的预期此时题目考的一定不是分解而是结构缺陷。2.3 “先跑工具”不丢人但要做一轮明文验证很多刚打CTF的人有个错觉工具跑出来的结果直接就是答案。实际上工具只是把候选明文交给你你需要确认这个明文是不是真正可用的flag。常见做法是把解密结果分别按字节流、HEX字符串、UTF-8文本三种方式打印出来再做一次格式检查。轩禹CTF_RSA工具3.6.1这类包里通常会提供一个验证选项但我建议自己写一个十行的校验函数逻辑简单也可靠def validate_plaintext(m_bytes): hex_preview m_bytes.hex()[:16] utf8_preview m_bytes[:16] if all(32 b 127 for b in m_bytes[:16]) else bnon-ascii print(hex , hex_preview) print(utf8 , utf8_preview) return bCTF{ in m_bytes or bflag{ in m_bytes这里m_bytes是工具解密后得到的原始字节。逻辑说明先按十六进制预览再按ASCII可见字符预览如果两种预览都出现可读字符串就说明方向对了CTF{或flag{前后缀只是快速判定有些比赛的flag格式不是这两种所以判定失败时不要急着下结论。参数说明all(32 b 127 for b in m_bytes[:16])只检查前16字节避免大对象打印刷屏。我自己习惯把validate_plaintext接到工具输出后面不管跑哪条攻击线都会过一遍这个函数。它还有一个隐藏作用如果解出来是连续的大整数说明明文本身可能做了进制转换或填充这时候要去检查题目给的编码方式而不是怀疑工具算错了。这个习惯帮我避免过至少两次“明明解对了却因为编码问题提交失败”的尴尬。3. 从zip到第一次跑通解压、依赖与最小用例3.1 解压结构与第一眼该看什么拿到轩禹CTF_RSA工具3.6.1.zip第一件事不是双击解压到桌面就完事而是先看一眼压缩包结构。我一般会用unzip -l列出内容重点是确认有没有主入口脚本、有没有依赖清单文件、有没有README或说明文档、以及有没有example目录。unzip -l 轩禹CTF_RSA工具3.6.1.zip | head -40这一条命令不会解压任何文件只是列出压缩包内容。逻辑说明把zip当成黑匣子之前先看它的入口长什么样如果有requirements.txt或者setup.py说明作者准备了一段安装流程跟着走比手动补依赖稳得多。参数说明head -40限制输出行数因为有些打包文件会把依赖目录整个带进来一次列几百行反而看不清入口。解压到工作目录时我习惯把路径设成不含中文或空格。不是玄学而是后续在命令行里引用脚本时带空格路径会让某些参数展开出问题。解压命令mkdir -p ~/ctf-tools cd ~/ctf-tools unzip 轩禹CTF_RSA工具3.6.1.zip ls -la解压后目录里会有多个脚本文件。这里我不替你猜具体文件名但一个可用的工具包一定会有一个“调用一切”的主脚本它通常支持类似python main.py --help这样的入口。如果没有README就看哪个文件在路径最深、文件名最像入口比如main.py、run.py、ctf_rsa.py。这一步的目的是建立“这个包怎么被调用”的直觉而不是把每个文件都读一遍。3.2 运行环境与依赖安装这类CTF RSA工具的核心依赖逃不出三件套gmpy2提供大数运算pycryptodome提供RSA加解密和PKCS#1填充处理sympy提供数论工具。如果你还要跑Coppersmith相关脚本常常还需要第三方实现或sagemath环境。先确认Python版本python3 --version如果版本低于3.8建议换解释器。原因很简单pycryptodome和gmpy2的新版wheel对旧Python支持越来越少与其在依赖上纠缠不如直接用干净的3.9或3.10环境。依赖安装一行搞定pip install gmpy2 pycryptodome sympy我在两台机器上安装时遇到过同一个报错gmpy2编译失败本质是机器没有构建C扩展所需的头文件。解决路径是安装预编译二进制或者改用conda环境。如果你也翻车先别急着折腾gcc确认一下pip是不是在尝试源码编译pip install --only-binary :all: gmpy2这条命令强制只使用预编译wheel。逻辑说明--only-binary :all:让pip放弃源码编译直接找二进制包对gmpy2这种C扩展尤其有效。参数说明如果提示没有匹配的wheel说明你的Python版本太新或系统平台太老最稳妥的后悔药是退回Python 3.9配合conda源安装。提示sagemath是跑Coppersmith相关攻击最顺手的解释器但整套环境体积大、启动慢。如果工具里Coppersmith只是可选项你完全可以先用pycryptodome把基础路线跑通遇到小根求解题再单独评估是否需要sagemath。3.3 命令行入口与第一个最小用例依赖装好之后先看工具帮我们准备了什么命令。常见做法是执行主脚本的helppython3 main.py --help重点看几个名词-n是模数-e是公钥指数-c是密文--attack或-a指定攻击类型--verbose打开详细输出。不同工具对参数名字定义不一样但意思大差不差。第一次跑通不追求攻击成功而是验证“工具能响应命令”。我通常会准备一组自己生成的超小参数两个50bit的素数乘起来当ne3加密一串短文本然后跑一次低指数攻击python3 main.py -a low_exp -n 0x... -e 3 -c 0x... --verbose如果工具能输出一个候选明文哪怕内容是乱码都说明整条链路是通的。这一步的意义在于建立“基线”一个能正常响应的工具后续换题目参数时才值得信。如果这里就报错优先检查--help里参数名是不是叫-n而不是--n这类小差异在工具3.x版本迭代里经常变。还有一类常见翻车是参数顺序有些脚本要求-c必须跟在-n之后否则内部解析器会把它当作positional argument导致报错。我的建议是固定写成-n、-e、-c的先后顺序多一个少一个参数都会在help里显出差异。第一次跑通后把这个最小命令行记下来后续所有题目都从这个模板改能省掉大量调试时间。4. 实战三类高频题型的标准打法和命令参考4.1 低加密指数攻击e3时直接开三次方低加密指数攻击的条件是e很小、密文c小于n且加密时没有随机填充。题目给一个巨大n、e3、c也很大时先算一下c的立方根是不是整数。手工Python代码只有几行核心是gmpy2的iroot函数它在实战中就是低指数攻击的手工版根import gmpy2 n int(你的n的十进制或十六进制值, 16) c int(你的c的十六进制值, 16) e 3 m, exact gmpy2.iroot(c, e) if exact: print(m , m) print(bytes:, int(m).to_bytes((m.bit_length() 7) // 8, big)) else: print(not exact cube, try Coppersmith)这里gmpy2.iroot返回两个值整数根和是否为精确开根。如果exact为True直接把根转字节输出如果为False说明c不是m^3的精确结果可能是加了padding或做了分组这时就要换工具里的Coppersmith攻击。参数说明比特位换算公式(m.bit_length() 7) // 8是固定套路把整数按大端序转成字节数组避免手动对齐丢失前缀零。在轩禹CTF_RSA工具3.6.1里对应命令一般是python3 main.py -a low_exp -n 0x... -e 3 -c 0x...跑通后不要直接提交把输出过一遍validate_plaintext因为有些题目出题人会做一次十六进制字符串拼接工具输出的原始字节中间可能夹着0x前缀。这种坑我在赛场上浪费过十五分钟之后每次都在解密结果后面再做一个bytes.fromhex处理。还有一个细节如果题目给的c是十进制且n非常大先确认c是否是模n之后的值还是未取模的原始幂结果两者的开方结果会差一个数量级。4.2 共模攻击同一个n、两组(e,c)的组合数共模攻击的条件是两个公钥指数e1、e2与同一个模数n配合加密同一个明文m得到c1、c2。由于gcd(e1,e2) 1就能用扩展欧几里得找到a、b使得ae1 be2 1然后用c1^a * c2^b mod n还原m。手工实现并不复杂def common_modulus(n, e1, e2, c1, c2): import gmpy2 g, a, b gmpy2.gcdext(e1, e2) if g ! 1: raise ValueError(e1 and e2 not coprime, try another pair) if a 0: c1 gmpy2.invert(c1, n) a -a if b 0: c2 gmpy2.invert(c2, n) b -b m (pow(c1, a, n) * pow(c2, b, n)) % n return m逻辑说明gcdext返回(gcd, a, b)a、b可能是负数而模幂运算需要非负指数所以对负系数对应的密文先做模逆运算再取反。两处if就是做这个修正。参数说明必须确认两个c对应同一个明文如果其中一组c是用不同填充方式生成的共模攻击会得到错误结果如果gcd(e1,e2)不等于1直接报错并提示换密文对。工具命令通常是python3 main.py -a common_modulus -n 0x... -e1 65537 -e2 65539 -c1 0x... -c2 0x...我自己的排查习惯先看e1、e2是不是互质。很多出题人会故意给e165537、e265537两次加密那种情况下gcd不是1共模攻击直接失效题目实际在考随机填充或明文分组。遇到gcd不为1的情况不要放弃看一眼gcd具体是多少如果gcd等于2可能还有一条假设偶数指数的路但工具通常不会自动覆盖这种场景需要手动缩减指数后再尝试。4.3 Wiener/连分数攻击e接近n、d过小的情况当私钥指数d小于n的0.25次方时理论上可以用连分数逼近e/n逐项试算k/d。工具内部实现通常分两段先算连分数展开再解二次方程恢复p、q。人工判断依据是e的比特位接近n的比特位并且这个题目不是常规65537路线。工具命令python3 main.py -a wiener -n 0x... -e 0x...这一条命令只需要n和e因为Wiener攻击不依赖密文就能恢复私钥。跑出来后工具一般会打印候选的d、p、q。我见过最坑的输出是d正确但p、q对不上模数n。原因很可能是模数读入时带了换行符或其他不可见字符导致int解析出错。解决方法是先用2.1节的inspect_params打印一遍n的十六进制长度和题目给的字符串长度比对长度差2以上基本就是解析问题。如果连分数方法几秒钟内没有退回结果不要死等尝试Boneh-Durfee类路线。两者边界在于d的大小d n^0.25用Wiener足够d在n^0.25到n^0.292之间Wiener会跑偏。工具3.6.1如果内置多条连分数变形一般会把它们顺序跑一遍但耗时可能翻几倍我自己的经验是超过三十秒没有收敛就切换路线。另一个判断技巧是题目里如果除了n和e还给了两个长度相同的c且e异常大往往不是Wiener而是共模或分组加密e异常大且只有一个cWiener的可能性才真正升高。5. 避坑这五个RSA工具翻车点可以直接绕开5.1 现象gmpy2安装失败pip在编译C扩展时报错第一次在新机器上布置环境时最常见的情况是执行pip install gmpy2后报错日志尾部出现fatal error: gmp.h: No such file or directory。原因很明显pip在尝试从源码编译但系统缺少GMP开发头文件。解决路径有两条。一条是装系统依赖Debian系用apt install libgmp-dev libmpfr-dev libmpc-dev装完重新pip安装另一条是用pip install --only-binary :all: gmpy2强制拉预编译wheel前提是Python版本和平台匹配。如果两条路都走不通conda创建Python 3.9环境通常是最后的后悔药conda源里预编译的gmpy2很全。不要在编译日志里硬扛这纯属浪费时间。5.2 现象十六进制串被当十进制读入这是RSA工具使用中最多的隐形杀手。题目通常给n 0x...这样的字符串很多脚本只用int(n)于是把字符型的十六进制串按十进制解析导致n变小或报错。判断自己的工具是否中招跑任何攻击前先打印n.bit_length()如果结果和题目描述的位数差太远基本就是进制解析错了。解决方式统一为raw 00b8... # 题目给的十六进制串 n int(raw, 16) if raw.startswith((0x, 00, abcdef)) else int(raw)这里的判断不是万能规则而是经验hex格式的模数常常以0x开头或者以00开头表示整数字节串而纯十进制字符串很少出现以字母开头的情况。注意写这一条不是鼓励你去猜而是提醒你保留原始字符串等bit_length不匹配时回去检查。这个坑最阴险的地方在于不是报错而是静默地给出错误结果。5.3 现象共模攻击跑出乱码共模攻击的结果是一大串数字转字节后完全不可读最常见的原因是两对(e,c)对应的明文来源不一致。比如出题人用PKCS#1 v1.5填充加密了一次又用OAEP填充加密了一次两者虽然共模但得到的c不是同一个m的两次加密共模公式自然失效。另一种情况是e1、e2其中一个是偶数或与phi(n)不互质导致扩展欧几里得得到的系数虽然存在但幂运算结果并不能还原明文。解决方式是先检查e1、e2的奇偶性并打印gcdgcd等于1再上共模攻击如果gcd正确但结果仍乱码则把c1^a * c2^b的计算方向反过来即交换c1、c2再算一次。手动交换的次数不丢人我这个习惯帮我救回过至少两题都是因为题目脚本里把c1和c2的赋值顺序写反了。5.4 现象低加密指数攻击返回“not exact cube”但题目确实是低指数当e3且确认没有做分组加密时exact仍然为False通常是因为密文c不是完整的三次幂而是取模n之后的结果。取模后的c小于n但理论上c应该正好等于m^3出现偏差的原因大概率是明文m n出题人做了分段或者对m做了额外编码。不要立刻放弃这题改用Coppersmith的短填充攻击给定c的高位或低位已知时可以恢复完整明文。工具3.6.1里如果内置了related_message或short_pad_attack可以直接切换过去。如果没有也可以自己写一个已知明文的前缀是flag{把m prefix x的表达式代入c m^3 mod n构造一个以x为未知数的一元多项式再用Coppersmith求解。注意这种场景对x的长度有要求x超过n的1/3长度时会解不出来。5.5 现象Wiener攻击输出了一堆候选但验证全部失败Wiener攻击用连分数逼近e/n时会生成一串候选分数每一个都可能对应(k,d)。正确的只有一个判断标准是用候选d去解ed - 1 k * phi(n)再尝试还原p、q最后验证p*q是否等于n。如果所有候选都验证失败优先确认n是否被正确读入其次是e是否真的接近n还有一种被忽视的情况是n和e以PEM格式给出工具内部只接受整数参数PEM文件需要先手动解析出整数值。我在赛场上吃过这个亏后来养成了习惯凡是报Wiener验证失败先把n和e的十六进制字符串打印一遍肉眼核对无误再怀疑算法。还有一个冷知识连分数候选列表的前几项通常对应错误的k/d组合人工查看时不要只看第一个至少检查前十个有些正确解藏在很靠后的位置。6. 把工具用成自己的武器库封装、回环验证与一个习惯工具装好、命令能跑这只是起点。真正让你在赛场上快人一步的是把这些分散命令封装成自己的攻击接口。我一般会维护一个rsa_kit.py把上面提到的inspect_params、low_exp_attack、common_modulus、wiener_attack都收敛成统一的函数签名输入n、e、c列表输出候选明文列表。这样做有三个实际好处一是换题目时只改参数文件不改代码二是可以针对同一个题目自动顺序跑多条攻击线减少人工切换三是把踩过的规避逻辑写进封装函数里比如自动处理十六进制前缀、自动验证输出是否为可读flag。回环验证是检验这套封装是否靠谱的关键动作。我建议在本地自己生成一组RSA参数加密一段已知明文再跑封装函数确认能还原原文。这里给一个简单的生成器from Crypto.PublicKey import RSA key RSA.generate(1024) n, e, d key.n, key.e, key.d msg bCTF{loopback_test} c pow(int.from_bytes(msg, big), e, n) m pow(c, d, n) print(int(m).to_bytes((m.bit_length() 7) // 8, big))这段代码生成一个1024bit的RSA临时密钥用原始模幂做加密和解密。逻辑说明pow(msg_int, e, n)是教科书版RSA加密pow(c, d, n)是解密两者只要p、q、d一致就能还原原文。参数说明实际CTF题极少用这种裸RSA有兴趣再套一层PKCS#1填充但回环验证用裸RSA足矣目的是确认你的攻击脚本调用链没有传错参数。回环验证通过后还要养成一个固定习惯每次拿到题目先跑inspect_params输出参数画像再决定攻击顺序。这套流程表面上看多花5秒实际上能避免一半以上的方向性错误。去年某场比赛我贪快看到e3就把所有精力放在低指数上跑完才发现c是分块后的第二块单独开三次方根本得不到明文——如果先做参数画像就会发现c的位数和n接近根本不该期望iroot能精确开根。从那以后我把“先画像再攻击”固定成肌肉记忆。轩禹CTF_RSA工具3.6.1能给你的不是“跑一下就出flag”的保证而是一套把攻击面摆到台面上的节奏感。工具只是把手动操作压缩成命令真正决定你能不能出flag的是你对每条攻击边界的理解和对输出结果的验证深度。希望这篇笔记能帮你在下一次遇到RSA题时少走几步弯路。本文还有配套的精品资源点击获取