手把手复现BUUCTF babyRSA:用Python sympy和gmpy2库实战RSA已知私钥d的解密过程
从零破解BUUCTF babyRSA:Python密码学工具链深度实战
当你在CTF竞赛中遇到一道RSA题目,手头只有公钥指数e、私钥d和密文c时,该如何逆向还原出明文?本文将带你用Python密码学工具链,一步步拆解BUUCTF平台上的经典题目babyRSA。我们将重点使用 sympy 和 gmpy2 这两个库,通过数学推导和编程实践,完整复现从私钥d恢复RSA参数并解密密文的全过程。
1. 题目分析与数学基础准备
首先我们需要理解题目给出的关键信息。根据附件源码,可以提取以下已知参数:
- 公钥指数 e = 0x10001 (65537)
- 私钥 d = 192757... (一个2048位的大整数)
- 密文 c = 538272... (另一个2048位大整数)
题目特别之处在于素数生成方式:q是p的下一个素数。这意味着p和q非常接近,这在标准的RSA实现中是不安全的,但也正是我们破解的突破口。
RSA的核心数学关系是:
e * d ≡ 1 mod φ(n)
其中φ(n) = (p-1)*(q-1)。这意味着存在某个整数k,使得:
e * d - 1 = k * φ(n)
我们的目标就是找到这个k,进而恢复φ(n)和原始素数p、q。
2. 确定k的合理取值范围
为了高效爆破k值,我们需要先确定它的合理范围。已知:
- e*d-1的位数:约2063-2064位
- φ(n) = (p-1)(q-1)的位数:约2048位(因为p和q都是1024位素数)
因此k的位数应该是2063-2048=15到16位之间。我们可以用以下代码验证:
import gmpy2
d = 19275778946037899718035455438175509175723911466127462154506916564101519923603308900331427601983476886255849200332374081996442976307058597390881168155862238533018621944733299208108185814179466844504468163200369996564265921022888670062554504758512453217434777820468049494313818291727050400752551716550403647148197148884408264686846693842118387217753516963449753809860354047619256787869400297858568139700396567519469825398575103885487624463424429913017729585620877168171603444111464692841379661112075123399343270610272287865200880398193573260848268633461983435015031227070217852728240847398084414687146397303110709214913
e = 0x10001
for i in range(1000, 3000):
if e*d - 1 > 2**i and e*d - 1 < 2**(i+1):
print(f"e*d-1 is between 2^{i} and 2^{i+1}")
break
这段代码会输出e*d-1的二进制位数范围,确认我们的估算。
3. 爆破正确的k值并恢复素数p和q
有了k的范围(2^15到2^16),我们就可以编写爆破脚本了。关键步骤如下:
- 遍历可能的k值
- 对每个k,计算φ(n) = (e*d - 1) // k
- 由于p和q相邻,我们可以假设φ(n) ≈ (sqrt(n) - 1)^2
- 使用sympy.prevprime和gmpy2.next_prime找到相邻的素数对
实现代码如下:
import sympy
from Crypto.Util.number import long_to_bytes
for k in range(2**15, 2**16):
if (e*d - 1) % k == 0:
phi = (e*d - 1) // k
# 估算p和q的位置
approx_p = sympy.prevprime(gmpy2.iroot(phi, 2)[0] + 1)
q = gmpy2.next_prime(approx_p)
# 验证φ(n)是否正确
if (approx_p - 1) * (q - 1) == phi:
p = approx_p
break
elif (q - 1) * (gmpy2.next_prime(q) - 1) == phi:
p = q
q = gmpy2.next_prime(p)
break
n = p * q
这段代码有几个需要注意的细节:
gmpy2.iroot用于计算整数平方根,比普通浮点运算更精确sympy.prevprime找到小于给定数的最大素数- 需要验证找到的素数对确实满足φ(n)的关系
4. 解密获取flag
一旦我们恢复了p和q,计算n=p*q,就可以直接使用RSA解密公式:
m = c^d mod n
Python实现非常简单:
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag)
但这里有几个实际编码中容易踩的坑:
- 大整数运算效率 :直接使用Python的pow函数处理2048位的大整数可能会很慢。gmpy2库提供了更快的实现:
m = gmpy2.powmod(c, d, n)
- 字节转换 :Crypto.Util.number.long_to_bytes可能会因为填充问题导致输出异常,可以尝试以下替代方案:
flag = m.to_bytes((m.bit_length() + 7) // 8, 'big')
- 编码验证 :有时解密出的明文需要进一步解码,如UTF-8:
try:
print(flag.decode('utf-8'))
except UnicodeDecodeError:
print(flag.hex())
5. 完整攻击脚本与优化技巧
将上述步骤整合,我们得到完整的攻击脚本:
import gmpy2
import sympy
from Crypto.Util.number import long_to_bytes
# 给定参数
d = 19275778946037899718035455438175509175723911466127462154506916564101519923603308900331427601983476886255849200332374081996442976307058597390881168155862238533018621944733299208108185814179466844504468163200369996564265921022888670062554504758512453217434777820468049494313818291727050400752551716550403647148197148884408264686846693842118387217753516963449753809860354047619256787869400297858568139700396567519469825398575103885487624463424429913017729585620877168171603444111464692841379661112075123399343270610272287865200880398193573260848268633461983435015031227070217852728240847398084414687146397303110709214913
c = 5382723168073828110696168558294206681757991149022777821127563301413483223874527233300721180839298617076705685041174247415826157096583055069337393987892262764211225227035880754417457056723909135525244957935906902665679777101130111392780237502928656225705262431431953003520093932924375902111280077255205118217436744112064069429678632923259898627997145803892753989255615273140300021040654505901442787810653626524305706316663169341797205752938755590056568986738227803487467274114398257187962140796551136220532809687606867385639367743705527511680719955380746377631156468689844150878381460560990755652899449340045313521804
e = 0x10001
# 爆破k值
found = False
for k in range(2**15, 2**16):
if (e*d - 1) % k == 0:
phi = (e*d - 1) // k
# 尝试两种可能的素数对组合
approx_p = sympy.prevprime(gmpy2.iroot(phi, 2)[0] + 1)
q = sympy.nextprime(approx_p)
if (approx_p - 1) * (q - 1) == phi:
p = approx_p
found = True
break
next_q = sympy.nextprime(q)
if (q - 1) * (next_q - 1) == phi:
p = q
q = next_q
found = True
break
if not found:
print("Failed to find p and q")
exit()
n = p * q
m = gmpy2.powmod(c, d, n)
flag = long_to_bytes(m)
print("Recovered primes:")
print(f"p = {p}")
print(f"q = {q}")
print("\nFlag:")
print(flag)
性能优化技巧 :
- 并行计算 :使用multiprocessing库并行化k的爆破过程
- 早期终止 :一旦找到有效的k值就立即终止循环
- 预计算 :对于固定的e和d,可以预先计算e*d-1
- 缓存 :缓存素数检查结果,避免重复计算
6. 密码学安全启示与防御建议
通过这道题目,我们可以学到几个重要的密码学安全经验:
-
素数生成方式的重要性 :
- 永远不要使用相邻的素数作为p和q
- 使用安全的随机素数生成方法,如OpenSSL的BN_generate_prime_ex
-
密钥参数验证 :
- 实现时应检查p和q的差值足够大
- 验证gcd(p-1, e) = 1和gcd(q-1, e) = 1
-
实际应用中的防御措施 :
| 攻击类型 | 防御方法 |
|---|---|
| 因式分解攻击 | 使用足够大的密钥长度(至少2048位) |
| 侧信道攻击 | 使用恒定时间算法 |
| 错误注入攻击 | 实现参数验证和错误检查 |
- CTF中的常见RSA变种 :
- 小公钥指数e(如e=3)
- 共模攻击
- Wiener攻击(当d较小时)
- 已知部分私钥信息攻击
7. 扩展练习与进一步学习
为了加深理解,建议尝试以下扩展练习:
- 修改题目参数 :尝试不同的e值(如e=3)观察破解难度变化
- 更大密钥长度 :将素数长度扩展到2048位,测试脚本的适应性
- 其他CTF题目 :尝试解决类似的RSA题目,如:
- "Very Smooth" (PicoCTF)
- "Broken RSA" (HackTheBox)
- 性能对比 :比较sympy、gmpy2和原生Python在大数运算中的性能差异
推荐的学习资源:
- 《应用密码学手册》中的RSA章节
- Cryptopals挑战中的RSA相关题目
- OpenSSL源码中的RSA实现
在实际CTF比赛中遇到RSA题目时,这套方法可以作为一个标准的工作流程:分析题目给出的特殊条件,利用数学关系推导可能的攻击路径,最后用Python密码学工具链实现攻击。
更多推荐
所有评论(0)