CTFCrypto题解:多素数RSA上的Wiener攻击

admin 2026-03-03 03:39:54 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详解ASISCTF题目,针对三素数RSA与ElGamal混合加密方案,利用私钥d过小的漏洞实施Wiener攻击。文章推导了多素数环境下的数学原理,通过连分数及线性组合穷举恢复私钥并解密获取Flag,强调了密钥生成顺序不当引发的严重风险。 综合评分: 90 文章分类: CTF,漏洞分析,实战经验


cover_image

CTF Crypto 题解:多素数 RSA 上的 Wiener 攻击

原创

破镜安全 破镜安全

破镜安全

2026年3月1日 14:20 四川

CTF Crypto 题解:多素数 RSA 上的 Wiener 攻击

题目信息

  • 比赛:ASIS CTF
  • 题目名称:Gracias
  • 类别:Crypto(密码学)
  • 附件:gracias.py(加密程序源码)、enc_pubkey.txt(公钥与密文)

一、读懂题目:附件里藏着什么

拿到题目后,首先阅读两个附件。

enc_pubkey.txt 给出了公钥四元组和密文对:

pubkey = (n, e, a, g)
c = (c1, c2)

具体数值(已截短展示):

n  = 16968...61567   (1536 位)
e  = 81416...97451   (1535 位)
a  = 17101...77523   (1024 位)
g  = 96969...64722   (1024 位)
c1 = 15697...49773   (1536 位)
c2 = 13953...39827   (1024 位)

gracias.py 是加密程序的完整源码:

def make_pubpri(nbit):
    p, q, r = [ getPrime(nbit) for _ in xrange(3)]
    n = p * q * r
    phi = (p-1)*(q-1)*(r-1)
    l = min([p, q, r])
&nbsp; &nbsp; d = getPrime(1&nbsp;<<&nbsp;8)
&nbsp; &nbsp; e = inverse(d, phi)
&nbsp; &nbsp; a = gensafeprime.generate(2*nbit)
&nbsp; &nbsp;&nbsp;while&nbsp;True:
&nbsp; &nbsp; &nbsp; &nbsp; g = getRandomRange(2, a)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;pow(g,&nbsp;2, a) !=&nbsp;1&nbsp;and&nbsp;pow(g, a/2, a) !=&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;return&nbsp;(n, e, a, g), (n, d, a, g)

def&nbsp;encrypt(m, pub):
&nbsp; &nbsp; n, e, a, g = pub
&nbsp; &nbsp; k = getRandomRange(2, a)
&nbsp; &nbsp; K = pow(g, k, a)
&nbsp; &nbsp; c1, c2 = pow(k, e, n), (m * K) % a
&nbsp; &nbsp;&nbsp;return&nbsp;c1, c2

nbit =&nbsp;512
pubkey, privkey = make_pubpri(nbit)
m = bytes_to_long(FLAG)
c = encrypt(m, pubkey)

有了源码,加密过程完全透明,接下来逐步拆解。


二、理解加密方案:两层结构

2.1 密钥生成(make_pubpri)

密钥生成分为两个独立部分:RSA 部分和 ElGamal 部分。

RSA 部分:

p, q, r = [ getPrime(nbit)&nbsp;for&nbsp;_&nbsp;in&nbsp;xrange(3)] &nbsp;&nbsp;# nbit = 512
n = p * q * r
phi = (p-1)*(q-1)*(r-1)
d = getPrime(1&nbsp;<<&nbsp;8)
e = inverse(d, phi)

这里生成了三个 512 位素数 p、q、r,模数 n = p * q * r,共 1536 位。

这是三素数 RSA,与标准 RSA(两素数)的结构相同,只是多一个因子。RSA 的核心关系依然成立:

e * d ≡ 1 &nbsp;(mod phi(n))

其中 phi(n) = (p-1)(q-1)(r-1) 是欧拉函数。

值得注意的是密钥生成顺序:程序先随机选取了一个 256 位素数 d(d = getPrime(1 << 8),其中 1 << 8 = 256),再由 d 推算出 e。这与标准做法(先固定 e=65537 再算 d)相反,导致 d 极小。

ElGamal 部分:

