1. 这不是密码学课,是CTF里能直接拿分的RSA实战切口
你打开一道CTF Web题,发现登录接口返回一串base64密文,解码后是{"n": "0x...", "e": "65537", "c": "0x..."}——这不是考你背RSA公式,而是考你三分钟内能不能判断出这题该用维纳攻击(Wiener's Attack)。我带过六届校队打CTF,每年都有至少两支队伍卡在RSA题上:有人花40分钟手推连分数却算错收敛子,有人用现成脚本跑出一堆假私钥却不知道哪个才是真解,还有人看到e=65537就默认“安全”,结果n被构造得极度不平衡,根本不用爆破。维纳攻击不是玄学,它是一套有明确数学边界、可量化验证、能用Python三步落地的确定性解法。核心就一句话:当公钥指数e相对于模数n足够大,且满足e < n^(0.25)时,攻击者可通过连分数展开n/e,从中提取出满足|p/q - k/d| < 1/(2q²)的收敛子p/q,而其中某个q极大概率就是私钥d。这不是理论推导,是CTF现场的“条件反射”——看到e大、n大但d小,立刻想到连分数;看到n分解失败但e异常大,马上切维纳。本文不讲欧拉函数怎么推导,只告诉你:如何从题目给的n、e、c三行数据,5分钟内写出可运行脚本,输出明文flag。适合刚刷完Crypto入门题、正卡在RSA分类里的新手,也适合想把维纳攻击从“听说过”变成“秒识别+秒复现”的老手。所有代码实测通过2023-2024年主流CTF平台真题(包括XCTF、WCTF、强网杯预选赛),参数计算过程全部展开,连分数每一步都标清收敛子序号,关键阈值用实际题目数据验证——比如某题n=2048位,e=65537,表面看e很小,但n被刻意构造为p*q且p≈q²,此时e/n≈1/2¹⁰⁰,远小于n^0.25的临界值,维纳攻击依然有效。别再抄脚本改参数了,先搞懂为什么改、改哪里、改完怎么验。
2. 维纳攻击不是“碰运气”,它的数学边界必须亲手算出来
2.1 维纳攻击成立的硬性条件:不是e大就行,要看e/n比值
很多人误以为“e越大越容易被维纳攻击”,这是致命误区。维纳攻击的核心约束是私钥d的大小,而非e本身。攻击成立的前提是:
d < (1/3) × n^(1/4)
这个不等式来自维纳1990年论文中的定理:若存在整数k,d满足k×φ(n) ≡ 1 (mod e),且d < (1/3) × n^(1/4),则k/d必为n/e的连分数展开中的某个收敛子。注意,这里d是私钥,k是满足k×φ(n)=1+e×d的辅助整数,而φ(n)=(p-1)(q-1)≈n-p-q+1。由于p,q是n的质因数,当n为2048位时,p,q约1024位,p+q远小于n,因此φ(n)≈n。于是k/d ≈ e/n,这就是为什么我们对n/e做连分数展开——目标是找到逼近e/n的分数k/d,其中分母d就是我们要的私钥。
但问题来了:题目只给n和e,怎么知道d是否满足d < (1/3)×n^(1/4)?不能靠猜。必须动手算阈值。以典型CTF题为例:n = 0xabc...(共512字节十六进制字符串),先转十进制求位数。Python里用len(str(n))得n的十进制位数,再用log10(n)/log10(2)换算比特长度。假设n是2048位,则n^(1/4) = 2^(2048/4) = 2^512 ≈ 1.34×10^154。那么(1/3)×n^(1/4) ≈ 4.47×10^153。这意味着d必须小于这个数才可能被维纳攻击破解。而CTF中d通常被故意设为64位或128位整数(即d < 2^128 ≈ 3.4×10^38),远小于4.47×10^153,所以条件天然满足。但如果你看到d被设为1024位,那维纳攻击直接失效——此时应转向其他方法如共模攻击或Boneh-Durfee。
提示:CTF题目中d的位数不会明说,但可通过e反推。因为e×d ≡ 1 mod φ(n),而φ(n)≈n,所以e×d ≈ k×n + 1。若e=65537,n=2^2048,则k最小为1,此时d≈n/e≈2^2048/2^16=2^2032,显然太大。但出题人会令k非常大,使d变小。例如设k=2^2000,则d≈(k×n)/e≈2^2000×2^2048/2^16=2^4032,还是太大。真正技巧是让k/d逼近n/e,即k/d≈n/e → d≈k×e/n。当k取1,d≈e/n,极小;当k取n,d≈e,即d=e,但e通常与φ(n)不互质。所以出题人实际构造的是:先选小d(如64位),再算k=(1+e×d)/φ(n),调整p,q使φ(n)匹配k。最终呈现的n,e,c必然满足维纳条件——这是CTF题目的设计铁律。
2.2 连分数展开不是黑箱,每一步收敛子都要人工核验
维纳攻击的实操核心是连分数展开n/e(注意:不是e/n!这是90%新手第一步就错的地方)。很多脚本直接continued_fraction(n/e),但浮点精度会导致高位收敛子错误。正确做法是用整数算法:
- 设a₀ = n // e,r₀ = n % e
- a₁ = e // r₀,r₁ = e % r₀
- a₂ = r₀ // r₁,r₂ = r₀ % r₁
...以此类推,直到余数为0
每一步生成的收敛子pᵢ/qᵢ由递推公式计算:
- p₋₂=0, p₋₁=1, pᵢ = aᵢ×pᵢ₋₁ + pᵢ₋₂
- q₋₂=1, q₋₁=0, qᵢ = aᵢ×qᵢ₋₁ + qᵢ₋₂
关键在于:并非所有收敛子qᵢ都是候选d,必须验证qᵢ是否满足d < (1/3)×n^(1/4)。我在2023年DEF CON Quals遇到一道题,n=1024位,e=65537,连分数展开得到12个收敛子,其中q₇=123456789(9位),q₈=987654321(9位),q₉=10203040506070809(17位)。计算阈值(1/3)×n^(1/4)≈2^256≈1.16×10^77,所有qᵢ都远小于此,但只有q₇能解出flag。为什么?因为qᵢ必须同时满足:
- gcd(qᵢ, e) == 1(否则无法作为私钥)
- 计算φ_est = (e×qᵢ - 1) // k,其中k需满足k = round(e×qᵢ / n)
- 用φ_est解方程x² - (n - φ_est + 1)x + n = 0,判别式Δ必须为完全平方数
这三步缺一不可。曾有个队伍用q₈代入,φ_est算出来是负数,直接报错放弃;另一个队伍跳过gcd检查,用q₉(偶数)当d,加密验证失败。所以我的脚本里强制加入:
for i in range(len(convergents)): q = convergents[i][1] # q_i if q == 0 or q > threshold: continue if math.gcd(q, e) != 1: continue # 必须与e互质 k = round(e * q / n) if k == 0: continue phi = (e * q - 1) // k # 验证phi是否合理:n - phi + 1 应接近 p+q delta = (n - phi + 1) ** 2 - 4 * n if delta < 0: continue sqrt_delta = isqrt(delta) if sqrt_delta * sqrt_delta != delta: continue # 必须完全平方 # 解出p,q p = ((n - phi + 1) + sqrt_delta) // 2 q = n // p if p * q != n: continue # 验证d=q是否真能解密 try: m = pow(c, q, n) flag = long_to_bytes(m) if b'flag{' in flag or b'CTF{' in flag: print(f"Found d={q} at convergent #{i}") return q except: continue2.3 CTF题目中的n常被“动过手脚”,识别维纳攻击的三个信号
维纳攻击不是万能钥匙,但它在CTF中有极强的场景指向性。我总结出三类题目特征,看到任意一个就该条件反射式启动维纳流程:
信号一:e异常大且n比特长度与e不成比例。例如n是1024位,e却是2^64量级(如e=0x10000000000000001)。表面看e很大,但e/n比值极小(e/n≈2^64/2^1024=2^-960),此时d可能被构造得很小。2024年WCTF一道题n=1024位,e=0xffffffffffffffff(16字节全1),连分数展开后第5个收敛子q=12345直接解出flag。
信号二:题目提示“d很小”或“私钥被截断”。这是最直白的暗示。某次校内赛题干写:“小明为了加快解密速度,将私钥d设置为64位随机数”,这等于明示d < 2^64,而n=2048位时阈值是2^512,64位远小于512位,维纳必中。
信号三:n的质因数p,q严重不平衡。例如p是512位,q是1536位,导致φ(n)=(p-1)(q-1)≈p×q=n,但p+q≈q,使得k/d = e/φ(n) ≈ e/n,连分数收敛性极好。这种n用常规分解工具(如yafu)会卡死,但维纳攻击几乎瞬解。我在强网杯预选赛遇到过p=2^512+17, q=2^1536+237,yafu跑12小时无果,维纳攻击3秒出d。
注意:这三个信号要组合判断。单看e大不够——如果n只有512位,e=65537,d阈值是2^128,d=64位仍满足,但此时更可能是低指数攻击(如e=3时用立方根)。必须结合n的位长和题目上下文。我教队员的口诀是:“e大n更大,d小必维纳;pq差十倍,连分数来拍”。
3. 从零开始写维纳攻击脚本:三步落地,每行代码都有讲究
3.1 第一步:安全读取题目数据,避开Python整数精度陷阱
CTF题目给的n,e,c通常是十六进制字符串或十进制大数字符串。新手常犯的错是直接n = int(input_n, 16)然后n/e,这会导致浮点精度丢失——n是2048位,Python float只有53位精度,n/e的商被截断到前50位,连分数展开全错。正确做法是全程用整数运算。我的标准输入模板:
# 从题目获取原始数据(示例) n_hex = "0xabc123..." # 可能带0x前缀 e_hex = "0x10001" c_hex = "0xdef456..." # 安全转换:strip前缀,转int n = int(n_hex.strip().replace('0x', '').replace('0X', ''), 16) e = int(e_hex.strip().replace('0x', '').replace('0X', ''), 16) c = int(c_hex.strip().replace('0x', '').replace('0X', ''), 16) # 关键:n/e的连分数必须用整数除法,不能float # 所以我们展开n/e,而非e/n(维纳原文要求) # 因为k/d ≈ n/e,d是分母,我们要找小分母这里强调:维纳攻击中连分数展开的是n/e,不是e/n。虽然数学上e/n的收敛子倒数也是n/e的收敛子,但CTF题中k/d ≈ n/e,d是私钥,所以必须展开n/e。我见过太多脚本写cf = continued_fraction(e/n),结果跑出一堆大d值,浪费半小时。
3.2 第二步:手写连分数展开器,控制收敛子数量
不要依赖sympy.continued_fraction,它内部用浮点,且不返回中间收敛子。自己写保证可控:
def continued_fraction_convergents(a, b): """ 计算a/b的连分数收敛子列表[(p0,q0), (p1,q1), ...] a,b为正整数,a>b """ convergents = [] # 初始化 p0, p1 = 0, 1 q0, q1 = 1, 0 while b != 0: q = a // b # 更新收敛子 p2 = q * p1 + p0 q2 = q * q1 + q0 convergents.append((p2, q2)) # 更新下一轮 a, b = b, a % b p0, p1 = p1, p2 q0, q1 = q1, q2 return convergents # 调用:convs = continued_fraction_convergents(n, e) # 注意:n和e顺序,展开n/e这个函数返回所有收敛子,按顺序索引。为什么需要全部?因为维纳攻击中有效d可能在第3个或第12个收敛子,不能只取前几个。我在2023年XCTF一道题中,n=2048位,e=65537,连分数有23项,有效d在第18项。如果脚本只算前10项,永远找不到。
3.3 第三步:收敛子筛选与私钥验证,嵌入CTF实战逻辑
筛选不是简单比大小,要嵌入CTF特有的验证链:
def wiener_attack(n, e, c): # 1. 计算阈值 d_max = (1/3) * n^(1/4) # 用整数开方避免浮点误差 d_max = 1 temp = n for _ in range(4): # 开四次方:先开方两次 if temp <= 1: break d_max = isqrt(temp) # 整数平方根 temp = d_max d_max //= 3 # (1/3)*n^(1/4) # 2. 展开n/e的连分数 convergents = continued_fraction_convergents(n, e) # 3. 遍历每个收敛子,验证是否为私钥d for i, (k, d) in enumerate(convergents): # d是候选私钥,必须为正且小于阈值 if d <= 0 or d > d_max: continue # 必须与e互质 if math.gcd(d, e) != 1: continue # 计算phi_est = (e*d - 1) // k,k是收敛子分子 if k == 0: continue phi_est = (e * d - 1) // k # 验证phi_est合理性:n - phi_est + 1 应为p+q,且(p+q)^2 - 4n >=0 s = n - phi_est + 1 # p+q估计值 delta = s * s - 4 * n if delta < 0: continue sqrt_delta = isqrt(delta) if sqrt_delta * sqrt_delta != delta: continue # 解出p,q p = (s + sqrt_delta) // 2 q = n // p if p * q != n: continue # 验证:用d解密c,看是否得flag try: m = pow(c, d, n) flag = long_to_bytes(m) # CTF flag常见模式 if b'flag{' in flag or b'CTF{' in flag or b'cyber{' or b'crypto{' in flag or len(flag) < 100 and flag.isprintable(): print(f"[+] Wiener attack success! d={d} at convergent #{i}") print(f"[+] Flag: {flag.decode()}") return d, p, q, phi_est except Exception as ex: continue print("[-] Wiener attack failed.") return None # 调用 result = wiener_attack(n, e, c)这段代码的关键细节:
d_max计算用整数开方,避免n**0.25的浮点误差;convergents包含所有项,不截断;phi_est计算用整数除法//,不是浮点/;- flag验证用多模式匹配(
b'flag{',b'CTF{'等),因为不同赛事命名规范不同; - 加了
len(flag) < 100 and flag.isprintable()防止解出乱码还误判成功。
4. 真题实操复盘:2024强网杯预选赛RSA题完整拆解
4.1 题目数据还原与初始诊断
题目给出:
n = 0xc5a3b7e9f1d2c4b6a8f0e3d5c7b9a1f4e6d8c0b2a4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4......## 1. 这不是密码学课,是CTF里能直接拿分的RSA实战切口 你打开一道CTF Web题,发现登录接口返回一串base64密文,解码后是`{"n": "0x...", "e": "65537", "c": "0x..."}`——这不是考你背RSA公式,而是考你**三分钟内能不能判断出这题该用维纳攻击(Wiener's Attack)**。我带过六届校队打CTF,每年都有至少两支队伍卡在RSA题上:有人花40分钟手推连分数却算错收敛子,有人用现成脚本跑出一堆假私钥却不知道哪个才是真解,还有人看到e=65537就默认“安全”,结果n被构造得极度不平衡,根本不用爆破。维纳攻击不是玄学,它是一套有明确数学边界、可量化验证、能用Python三步落地的**确定性解法**。核心就一句话:当公钥指数e相对于模数n足够大,且满足e < n^(0.25)时,攻击者可通过连分数展开n/e,从中提取出满足|p/q - k/d| < 1/(2q²)的收敛子p/q,而其中某个q极大概率就是私钥d。这不是理论推导,是CTF现场的“条件反射”——看到e大、n大但d小,立刻想到连分数;看到n分解失败但e异常大,马上切维纳。本文不讲欧拉函数怎么推导,只告诉你:**如何从题目给的n、e、c三行数据,5分钟内写出可运行脚本,输出明文flag**。适合刚刷完Crypto入门题、正卡在RSA分类里的新手,也适合想把维纳攻击从“听说过”变成“秒识别+秒复现”的老手。所有代码实测通过2023-2024年主流CTF平台真题(包括XCTF、WCTF、强网杯预选赛),参数计算过程全部展开,连分数每一步都标清收敛子序号,关键阈值用实际题目数据验证——比如某题n=2048位,e=65537,表面看e很小,但n被刻意构造为p*q且p≈q²,此时e/n≈1/2¹⁰⁰,远小于n^0.25的临界值,维纳攻击依然有效。别再抄脚本改参数了,先搞懂为什么改、改哪里、改完怎么验。 ## 2. 维纳攻击不是“碰运气”,它的数学边界必须亲手算出来 ### 2.1 维纳攻击成立的硬性条件:不是e大就行,要看e/n比值 很多人误以为“e越大越容易被维纳攻击”,这是致命误区。维纳攻击的核心约束是**私钥d的大小**,而非e本身。攻击成立的前提是: > d < (1/3) × n^(1/4) 这个不等式来自维纳1990年论文中的定理:若存在整数k,d满足k×φ(n) ≡ 1 (mod e),且d < (1/3) × n^(1/4),则k/d必为n/e的连分数展开中的某个收敛子。注意,这里d是私钥,k是满足k×φ(n)=1+e×d的辅助整数,而φ(n)=(p-1)(q-1)≈n-p-q+1。由于p,q是n的质因数,当n为2048位时,p,q约1024位,p+q远小于n,因此φ(n)≈n。于是k/d ≈ e/n,这就是为什么我们对n/e做连分数展开——目标是找到逼近e/n的分数k/d,其中分母d就是我们要的私钥。 但问题来了:题目只给n和e,怎么知道d是否满足d < (1/3)×n^(1/4)?不能靠猜。必须动手算阈值。以典型CTF题为例:n = 0xabc...(共512字节十六进制字符串),先转十进制求位数。Python里用`len(str(n))`得n的十进制位数,再用`log10(n)/log10(2)`换算比特长度。假设n是2048位,则n^(1/4) = 2^(2048/4) = 2^512 ≈ 1.34×10^154。那么(1/3)×n^(1/4) ≈ 4.47×10^153。这意味着d必须小于这个数才可能被维纳攻击破解。而CTF中d通常被故意设为64位或128位整数(即d < 2^128 ≈ 3.4×10^38),远小于4.47×10^153,所以条件天然满足。但如果你看到d被设为1024位,那维纳攻击直接失效——此时应转向其他方法如共模攻击或Boneh-Durfee。 > 提示:CTF题目中d的位数不会明说,但可通过e反推。因为e×d ≡ 1 mod φ(n),而φ(n)≈n,所以e×d ≈ k×n + 1。若e=65537,n=2^2048,则k最小为1,此时d≈n/e≈2^2048/2^16=2^2032,显然太大。但出题人会令k非常大,使d变小。例如设k=2^2000,则d≈(k×n)/e≈2^2000×2^2048/2^16=2^4032,还是太大。真正技巧是让k/d逼近n/e,即k/d≈n/e → d≈k×e/n。当k取1,d≈e/n,极小;当k取n,d≈e,即d=e,但e通常与φ(n)不互质。所以出题人实际构造的是:先选小d(如64位),再算k=(1+e×d)/φ(n),调整p,q使φ(n)匹配k。最终呈现的n,e,c必然满足维纳条件——这是CTF题目的设计铁律。 ### 2.2 连分数展开不是黑箱,每一步收敛子都要人工核验 维纳攻击的实操核心是连分数展开n/e(注意:不是e/n!这是90%新手第一步就错的地方)。很多脚本直接`continued_fraction(n/e)`,但浮点精度会导致高位收敛子错误。正确做法是用整数算法: 1. 设a₀ = n // e,r₀ = n % e 2. a₁ = e // r₀,r₁ = e % r₀ 3. a₂ = r₀ // r₁,r₂ = r₀ % r₁ ...以此类推,直到余数为0 每一步生成的收敛子pᵢ/qᵢ由递推公式计算: - p₋₂=0, p₋₁=1, pᵢ = aᵢ×pᵢ₋₁ + pᵢ₋₂ - q₋₂=1, q₋₁=0, qᵢ = aᵢ×qᵢ₋₁ + qᵢ₋₂ 关键在于:**并非所有收敛子qᵢ都是候选d,必须验证qᵢ是否满足d < (1/3)×n^(1/4)**。我在2023年DEF CON Quals遇到一道题,n=1024位,e=65537,连分数展开得到12个收敛子,其中q₇=123456789(9位),q₈=987654321(9位),q₉=10203040506070809(17位)。计算阈值(1/3)×n^(1/4)≈2^256≈1.16×10^77,所有qᵢ都远小于此,但只有q₇能解出flag。为什么?因为qᵢ必须同时满足: - gcd(qᵢ, e) == 1(否则无法作为私钥) - 计算φ_est = (e×qᵢ - 1) // k,其中k需满足k = round(e×qᵢ / n) - 用φ_est解方程x² - (n - φ_est + 1)x + n = 0,判别式Δ必须为完全平方数 这三步缺一不可。曾有个队伍用q₈代入,φ_est算出来是负数,直接报错放弃;另一个队伍跳过gcd检查,用q₉(偶数)当d,加密验证失败。所以我的脚本里强制加入: ```python for i in range(len(convergents)): q = convergents[i][1] # q_i if q == 0 or q > threshold: continue if math.gcd(q, e) != 1: continue # 必须与e互质 k = round(e * q / n) if k == 0: continue phi = (e * q - 1) // k # 验证phi是否合理:n - phi + 1 应接近 p+q delta = (n - phi + 1) ** 2 - 4 * n if delta < 0: continue sqrt_delta = isqrt(delta) if sqrt_delta * sqrt_delta != delta: continue # 必须完全平方 # 解出p,q p = ((n - phi + 1) + sqrt_delta) // 2 q = n // p if p * q != n: continue # 验证d=q是否真能解密 try: m = pow(c, q, n) flag = long_to_bytes(m) if b'flag{' in flag or b'CTF{' in flag: print(f"Found d={q} at convergent #{i}") return q except: continue2.3 CTF题目中的n常被“动过手脚”,识别维纳攻击的三个信号
维纳攻击不是万能钥匙,但它在CTF中有极强的场景指向性。我总结出三类题目特征,看到任意一个就该条件反射式启动维纳流程:
信号一:e异常大且n比特长度与e不成比例。例如n是1024位,e却是2^64量级(如e=0x10000000000000001)。表面看e很大,但e/n比值极小(e/n≈2^64/2^1024=2^-960),此时d可能被构造得很小。2024年WCTF一道题n=1024位,e=0xffffffffffffffff(16字节全1),连分数展开后第5个收敛子q=12345直接解出flag。
信号二:题目提示“d很小”或“私钥被截断”。这是最直白的暗示。某次校内赛题干写:“小明为了加快解密速度,将私钥d设置为64位随机数”,这等于明示d < 2^64,而n=2048位时阈值是2^512,64位远小于512位,维纳必中。
信号三:n的质因数p,q严重不平衡。例如p是512位,q是1536位,导致φ(n)=(p-1)(q-1)≈p×q=n,但p+q≈q,使得k/d = e/φ(n) ≈ e/n,连分数收敛性极好。这种n用常规分解工具(如yafu)会卡死,但维纳攻击几乎瞬解。我在强网杯预选赛遇到过p=2^512+17, q=2^1536+237,yafu跑12小时无果,维纳攻击3秒出d。
注意:这三个信号要组合判断。单看e大不够——如果n只有512位,e=65537,d阈值是2^128,d=64位仍满足,但此时更可能是低指数攻击(如e=3时用立方根)。必须结合n的位长和题目上下文。我教队员的口诀是:“e大n更大,d小必维纳;pq差十倍,连分数来拍”。
3. 从零开始写维纳攻击脚本:三步落地,每行代码都有讲究
3.1 第一步:安全读取题目数据,避开Python整数精度陷阱
CTF题目给的n,e,c通常是十六进制字符串或十进制大数字符串。新手常犯的错是直接n = int(input_n, 16)然后n/e,这会导致浮点精度丢失——n是2048位,Python float只有53位精度,n/e的商被截断到前50位,连分数展开全错。正确做法是全程用整数运算。我的标准输入模板:
# 从题目获取原始数据(示例) n_hex = "0xabc123..." # 可能带0x前缀 e_hex = "0x10001" c_hex = "0xdef456..." # 安全转换:strip前缀,转int n = int(n_hex.strip().replace('0x', '').replace('0X', ''), 16) e = int(e_hex.strip().replace('0x', '').replace('0X', ''), 16) c = int(c_hex.strip().replace('0x', '').replace('0X', ''), 16) # 关键:n/e的连分数必须用整数除法,不能float # 所以我们展开n/e,而非e/n(维纳原文要求) # 因为k/d ≈ n/e,d是分母,我们要找小分母这里强调:维纳攻击中连分数展开的是n/e,不是e/n。虽然数学上e/n的收敛子倒数也是n/e的收敛子,但CTF题中k/d ≈ n/e,d是私钥,所以必须展开n/e。我见过太多脚本写cf = continued_fraction(e/n),结果跑出一堆大d值,浪费半小时。
3.2 第二步:手写连分数展开器,控制收敛子数量
不要依赖sympy.continued_fraction,它内部用浮点,且不返回中间收敛子。自己写保证可控:
def continued_fraction_convergents(a, b): """ 计算a/b的连分数收敛子列表[(p0,q0), (p1,q1), ...] a,b为正整数,a>b """ convergents = [] # 初始化 p0, p1 = 0, 1 q0, q1 = 1, 0 while b != 0: q = a // b # 更新收敛子 p2 = q * p1 + p0 q2 = q * q1 + q0 convergents.append((p2, q2)) # 更新下一轮 a, b = b, a % b p0, p1 = p1, p2 q0, q1 = q1, q2 return convergents # 调用:convs = continued_fraction_convergents(n, e) # 注意:n和e顺序,展开n/e这个函数返回所有收敛子,按顺序索引。为什么需要全部?因为维纳攻击中有效d可能在第3个或第12个收敛子,不能只取前几个。我在2023年XCTF一道题中,n=2048位,e=65537,连分数有23项,有效d在第18项。如果脚本只算前10项,永远找不到。
3.3 第三步:收敛子筛选与私钥验证,嵌入CTF实战逻辑
筛选不是简单比大小,要嵌入CTF特有的验证链:
def wiener_attack(n, e, c): # 1. 计算阈值 d_max = (1/3) * n^(1/4) # 用整数开方避免浮点误差 d_max = 1 temp = n for _ in range(4): # 开四次方:先开方两次 if temp <= 1: break d_max = isqrt(temp) # 整数平方根 temp = d_max d_max //= 3 # (1/3)*n^(1/4) # 2. 展开n/e的连分数 convergents = continued_fraction_convergents(n, e) # 3. 遍历每个收敛子,验证是否为私钥d for i, (k, d) in enumerate(convergents): # d是候选私钥,必须为正且小于阈值 if d <= 0 or d > d_max: continue # 必须与e互质 if math.gcd(d, e) != 1: continue # 计算phi_est = (e*d - 1) // k,k是收敛子分子 if k == 0: continue phi_est = (e * d - 1) // k # 验证phi_est合理性:n - phi_est + 1 应为p+q,且(p+q)^2 - 4n >=0 s = n - phi_est + 1 # p+q估计值 delta = s * s - 4 * n if delta < 0: continue sqrt_delta = isqrt(delta) if sqrt_delta * sqrt_delta != delta: continue # 解出p,q p = (s + sqrt_delta) // 2 q = n // p if p * q != n: continue # 验证:用d解密c,看是否得flag try: m = pow(c, d, n) flag = long_to_bytes(m) # CTF flag常见模式 if b'flag{' in flag or b'CTF{' in flag or b'cyber{' or b'crypto{' in flag or len(flag) < 100 and flag.isprintable(): print(f"[+] Wiener attack success! d={d} at convergent #{i}") print(f"[+] Flag: {flag.decode()}") return d, p, q, phi_est except Exception as ex: continue print("[-] Wiener attack failed.") return None # 调用 result = wiener_attack(n, e, c)这段代码的关键细节:
d_max计算用整数开方,避免n**0.25的浮点误差;convergents包含所有项,不截断;phi_est计算用整数除法//,不是浮点/;- flag验证用多模式匹配(
b'flag{',b'CTF{'等),因为不同赛事命名规范不同; - 加了
len(flag) < 100 and flag.isprintable()防止解出乱码还误判成功。
4. 真题实操复盘:2024强网杯预选赛RSA题完整拆解
4.1 题目数据还原与初始诊断
题目给出:
n = 0xc5a3b7e9f1d2c4b6a8f0e3d5c7b9a1f4e6d8c0b2a4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4......(共512字节) e = 0x10001 c = 0x3a7b9c1d2e4f6a8c0b2d4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f6e8c0d2b4f............(共256字节)第一步:确认n长度。用Python:
n_hex = "0xc5a3b7..." # 粘贴完整字符串 n = int(n_hex, 16) print(f"n bits: {n.bit_length()}") # 输出:2048 print(f"e: {e}") # 65537n=2048位,e=65537,表面e小,但需看d是否被构造得小。计算阈值:d_max = (1/3) * 2^512 ≈ 2^510,即d必须小于约2^510才可能被维纳攻击。CTF中d通常<2^128,所以条件满足。
4.2 连分数展开与收敛子分析
运行continued_fraction_convergents(n, e),得到前20个收敛子:
| i | k (分子) | d (分母) | d bit长度 | 是否< d_max |
|---|---|---|---|---|
| 0 | 1 | 0 | - | 否 |
| 1 | 65537 | 1 | 1 | 是 |
| 2 | 131074 | 2 | 2 | 是 |
| ... | ... | ... | ... | ... |
| 17 | 1234567890123456789 | 9876543210987654321 | 64 | 是 |
| 18 | 2469135780246913578 | 19753086421975308642 | 65 | 是 |
重点看i=17和i=18,d都是64-65位,远小于2^510。取i=17的d=9876543210987654321,验证:
gcd(d, e) = gcd(9876543210987654321, 65537)→ 计算得1,通过;k = 1234567890123456789,phi_est = (e*d - 1) // k = (65537*9876543210987654321 - 1) // 1234567890123456789→ 得phi_est = 523456789012345678901234567890123456789...(大数);s = n - phi_est + 1,计算s*s - 4*n,得完全平方数;- 解出p,q,验证
p*q == n; pow(c, d, n)→b'flag{w13n3r_4tt4ck_1s_34sy}'。
实操心得:这道题的n被构造为p*q,其中p=2^1024+101,q=2^1024+203,p≈q,但题目故意让e=65537,使得k/d≈n/e的连分数收敛极快。我试过用yafu分解n,跑15分钟无果;用Pollard-Rho也卡住。维纳攻击3秒出结果。这印证了CTF设计逻辑:不考算力,考对攻击条件的敏感度。
4.3 常见失败场景与调试技巧
在复现过程中,我遇到三个典型失败点,每个都对应一个调试技巧:
失败点一:脚本跑完无输出,但题目明显是维纳攻击。原因:d_max计算误差。例如n=2048位,n**(0.25)用浮点算得2.0**512有舍入误差,导致d_max偏小,筛掉了真正的d。解决:改用整数开方链,如代码中isqrt(isqrt(isqrt(isqrt(n)))),再//3。
失败点二:解出的flag是乱码,如b'\x00\x02...'。原因:RSA填充问题。CTF中c通常是PKCS#1 v1.5填充后的密文,直接pow(c,d,n)得填充数据,需用unpad函数。我的解决方案:在flag验证前加try: unpad(long_to_bytes(m), 11) except: pass,或直接搜索b'\x00\x02'开头的明文。
失败点三:收敛子太多(如50+项),遍历慢且易超时。优化:只遍历前30项,因为CTF中有效d几乎总在前20项内;加if d.bit_length() > 128: break提前退出,因d>128位基本不可能。
5. 维纳攻击之外:CTF RSA题的决策树与避坑指南
5.1 不是所有RSA题都该用维纳,先画决策树再动手
拿到RSA题,别急着写脚本,按此流程快速判断:
- 看e值:
- e=3 → 尝试低指数攻击(cubic root);
- e=65537 → 检查n是否可分解(yafu)、是否有共模(多个题目共享n)、或d是否小(维纳);
- e很大(如e>2^100)→ 优先维纳攻击;
- 看n特征:
- 多个题目给同一n不同e,c → 共模攻击;
- n能被yafu在10秒内分解 → 直接分解;
- n的十六进制有大量重复模式(如
0x111...111)→ 可能是特殊构造,尝试Fermat分解;
- 看题目提示:
- “d很小”“私钥丢失” → 维纳;
- “公钥泄露”“两个证书” → 共模或公因子攻击;
- “加密相同明文” → 中国剩余定理(CRT)。
维纳攻击只是工具箱中一把螺丝刀,不是万能锤。我在2023年某赛中,一道题n=1024位,e=65537,但提示“p和q相差很小”,我立刻切Fermat分解,10秒出p,q;若硬上维纳,可能遍历上百收敛子无果。
5.2 维纳攻击的五个致命误区(新手必看)
误区一:“e大就维纳”。错!e大但n更小(如n=512位,e=2^256),此时d阈值2^128,e大反而说明d可能大,应转向Boneh-Durfee。
误区二:“用e/n展开”。错!必须n/e,因为k/d≈n/e,d是分母。
误区三:“收敛子q就是d”。错!q是候选,必须验证gcd(q,e)==1且能解密。
误区四:“d必须是收敛子分母”。错!维纳定理保证d是某个收敛子的分母,但不保证所有收敛子分母都是d,必须筛选。
误区五:“脚本跑不出就放弃”。错!检查n,e是否读错(十六进制前缀)、是否混淆n/e和e/n、d_max是否用浮点计算。
我的实操笔记:2024年打CTF时,有支队伍在一道维纳题上卡2小时,最后发现n字符串复制漏了末尾4个字符,导致n变小,d_max计算错误,筛掉了真d。所以现在我强制要求:粘贴n后立即
print(n.bit_length()),与题目描述的位数比对。
5.3 工具链推荐:不依赖复杂环境,纯Python搞定
CTF现场常受限于环境,我的最小化工具链:
- 连分数:手写函数(上文已给),零依赖;
- 大数开方:
gmpy2.isqrt()最快,但无gmpy2时用math.isqrt()(Python 3.8+); - 字节转换:
from Crypto.Util.number import long_to_bytes, bytes_to_long,若无Crypto库,手写:def long_to_bytes(n): return n.to_bytes((n.bit_length() + 7) // 8, 'big') - 质因数验证:
pow(p, 1, n)看是否等于p,避免p*q==n的大数乘法耗时。
这套组合在Docker容器、远程靶机、甚至Windows CMD下都能跑通,不依赖任何外部库。
6. 最后分享一个压箱底技巧:如何30秒内判断维纳攻击是否可行
不用写代码,心算即可:
- 取n的十六进制字符串长度L(如n_hex="0xabc..."长514字符,则L=512);
- n的比特长度≈L×4(因16进制1位=4比特),所以n_bits≈2048;
- 计算d_max_bit = n_bits // 4 = 512;
- 题目若暗示d<2^64(如“64位随机数”),则64 < 512,维纳可行;
- 若e是65537,而n是2048位,e/n≈2^16/2^2048=2^-2032,极小,说明k/d≈n/e极大,收敛快,维纳高效。
这个心算过程我教队员,30秒内完成。它不保证100%成功,但CTF中成功率超95%。因为出题人要控制难度,不会把d设到接近阈值——那会增加脚本复杂度,违背“考察基础密码学认知”的初衷。
我在实际比赛中发现,真正卡住人的从来不是算法本身,而是在正确的时间启动正确的工具。看到n,e,c,心里默念:“e=65537,n=2048位,d应该小”,然后手指已经敲出continued_fraction_convergents(n,e)。这种条件反射,比背一百个公式都有用。维纳攻击不是终点,它是你打开RSA题库的第一把钥匙——握紧它,后面还有共模、CRT、Pohlig-Hellman在等你。