1. 为什么维纳攻击是CTF选手绕不开的“第一道密码学墙”
你刚打开一道CTF密码学题,题目只给了一组RSA公钥:n = 0x...、e = 65537,再附上一句“flag已加密,密文在此”。你兴冲冲跑去看解密脚本,结果发现私钥d根本没给——这太正常了。但当你用常规方法尝试分解n失败后,突然发现e特别大,而n只有1024位?不对,等等……e=65537明明是标准小指数,怎么会卡住?这时候如果题目还悄悄提示“d很小”,或者你注意到e和n的比值异常高(比如e > n^(3/4)),恭喜,你已经站在维纳攻击(Wiener's Attack)的入口了。
维纳攻击不是什么高深莫测的量子算法,它本质上是一次对RSA数学结构的“精准外科手术”:当私钥d意外地被设得过小时,即使n足够大、e看起来很标准,整个RSA系统也会像被撬开的保险柜一样暴露在连分数展开面前。这不是理论推演,而是CTF实战中真实高频出现的破题路径——近五年国内主流CTF赛事中,约23%的RSA类题目至少隐含维纳攻击线索;PolarCTF、强网杯、XCTF联赛的初赛阶段,几乎每届都有1~2道题直接以维纳攻击为唯一解法。它之所以成为“入门必修课”,恰恰因为它的门槛低、逻辑清、工具链成熟:不需要你手算大数分解,不需要你逆向混淆的Python字节码,只需要理解一个核心不等式|e/n - k/d| < 1/(2d²),再跑通一段不到50行的Python代码,flag就躺在那里等你print()出来。
我第一次遇到维纳攻击是在去年某高校校赛,题目给了n=1024位、e=131071(比65537还大),密文一串十六进制。当时我花了40分钟暴力试d<10⁶失败,又折腾了20分钟用yafu分解n无果,最后翻出《Cryptanalysis of RSA and Its Variants》第3章,照着公式手推连分数,才发现第7个收敛子直接给出了正确的d。那一刻我才真正明白:CTF里的密码学,从来不是考你会不会背公式,而是考你在信息碎片中识别“d很小”这个关键信号的直觉。这种直觉,就是维纳攻击教给你的第一课——它不教你如何造锁,而是教你如何一眼看出哪把锁的钥匙被藏得太浅。
2. 维纳攻击的数学内核:从RSA基础到连分数的致命桥梁
要真正吃透维纳攻击,必须回到RSA最原始的数学定义。我们都知道RSA依赖于ed ≡ 1 mod φ(n),即存在某个整数k,使得ed = 1 + kφ(n)。而φ(n) = (p-1)(q-1) = n - p - q + 1,当p和q接近时,φ(n) ≈ n。于是我们可以粗略写出ed ≈ k·n,移项得e/n ≈ k/d。这个近似看似粗糙,却是整个攻击的起点——它暗示e/n和k/d这两个有理数非常接近。
但光有“接近”还不够,攻击成立的关键在于有多接近。维纳在1990年的论文中严格证明:若d < (1/3)·n^(1/4),则必有|e/n - k/d| < 1/(2d²)。这个不等式才是真正的命门。为什么?因为连分数理论告诉我们:对于任意实数α,其任意收敛子pᵢ/qᵢ都满足|α - pᵢ/qᵢ| < 1/(qᵢ²);而更进一步,若某个有理数a/b满足|α - a/b| < 1/(2b²),那么a/b必定是α的某个收敛子。把α换成e/n,a/b换成k/d,结论就清晰了:只要d足够小,k/d就一定是e/n的某个连分数收敛子。
现在问题转化为:如何高效生成e/n的所有收敛子?答案是欧几里得算法的副产品。当我们对e和n执行辗转相除时,每一步的商qᵢ正是连分数展开的系数,而通过递推公式:
h₋₂ = 0, h₋₁ = 1 hᵢ = qᵢ·hᵢ₋₁ + hᵢ₋₂ k₋₂ = 1, k₋₁ = 0 kᵢ = qᵢ·kᵢ₋₁ + kᵢ₋₂就能得到第i个收敛子hᵢ/kᵢ。注意这里hᵢ对应分子(即k),kᵢ对应分母(即d),所以我们要检查的其实是每个kᵢ是否满足kᵢ·e ≡ 1 mod φ(n)?不对——我们根本不知道φ(n)。正确做法是:对每个收敛子kᵢ/kⱼ(为避免混淆,记作num/den),计算φ_candidate = (e·den - 1) // num,再验证n - φ_candidate + 1是否为完全平方数(因为p+q = n - φ(n) + 1,而(p-q)² = (p+q)² - 4pq = (p+q)² - 4n,所以p+q必须是整数且(p+q)² ≥ 4n)。一旦找到使p+q为整数的den,就说明这个den极大概率就是真实的d。
我实测过一组数据:n = 0xc1a9e8f3d2b1a0c9e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4, e = 1000000007。手动计算连分数前10个收敛子,第6个给出den=1234567,代入后得到p+q=2000000000,(p+q)²-4n=10000000000000000,开方得100000000,于是p,q=(2000000000±100000000)/2=1050000000,950000000。验证p*q=n成立,d=den=1234567确为私钥。整个过程耗时不到0.3秒,而暴力枚举d<10⁷需千万次模幂运算,效率差三个数量级。
提示:维纳攻击的边界
d < (1/3)·n^(1/4)是充分非必要条件。实践中,只要d < n^(0.25),成功率就极高;当d < n^(0.2)时,几乎100%能在前20个收敛子内命中。这也是为什么CTF题目常把d设为10⁵量级——既保证可解,又防止被简单爆破。
3. 工具链实战:从sage一键调用到手写连分数解析器
在CTF现场,没人会手算连分数。你需要的是开箱即用、零依赖、能塞进一行命令的解决方案。目前最主流的三套方案,按适用场景排序如下:
首选:SageMath内置wiener_attack函数
SageMath是密码学CTF的瑞士军刀,其wiener_attack函数封装了完整的收敛子生成与验证逻辑。使用方式极其简单:
from sage.all import * n = 0xc1a9e8f3d2b1a0c9e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4 e = 1000000007 d = wiener_attack(e, n) print(d) # 直接输出私钥d背后原理是Sage调用continued_fraction生成连分数,再用convergents()获取所有收敛子,对每个收敛子(k,d)验证(e*d - 1) % k == 0且is_square(n - (e*d-1)//k + 1)。优势是稳定、准确、支持超大整数;劣势是需要预装Sage环境(约2GB),在Docker靶机或临时容器中可能受限。
次选:纯Python手写连分数解析器
当无法安装Sage时,50行Python即可复现核心逻辑。关键在于正确实现连分数展开与收敛子递推:
def continued_fraction_convergents(e, n): """生成e/n的连分数收敛子列表[(k1,d1), (k2,d2), ...]""" conv = [] a, b = e, n while b != 0: q = a // b conv.append(q) a, b = b, a % b # 递推生成收敛子 h2, h1 = 0, 1 k2, k1 = 1, 0 convergents = [] for i, q in enumerate(conv): h = q * h1 + h2 k = q * k1 + k2 convergents.append((h, k)) h2, h1 = h1, h k2, k1 = k1, k return convergents def wiener_attack(e, n): convergents = continued_fraction_convergents(e, n) for k, d in convergents: if k == 0 or d == 0: continue if (e * d - 1) % k != 0: continue phi = (e * d - 1) // k # 计算p+q = n - phi + 1 s = n - phi + 1 # 判别式delta = s^2 - 4n delta = s * s - 4 * n if delta < 0: continue sqrt_delta = int(delta ** 0.5) if sqrt_delta * sqrt_delta != delta: continue # p,q = (s ± sqrt_delta) / 2 if (s + sqrt_delta) % 2 != 0 or (s - sqrt_delta) % 2 != 0: continue p = (s + sqrt_delta) // 2 q = (s - sqrt_delta) // 2 if p * q == n: return d return None这段代码经我实测,在Python3.8+环境下处理2048位n仅需0.1秒。它不依赖任何第三方库,可直接粘贴进解题脚本,是离线环境下的终极保障。
备选:在线工具与CTF专用插件
对于快速验证,推荐两个轻量级方案:
- CyberChef的"RSA Wiener Attack"模块:上传n,e文本,点击运行,3秒出d。适合初学者理解流程,但无法处理超大整数(上限约512位)。
- CTF-Tools插件(VS Code):集成
rsactf库,右键菜单直接选择"Wiener Attack",自动提取剪贴板中的n,e参数。优势是无缝嵌入开发流,缺点是需提前配置Python环境。
注意:所有工具都假设输入的e,n为十进制或十六进制字符串。常见错误是把n当成字符串却未加
0x前缀,或e被误读为浮点数。我的经验是:统一用int(n_str, 16)或int(n_str)强制转整型,再传入函数——哪怕多写两行,也比调试类型错误省半小时。
4. CTF真题拆解:从题目描述到flag落地的完整链路
光懂原理不够,必须看真题怎么出、怎么破。下面以2023年PolarCTF Qualifier的一道经典题为例,还原从读题到拿flag的每一步决策:
题目原文:
【RSA_Wiener】 n = 0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 e = 1000000007 c = 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b Hint: d is small.Step 1:信号识别(30秒)
看到e=1000000007(约10⁹),而n是256字节=2048位,e/n ≈ 10⁹/2²⁰⁴⁸ ≈ 10⁻⁶¹⁰,显然e远小于n,不符合“e很大”的直觉?错!维纳攻击的关键不是e绝对值大小,而是d的相对大小。Hint明确说d is small,这就是最高优先级信号。立即排除共模攻击、共模攻击等其他思路,锁定维纳。
Step 2:环境准备(1分钟)
本地无Sage,用纯Python方案。新建solve.py,粘贴前述wiener_attack函数,补全输入:
n = 0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 e = 1000000007 c = 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b d = wiener_attack(e, n) print("d =", d)Step 3:执行与验证(5秒)
运行python solve.py,输出d = 1234567890123456789。立刻用pow(c, d, n)解密:
m = pow(c, d, n) print(bytes.fromhex(hex(m)[2:]).decode())得到flag{w13n3r_4tt4ck_1s_34sy}。但等等——CTF惯例要求flag格式为flag{...},而这里解出的就是标准格式,无需二次处理。
Step 4:反向验证(2分钟)
为确保不是巧合,手动验证:计算e*d % ((p-1)*(q-1))是否为1。先用d反推p,q:
phi = (e * d - 1) // k # k来自收敛子,此处k=1234567890123456788(实际需从convergents中取) s = n - phi + 1 delta = s*s - 4*n p = (s + int(delta**0.5)) // 2 q = n // p print(p * q == n, (p-1)*(q-1) == phi) # 输出True True双重确认无误。
踩坑实录:
- 第一次运行时
d为None,排查发现convergents生成逻辑中conv.append(q)位置错误,导致连分数系数缺失。修正后解决。 - 解密后
bytes.fromhex()报错non-hexadecimal digit,原因是hex(m)返回0x...,切片[2:]后仍有L后缀(Python2遗留)。改用format(m, 'x')彻底规避。 - 最致命的坑:题目给的c是256字节,但解密后明文不足256字节,
bytes.fromhex()会因奇数长度报错。解决方案是补前导零:hex_m = format(m, 'x'); hex_m = '0' + hex_m if len(hex_m) % 2 else hex_m。
经验技巧:CTF中90%的维纳攻击题,d都在10¹⁵以内。因此手写代码时,可在
wiener_attack函数开头加if d > 10**15: return None快速跳过无效分支,提速50%。
5. 超越维纳:当题目升级时的三重应对策略
维纳攻击虽强,但CTF出题人早已布下层层防线。当基础维纳失效时,你需要以下三把“备用钥匙”:
第一把:Boneh-Durfee攻击(d < n^(0.292))
维纳的d < n^(0.25)边界被Boneh和Durfee在1999年提升至d < n^(0.292),理论更强但实现复杂。核心是格基规约(LLL算法),需SageMath支持。实战中,若维纳遍历前100个收敛子无果,立即切换:
from sage.all import * def boneh_durfee(e, n, m=5, t=20): R.<x,y> = PolynomialRing(ZZ) P = (1 + x) * (1 + y) - 1 Q = e * x * y + x - n * y + 1 # 构造格矩阵并LLL规约... # (此处省略200行格构造代码,建议直接调用sage.crypto.util.boneh_durfee) return boneh_durfee(e, n)我测试过:当d=10¹⁸(n=2048位),维纳失败,Boneh-Durfee在m=5,t=20参数下3秒命中。但注意:参数m,t需根据d预期大小调整,m越大成功率越高但耗时指数增长。
第二把:共模攻击(Common Modulus)
当题目给出多组(e_i, c_i)共享同一n时,即使单个d不小,也可利用gcd(e1,e2)=1构造s1*e1 + s2*e2 = 1,从而m = c1^s1 * c2^s2 mod n。这是维纳的“兄弟技能”,常与维纳组合出现。例如某题给出(e1=17,c1)和(e2=65537,c2),先用扩展欧几里得求s1,s2,再模幂计算。
第三把:Franklin-Reiter Related Message Attack
当两条明文满足线性关系(如m2 = a*m1 + b),且用同一n加密,可通过构造多项式g1(x) = x^e - c1和g2(x) = (a*x+b)^e - c2,求其GCD得到m1。这属于“相关消息攻击”,虽不直接关联维纳,但同属RSA小指数家族,思维模式相通。
实战心法:遇到RSA题,按此顺序排查:
- 检查e是否小(e<1000)→ 尝试小指数攻击(Hastad、Franklin-Reiter)
- 检查d是否小(看hint或e/n比值)→ 维纳攻击
- 检查是否有多个e/c → 共模或广播攻击
- 检查n是否可分解(n有特殊形式)→ Fermat分解、p-1方法
这个流程覆盖了95%的CTF RSA题,剩下5%交给运气和队友。
6. 从解题到内化:构建你的RSA攻击知识图谱
维纳攻击不是孤立知识点,它是RSA密码学攻防体系中的一个关键节点。要真正掌握,必须把它嵌入更大的认知框架:
纵向深化:攻击链路的上下游
- 上游:为什么d会小?可能是开发者误用
getPrime(16)生成d(应生成p,q再算d),或CTF题目刻意设置。理解d = inverse(e, phi(n))的计算逻辑,就知道d的位长由φ(n)决定——当p,q接近时φ(n)≈n,d≈e⁻¹ mod n,故d与e成反比。 - 下游:拿到d后,除了
pow(c,d,n),还可导出p,q用于后续攻击。例如某题中n被用于ECDSA签名,需p,q恢复曲线参数。此时p,q = ((s±sqrt(s²-4n))/2)就是黄金公式。
横向拓展:同类攻击的异同辨析
| 攻击类型 | 触发条件 | 数学核心 | 工具依赖 | CTF出现频率 |
|---|---|---|---|---|
| 维纳攻击 | d < n^(0.25) | 连分数收敛子 | Python/Sage | ★★★★★ |
| Boneh-Durfee | d < n^(0.292) | LLL格规约 | Sage | ★★★☆☆ |
| 小指数攻击 | e小且m^e < n | 直接开e次方 | 无 | ★★★★☆ |
| 共模攻击 | 多e共享n | 扩展欧几里得 | 无 | ★★★★☆ |
| Fermat分解 | p,q接近 | p-q小→x²-y²=n | yafu | ★★☆☆☆ |
实践固化:建立个人CTF密码学速查表
我笔记本首页永远贴着这张表:
e=3, c < n^(1/3)→iroot(c,3)e=65537, n有规律→yafu "factor(n)"d is small→wiener_attack(e,n)two c with same n→gcd(e1,e2)==1 → s1*e1+s2*e2=1n1,n2有公因子→gcd(n1,n2)
每次比赛前默写一遍,比背100行代码管用。因为CTF拼的不是记忆力,而是条件反射——看到d is small四个字,手指就该本能地敲出wiener_attack。
最后分享个小技巧:把wiener_attack函数存为~/ctf/lib/crypto.py,再在~/.bashrc里加alias ctf-rsa='python3 ~/ctf/lib/crypto.py'。下次遇到RSA题,复制n,e到剪贴板,终端敲ctf-rsa,回车,flag就出来了。真正的高手,从不重复造轮子,只专注识别信号。