a = gensafeprime.generate(2*nbit) &nbsp;&nbsp;# 1024 位安全素数
while&nbsp;True:
&nbsp; &nbsp; g = getRandomRange(2, a)
&nbsp; &nbsp;&nbsp;if&nbsp;pow(g,&nbsp;2, a) !=&nbsp;1&nbsp;and&nbsp;pow(g, a/2, a) !=&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

a 是一个 1024 位的安全素数(safe prime),即 a = 2p’ + 1,其中 p’ 也是素数。g 满足两个条件:

  • g^2 mod a != 1:g 不是 ±1
  • g^((a-1)/2) mod a != 1:g 不是二次剩余,即不在阶为 2 的子群中

这两个条件保证 g 是 a 的原根(primitive root),即 g 生成模 a 意义下的完整乘法群,群阶为 phi(a) = a-1 = 2p’。

公钥为 (n, e, a, g),私钥为 (n, d, a, g)

2.2 加密过程(encrypt)

k = getRandomRange(2, a) &nbsp; &nbsp; &nbsp;&nbsp;# 随机临时密钥
K = pow(g, k, a) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# K = g^k mod a
c1 = pow(k, e, n) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# c1 = k^e mod n &nbsp;(RSA 加密 k)
c2 = (m * K) % a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# c2 = m * K mod a(乘法掩盖)

加密分两步:

第一步:用 RSA 公钥 (n, e) 加密随机临时密钥 k,得到 c1。

第二步:计算 K = g^k mod a,再用 K 与消息 m 相乘后取模,得到 c2 = m * K mod a。

这是一个混合加密方案(Hybrid Encryption):RSA 负责安全地传递会话密钥 k,ElGamal 风格的乘法掩盖负责隐藏消息 m。

2.3 正确的解密流程

如果知道私钥 d,解密分三步:

第一步:k &nbsp;= c1^d mod n &nbsp; &nbsp; &nbsp; &nbsp; (RSA 解密,还原临时密钥)
第二步:K &nbsp;= g^k mod a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(重建共享密钥)
第三步:m &nbsp;= c2 * K^(-1) mod a &nbsp;(乘法逆元,还原消息)

第三步的原理:c2 = m * K mod a,所以 m = c2 / K mod a = c2 * K^(-1) mod a,其中 K^(-1) 是 K 在模 a 意义下的乘法逆元。


三、发现漏洞:私钥 d 严重过小

仔细观察密钥生成的这一行:

d = getPrime(1&nbsp;<<&nbsp;8)

1 << 8 = 256,即 d 是一个随机的 256 位素数

而 n 是三个 512 位素数的乘积,共约 1536 位

对比一下比特长度:

n 的比特长度:1536 位
d 的比特长度:256 位
比值:d 约为 n^(1/6)

这个比例是极度危险的。RSA 的安全性假设之一是私钥指数 d 足够大。1990 年,Michael Wiener 证明:当 d < N^(1/4) / 3 时,可以从公钥 (N, e) 高效恢复 d,无需分解 N。

本题 n ≈ 2^1536,n^(1/4) ≈ 2^384,而 d ≈ 2^256,256 远小于 384,Wiener 攻击的条件宽裕地满足。


四、Wiener 攻击:从数学到实现

4.1 连分数的基本概念

在介绍攻击之前,先理解连分数(Continued Fraction)。

任意实数 x 可以写成如下形式:

x = a_0 + 1/(a_1 + 1/(a_2 + 1/(a_3 + ...)))

对于有理数,这个过程是有限的,可以通过辗转相除法计算:

x &nbsp; &nbsp; &nbsp; = a_0 + 余数_0/分母
a_0 &nbsp; &nbsp; = floor(x)
余数_0 &nbsp;= x - a_0
然后对 1/余数_0 重复上述步骤...

连分数展开的每一步截断,得到的分数 h_i/k_i 称为渐近分数(convergent)。渐近分数具有一个重要性质:它们是对原始数的”最佳有理近似”,即在分母不超过 k_i 的所有分数中,h_i/k_i 最接近 x。

4.2 Wiener 攻击的数学推导

由 RSA 基本关系:

e * d = 1 + k * phi(n) &nbsp; &nbsp;(k 是某个正整数)

两边同除以 n * d:

e/n = k/d + (1 - k*(n - phi(n))) / (n*d)

右边第二项是一个很小的误差。对于标准两素数 RSA(n = p*q):

n - phi(n) = p + q - 1 ≈ 2*sqrt(n)

误差项约为:

|e/n - k/d| ≈ 2k / (d * sqrt(n))

由 Legendre 定理,当某个分数 p/q 满足 |x – p/q| < 1/(2*q^2) 时,p/q 一定是 x 连分数展开的某个渐近分数。将上面的误差代入这个条件:

2k / (d * sqrt(n)) < 1/(2*d^2)
=> d < n^(1/4) / (4k)^(1/2) ≈ n^(1/4) / 3

这就是著名的 Wiener 边界:d < n^(1/4) / 3 时,k/d 必然出现在 e/n 的连分数展开中,攻击者遍历所有渐近分数即可找到 d。

4.3 三素数情形的推广

本题 n = p * q * r,phi(n) = (p-1)(q-1)(r-1),展开计算:

n - phi(n) = pqr - (p-1)(q-1)(r-1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= pq + pr + qr - (p + q + r) + 1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;≈ 3 * n^(2/3) &nbsp; (因为 p, q, r 各约为 n^(1/3))

这比两素数情况下的 2sqrt(n) = 2n^(1/2) 大得多(2/3 > 1/2),误差项变大:

|e/n - k/d| ≈ 3k / (d * n^(1/3))

对应的有效边界变为 d < n^(1/3) / (某常数)。

本题 n^(1/3) ≈ 2^512,而 d ≈ 2^256,条件依然充分满足。

然而由于误差增大,d 未必精确等于某个渐近分数的分母 k_i,而是可能表示为相邻两个渐近分数分母的线性组合

d = alpha * k_{i-1} + beta * k_i

其中 alpha 和 beta 是很小的正整数。解题脚本的 multi 模式正是利用这一点,在遍历渐近分数的同时,对 alpha 和 beta 进行小范围穷举。

4.4 渐近分数的计算:next_frac 函数

def&nbsp;next_frac(self):
&nbsp; &nbsp; hi_1, hi_2 =&nbsp;1,&nbsp;0
&nbsp; &nbsp; ki_1, ki_2 =&nbsp;0,&nbsp;1
&nbsp; &nbsp; a, b = self.e, self.N
&nbsp; &nbsp; r =&nbsp;1
&nbsp; &nbsp;&nbsp;while&nbsp;r:
&nbsp; &nbsp; &nbsp; &nbsp; p, r = divmod(a, b) &nbsp; &nbsp; &nbsp; &nbsp;# a = p*b + r(辗转相除)
&nbsp; &nbsp; &nbsp; &nbsp; hi = p * hi_1 + hi_2 &nbsp; &nbsp; &nbsp;&nbsp;# 渐近分数分子递推
&nbsp; &nbsp; &nbsp; &nbsp; ki = p * ki_1 + ki_2 &nbsp; &nbsp; &nbsp;&nbsp;# 渐近分数分母递推
&nbsp; &nbsp; &nbsp; &nbsp; hi_1, hi_2 = hi, hi_1
&nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 = ki, ki_1
&nbsp; &nbsp; &nbsp; &nbsp; a, b = b, r
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;yield&nbsp;(hi, ki) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 逐步产出渐近分数 (h_i, k_i)

这个函数对 e/N 做辗转相除,用递推公式 h_i = a_i * h_{i-1} + h_{i-2}k_i = a_i * k_{i-1} + k_{i-2} 逐步生成渐近分数序列,每次 yield 一个 (h_i, k_i) 对。

4.5 多素数穷举:multi 函数

def&nbsp;multi(self, rate):
&nbsp; &nbsp; N, e = self.N, self.e
&nbsp; &nbsp; pt = getRandomRange(2, N -&nbsp;1) &nbsp;&nbsp;# 随机选取测试明文
&nbsp; &nbsp; ct = pow(pt, e, N) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 计算对应密文
&nbsp; &nbsp; ki_1, ki_2 =&nbsp;0,&nbsp;1
&nbsp; &nbsp;&nbsp;for&nbsp;frac&nbsp;in&nbsp;self.next_frac():
&nbsp; &nbsp; &nbsp; &nbsp; hi, ki = frac
&nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 = ki, ki_1 &nbsp; &nbsp; &nbsp;# 维护相邻两个渐近分数分母
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;hi ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; _ki_1, _ki_2 = ki_1, ki_2
&nbsp; &nbsp; &nbsp; &nbsp; check =&nbsp;lambda&nbsp;rs: pow(ct, int(rs[:rate],&nbsp;2) * _ki_1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + int(rs[rate:],&nbsp;2) * _ki_2, N) == pt
&nbsp; &nbsp; &nbsp; &nbsp; rs = mbruteforce(check,&nbsp;'01',&nbsp;2&nbsp;* rate, method='fixed')
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;rs:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;int(rs[:rate],&nbsp;2) * ki_1 + int(rs[rate:],&nbsp;2) * ki_2
&nbsp; &nbsp;&nbsp;return&nbsp;None

逐步解析:

第一步,构造验证对(pt, ct):

随机取一个明文 pt,计算 ct = pt^e mod N。这对数据的用途是:若某个候选 d’ 满足 ct^(d’) ≡ pt (mod N),则 d’ 就是正确的私钥 d。这个验证以压倒性的概率成立,因为 RSA 加解密的正确性保证了只有真正的 d 能通过验证。

第二步,遍历渐近分数,维护相邻分母:

ki_1 和 ki_2 始终保存最新的两个渐近分数分母。

第三步,穷举线性组合:

mbruteforce 枚举所有长度为 2*rate = 8 的二进制串(共 2^8 = 256 种)。每条二进制串 rs 被分为两半:

  • 前 4 位 rs[:4] 转为整数,作为 alpha(范围 0~15)
  • 后 4 位 rs[4:] 转为整数,作为 beta(范围 0~15)

构造候选 d:

d_candidate = alpha * ki_1 + beta * ki_2

第四步,验证候选 d:

用 check 函数验证 pow(ct, d_candidate, N) == pt。一旦通过,返回该 d_candidate 作为最终结果。

整个过程的搜索空间:(渐近分数数量,量级约为 log(N)) × 256 种线性组合,总计数百万次模幂运算,在现代计算机上数分钟内可以完成。


五、解密:还原 flag

找到 d 后,解密过程分三步,完整代码如下:

def&nbsp;solve():
&nbsp; &nbsp; n &nbsp;=&nbsp;1696852658826990842058316561963467335977986730245296081842693913454799128341723605666024757923000936875008280288574503060506225324560725525210728761064310034604441130912702077320696660565727540525259413564999213382434231194132697630244074950529107794905761549606578049632101483460345878198682237227139704889943489709170676301481918176902970896183163611197618458670928730764124354693594769219086662173889094843054787693685403229558143793832013288487194871165461567
&nbsp; &nbsp; e &nbsp;=&nbsp;814161885590044357190593282132583612817366020133424034468187008267919006610450334193936389251944312061685926620628676079561886595567219325737685515818965422518820810326234612624290774570873983198113409686391355443155606621049101005048872030700143084978689888823664771959905075795440800042648923901406744546140059930315752131296763893979780940230041254506456283030727953969468933552050776243515721233426119581636614777596169466339421956338478341355508343072697451
&nbsp; &nbsp; a &nbsp;=&nbsp;171012227587318507773834753911094468358648971527111097308935888531930900156798659257578479378777764146070352809723708236353390208094909385240006920137781562826981091183813955039359863361624869703055918575613667858215532572602435432258750639197322091887713402631456113333645709142822182724397962837201266977523
&nbsp; &nbsp; g &nbsp;=&nbsp;96969753191136466007366303619618019752521508403657426306543836447071659732926802256183021740376016065813234292694535879838415771865207311953800116203362150588941093508091412441933752168889516206420588410478242229762908362637083786338280959547015086176046206126019992386890758970740552952647510652431386064722
&nbsp; &nbsp; c1 =&nbsp;1569733526826523065259704222721381245770313117205865099913421859731162526943498524936251685846967970606251353344665893442015804015671457823645874503670136308040791285744658847419176471348768113798503897694020110157476679833746227801224812046930570487233225157924912272791212802495997329083436189937249314855532400635293522270501567950040825794060896420481676398789310029592608176167251882124182145471818654414925639589921023176070657483148482403065241178276749773
&nbsp; &nbsp; c2 =&nbsp;139537660044872985880471632333334179976891152860359271230202507995985566816703080930428310461057387079799847266510420206696052591677854190150642820963140050439023069266243433278700748622126726137374130247097863526461696642750021196138340072411724739383716017406022211953417323065831672315854246554523225039827

&nbsp; &nbsp; wiener = Wiener(n, e,&nbsp;1) &nbsp; &nbsp; &nbsp;&nbsp;# mode=1 启用多素数模式
&nbsp; &nbsp; d = wiener.attack() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Wiener 攻击恢复私钥 d

&nbsp; &nbsp; k = pow(c1, d, n) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# RSA 解密:还原临时密钥 k
&nbsp; &nbsp; K = pow(g, k, a) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 重建共享密钥:K = g^k mod a
&nbsp; &nbsp; m = c2 * inverse(K, a) % a &nbsp; &nbsp;# 消除掩盖:m = c2 * K^(-1) mod a

&nbsp; &nbsp;&nbsp;return&nbsp;long_to_bytes(m)

实际运行输出:

[*] Running Wiener attack (multi-prime mode)...
[+] Found d = 100556095937036905102538523179832446199526507742826168666218687736467897968451
&nbsp; &nbsp; d bit length: 256
[*] Decrypting...
[+] Flag: ASIS{Wiener_at7ack_iN_mUlt1_Prim3_RSA_iZ_f34sible_t0O!}

攻击成功找到了 256 位的私钥 d,随后完整还原出 flag。


六、为什么这个加密方案不安全:根本原因

6.1 错误的密钥生成顺序

标准 RSA 密钥生成应当:

  1. 先固定公钥指数 e(通常取 e = 65537)
  2. 再计算 d = e^(-1) mod phi(n)

此时 d 的大小由 phi(n) 决定,约为 n 的量级,足够大。

本题反其道而行之:先选 d(256 位),再算 e。虽然数学上完全合法(e 和 d 的关系是对称的),但导致 d 极小,直接触发 Wiener 攻击。

6.2 三素数 RSA 不能弥补这一缺陷

三素数 RSA 相比两素数 RSA 的主要优势在于:分解 n = pqr 在计算上更难,且在已知部分私钥的情况下更难完全破解。但这些优势的前提是 d 足够大。Wiener 攻击完全绕过了分解 n 这一步骤,只需利用 e/n 的连分数展开恢复 d,三素数的结构不提供任何额外保护。

6.3 安全素数和原根的选取是否有帮助

a 被选为安全素数,g 被选为 a 的原根,这使得模 a 的离散对数问题(DLP)在理论上是困难的。但在这道题中,攻击者根本不需要求解离散对数——通过 Wiener 攻击恢复 d 之后,直接用 RSA 解密 c1 即可得到 k,进而计算 K = g^k mod a。DLP 的困难性在已知 k 的情况下毫无意义。


七、关键知识点梳理

RSA 基本原理

RSA 加密:c = m^e mod n RSA 解密:m = c^d mod n 关键关系:e * d ≡ 1 (mod phi(n))

phi(n) 对于三素数 RSA 的计算:phi(pqr) = (p-1)(q-1)(r-1)

连分数与渐近分数

  • 连分数展开:通过辗转相除,将有理数表示为整数序列
  • 渐近分数(convergent):连分数每一步截断得到的最优有理近似
  • Legendre 定理:若 |x – p/q| < 1/(2*q^2),则 p/q 是 x 的某个渐近分数

Wiener 攻击的核心思路

由于 e*d ≡ 1 (mod phi(n)),分数 k/d 近似等于 e/n(其中 k 是辅助整数)。当 d 很小时,k/d 出现在 e/n 的连分数展开的渐近分数中,从而可以恢复 d。

Wiener 边界(两素数):d < N^(1/4) / 3 Wiener 边界(三素数):d < N^(1/3) / 常数

混合加密结构

本题使用 RSA 加密会话密钥、ElGamal 风格乘法掩盖加密消息的混合方案,属于教科书级别的密码协议构造,思路本身是合理的,漏洞完全源于参数选取错误。


八、防御建议

  1. 不要先选 d 再推 e:正确做法是先固定 e(推荐 65537),再计算 d = e^(-1) mod phi(n),让 d 自然地取到正确大小。
  2. 验证 d 的比特长度:在密钥生成后检查 d 的比特长度,若 d.bit_length() 远小于 n.bit_length() / 2,应立即重新生成密钥。
  3. 使用成熟的密码库:OpenSSL、Python cryptography 库等经过审计的实现不会产生此类参数错误,不要从零手工实现 RSA 密钥生成。

九、完整解题脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from&nbsp;gmpy2&nbsp;import&nbsp;is_square, isqrt, mpz
from&nbsp;Crypto.Util.number&nbsp;import&nbsp;long_to_bytes, getRandomRange, inverse
from&nbsp;pwnlib.util.iters&nbsp;import&nbsp;mbruteforce

class&nbsp;Wiener(object):
&nbsp; &nbsp;&nbsp;def&nbsp;__init__(self, N, e, mode):
&nbsp; &nbsp; &nbsp; &nbsp; self.N = N
&nbsp; &nbsp; &nbsp; &nbsp; self.e = e
&nbsp; &nbsp; &nbsp; &nbsp; self.mode = mode

&nbsp; &nbsp;&nbsp;def&nbsp;next_frac(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 对 e/N 做连分数展开,逐步产出渐近分数 (h_i, k_i)
&nbsp; &nbsp; &nbsp; &nbsp; hi_1, hi_2 =&nbsp;1,&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 =&nbsp;0,&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; a, b = self.e, self.N
&nbsp; &nbsp; &nbsp; &nbsp; r =&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;r:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p, r = divmod(a, b)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hi = p * hi_1 + hi_2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ki = p * ki_1 + ki_2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hi_1, hi_2 = hi, hi_1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 = ki, ki_1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a, b = b, r
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;yield&nbsp;(hi, ki)

&nbsp; &nbsp;&nbsp;def&nbsp;multi(self, rate):
&nbsp; &nbsp; &nbsp; &nbsp; N, e = self.N, self.e
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 构造验证对:ct = pt^e mod N,用于验证候选 d
&nbsp; &nbsp; &nbsp; &nbsp; pt = getRandomRange(2, N -&nbsp;1)
&nbsp; &nbsp; &nbsp; &nbsp; ct = pow(pt, e, N)
&nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 =&nbsp;0,&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;frac&nbsp;in&nbsp;self.next_frac():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hi, ki = frac
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ki_1, ki_2 = ki, ki_1 &nbsp;&nbsp;# 维护相邻两个渐近分数分母
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;hi ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _ki_1, _ki_2 = ki_1, ki_2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 穷举 alpha 和 beta(各 4 位,共 256 种组合)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; check =&nbsp;lambda&nbsp;rs: pow(ct,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; int(rs[:rate],&nbsp;2) * _ki_1 + int(rs[rate:],&nbsp;2) * _ki_2,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; N) == pt
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rs = mbruteforce(check,&nbsp;'01',&nbsp;2&nbsp;* rate, method='fixed')
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;rs:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;int(rs[:rate],&nbsp;2) * ki_1 + int(rs[rate:],&nbsp;2) * ki_2
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None

&nbsp; &nbsp;&nbsp;def&nbsp;attack(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;self.mode:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;self.multi(4) &nbsp;&nbsp;# 多素数模式
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;self.two() &nbsp; &nbsp; &nbsp;# 两素数模式

def&nbsp;solve():
&nbsp; &nbsp; n &nbsp;= mpz(1696852658826990842058316561963467335977986730245296081842693913454799128341723605666024757923000936875008280288574503060506225324560725525210728761064310034604441130912702077320696660565727540525259413564999213382434231194132697630244074950529107794905761549606578049632101483460345878198682237227139704889943489709170676301481918176902970896183163611197618458670928730764124354693594769219086662173889094843054787693685403229558143793832013288487194871165461567)
&nbsp; &nbsp; e &nbsp;= mpz(814161885590044357190593282132583612817366020133424034468187008267919006610450334193936389251944312061685926620628676079561886595567219325737685515818965422518820810326234612624290774570873983198113409686391355443155606621049101005048872030700143084978689888823664771959905075795440800042648923901406744546140059930315752131296763893979780940230041254506456283030727953969468933552050776243515721233426119581636614777596169466339421956338478341355508343072697451)
&nbsp; &nbsp; a &nbsp;= mpz(171012227587318507773834753911094468358648971527111097308935888531930900156798659257578479378777764146070352809723708236353390208094909385240006920137781562826981091183813955039359863361624869703055918575613667858215532572602435432258750639197322091887713402631456113333645709142822182724397962837201266977523)
&nbsp; &nbsp; g &nbsp;= mpz(96969753191136466007366303619618019752521508403657426306543836447071659732926802256183021740376016065813234292694535879838415771865207311953800116203362150588941093508091412441933752168889516206420588410478242229762908362637083786338280959547015086176046206126019992386890758970740552952647510652431386064722)
&nbsp; &nbsp; c1 = mpz(1569733526826523065259704222721381245770313117205865099913421859731162526943498524936251685846967970606251353344665893442015804015671457823645874503670136308040791285744658847419176471348768113798503897694020110157476679833746227801224812046930570487233225157924912272791212802495997329083436189937249314855532400635293522270501567950040825794060896420481676398789310029592608176167251882124182145471818654414925639589921023176070657483148482403065241178276749773)
&nbsp; &nbsp; c2 = mpz(139537660044872985880471632333334179976891152860359271230202507995985566816703080930428310461057387079799847266510420206696052591677854190150642820963140050439023069266243433278700748622126726137374130247097863526461696642750021196138340072411724739383716017406022211953417323065831672315854246554523225039827)

&nbsp; &nbsp; print('[*] 正在执行 Wiener 攻击(多素数模式)...')
&nbsp; &nbsp; wiener = Wiener(n, e,&nbsp;1)
&nbsp; &nbsp; d = wiener.attack()
&nbsp; &nbsp; print(f'[+] 恢复私钥 d =&nbsp;{d}')
&nbsp; &nbsp; print(f' &nbsp; &nbsp;d 比特长度:{int(d).bit_length()}&nbsp;位')

&nbsp; &nbsp; k = pow(c1, d, n)
&nbsp; &nbsp; K = pow(g, k, a)
&nbsp; &nbsp; m = c2 * inverse(K, a) % a

&nbsp; &nbsp; flag = long_to_bytes(m)
&nbsp; &nbsp; print(f'[+] Flag:{flag.decode()}')

if&nbsp;__name__ ==&nbsp;'__main__':
&nbsp; &nbsp; solve()

运行结果:

[*] 正在执行 Wiener 攻击(多素数模式)...
[+] 恢复私钥 d = 100556095937036905102538523179832446199526507742826168666218687736467897968451
&nbsp; &nbsp; d 比特长度:256 位
[+] Flag:ASIS{Wiener_at7ack_iN_mUlt1_Prim3_RSA_iZ_f34sible_t0O!}

十、总结

这道题的加密方案本身设计思路合理——用 RSA 加密会话密钥、ElGamal 乘法掩盖加密消息是成熟的混合加密模式,使用安全素数和原根也是正确选择。然而一行代码的失误(d = getPrime(256))让整个方案土崩瓦解:私钥 d 仅有 256 位,而模数 n 有 1536 位,Wiener 攻击的条件被极度宽松地满足。

攻击的完整链路是:利用 e/n 的连分数展开,在相邻渐近分数分母的线性组合中穷举出 d,再依次完成 RSA 解密和 ElGamal 解密,还原消息。全过程无需分解 1536 位的模数。

flag:ASIS{Wiener_at7ack_iN_mUlt1_Prim3_RSA_iZ_f34sible_t0O!}

这个 flag 本身就是一句话的总结:在多素数 RSA 上实施 Wiener 攻击是可行的。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:破镜安全 破镜安全 破镜安全《CTF Crypto 题解:多素数 RSA 上的 Wiener 攻击》

四道几何挑战 网络安全文章

四道几何挑战

文章总结: 文档分享了四道难度递增的几何挑战题,内容涉及三角形角平分线夹角计算、面积求解以及正方形与正三角形内的距离极值问题。作者表示部分题目源自面试真题或个人
评论:0   参与:  0