Python密码学:手把手教你复现BUUCTF的babyRSA,理解RSA密钥生成中的‘坑’
Python密码学实战:从BUUCTF的babyRSA案例剖析RSA密钥生成隐患
在CTF竞赛中,RSA题目往往考验选手对密码学原理的深入理解而非单纯套用算法。BUUCTF平台上的babyRSA题目就是一个典型例子,它揭示了RSA密钥生成过程中一个容易被忽视却极其危险的问题——当两个大质数p和q过于接近时,系统将面临被快速破解的风险。本文将带您从零开始复现整个攻击流程,通过Python代码演示不安全的密钥生成方式如何导致加密体系崩溃。
1. 理解题目背景与密钥生成漏洞
BUUCTF的babyRSA题目源自NCTF2019,其核心漏洞源于附件中特殊的 nextPrime 函数实现。我们先来看原始密钥生成代码的关键部分:
from Crypto.Util.number import *
def nextPrime(n):
n += 2 if n & 1 else 1
while not isPrime(n):
n += 2
return n
p = getPrime(1024)
q = nextPrime(p) # 关键问题点
n = p * q
这段代码中, q 被设置为 p 的下一个相邻质数。虽然两者都是1024位的大质数,但它们的数值过于接近,这违背了RSA安全性的基本原则—— p和q应该独立随机生成 。
1.1 为什么相邻质数会带来风险?
当p和q接近时,它们的乘积n会满足一个特殊性质:
n = p × q ≈ p²
这意味着p和q都接近于√n。攻击者可以利用这个特性,从n的平方根附近开始搜索质数,而不是盲目遍历整个1024位数字空间,这将极大降低破解难度。
2. 复现不安全的密钥生成过程
为了更好地理解漏洞,我们先完整复现题目中的密钥生成流程。以下Python代码模拟了原题的密钥生成:
from Crypto.Util.number import getPrime, isPrime, inverse, bytes_to_long
import random
def nextPrime(n):
n += 2 if n & 1 else 1
while not isPrime(n):
n += 2
return n
# 生成有问题的密钥对
p = getPrime(1024)
q = nextPrime(p)
n = p * q
e = 0x10001 # 常见的RSA公钥指数
phi = (p-1)*(q-1)
d = inverse(e, phi)
# 模拟加密过程
flag = "flag{test_flag_for_demo}"
m = bytes_to_long(flag.encode())
c = pow(m, e, n)
print("模拟生成的n:", n)
print("加密后的密文c:", c)
print("私钥d:", d)
运行这段代码,您将得到一组看似正常但实际上脆弱的RSA密钥参数。接下来,我们将展示如何在不知道p和q的情况下,仅凭n、e、d和c破解这个系统。
3. 费马分解法破解相邻质数的RSA
费马分解法是一种专门针对p和q接近情况的因式分解算法。其核心思想是将n表示为两个平方数的差:
n = a² - b² = (a+b)(a-b)
当p和q接近时,b会非常小,使得算法效率极高。以下是Python实现:
import gmpy2
from Crypto.Util.number import long_to_bytes
def fermat_factor(n):
a = gmpy2.isqrt(n) + 1
b2 = a*a - n
while not gmpy2.is_square(b2):
a += 1
b2 = a*a - n
b = gmpy2.isqrt(b2)
return (a - b, a + b)
# 假设我们只有n、e、d和c
n = 12345678901234567890 # 替换为实际n值
e = 0x10001
d = 19275778946037899718035455438175509175723911466127462154506916564101519923603308900331427601983476886255849200332374081996442976307058597390881168155862238533018621944733299208108185814179466844504468163200369996564265921022888670062554504758512453217434777820468049494313818291727050400752551716550403647148197148884408264686846693842118387217753516963449753809860354047619256787869400297858568139700396567519469825398575103885487624463424429913017729585620877168171603444111464692841379661112075123399343270610272287865200880398193573260848268633461983435015031227070217852728240847398084414687146397303110709214913
c = 5382723168073828110696168558294206681757991149022777821127563301413483223874527233300721180839298617076705685041174247415826157096583055069337393987892262764211225227035880754417457056723909135525244957935906902665679777101130111392780237502928656225705262431431953003520093932924375902111280077255205118217436744112064069429678632923259898627997145803892753989255615273140300021040654505901442787810653626524305706316663169341797205752938755590056568986738227803487467274114398257187962140796551136220532809687606867385639367743705527511680719955380746377631156468689844150878381460560990755652899449340045313521804
# 分解n
p, q = fermat_factor(n)
print("分解得到的p:", p)
print("分解得到的q:", q)
# 验证分解结果
assert p * q == n
# 解密消息
phi = (p-1)*(q-1)
d_calculated = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print("解密后的flag:", flag)
3.1 费马分解的效率分析
为了展示费马分解法在p和q接近时的效率优势,我们对比不同质数间距下的计算步骤:
| 质数间距(bits) | 平均尝试次数 | 相对时间 |
|---|---|---|
| 0-10 | <100 | 1x |
| 10-100 | ~1,000 | 10x |
| 100-1000 | ~100,000 | 1000x |
| 随机分布 | >2¹⁰²⁴ | 不可行 |
这个表格清晰地展示了为什么RSA标准要求p和q必须独立随机生成——即使微小的非随机性也可能导致安全性的灾难性崩溃。
4. 从数学原理理解攻击可行性
要深入理解这个攻击,我们需要探讨背后的数学原理。已知:
ed ≡ 1 mod φ(n)
φ(n) = (p-1)(q-1) = n - (p+q) + 1
当p和q接近时,我们可以利用以下关系:
- 设p ≈ q ≈ √n
- 因此,p + q ≈ 2√n
- φ(n) ≈ n - 2√n + 1
攻击者可以利用这个近似关系,通过ed-1是φ(n)的倍数这一事实来缩小搜索范围:
from sympy import prevprime, next_prime
def attack(e, d, c):
kphi = e * d - 1 # φ(n)的某个倍数
# 尝试可能的k值
for k in range(2**15, 2**16): # 根据位数估计k的范围
if kphi % k != 0:
continue
phi_candidate = kphi // k
# 解方程 x² - (n - phi_candidate + 1)x + n = 0
# 其中x代表p+q
# 但由于我们没有n,需要其他方法
# 替代方法:假设p≈q≈√(phi_candidate)
p_guess = prev_prime(isqrt(phi_candidate))
q_guess = next_prime(p_guess)
if (p_guess-1)*(q_guess-1) == phi_candidate:
n_found = p_guess * q_guess
m = pow(c, d, n_found)
return long_to_bytes(m)
return b"Attack failed"
5. 安全编码实践与防御措施
理解了攻击原理后,我们需要学习如何安全地生成RSA密钥。以下是关键的安全准则:
绝对避免的做法:
- 使用连续质数作为p和q
- 基于一个质数通过简单变换得到另一个质数
- 使用有固定数学关系的质数对
推荐的安全实践:
- 独立生成大质数 :
from Crypto.Util.number import getPrime
def generate_secure_rsa_keys(bits=2048):
# 确保p和q有足够的间距
while True:
p = getPrime(bits)
q = getPrime(bits)
# 检查质数间距是否足够大
if abs(p - q) > 2**(bits//2 - 100):
break
n = p * q
e = 0x10001
phi = (p-1)*(q-1)
d = inverse(e, phi)
return (e, n), (d, n)
- 添加额外验证 :
def validate_rsa_primes(p, q, min_bit_diff=100):
"""确保两个质数有足够的安全间距"""
bit_diff = abs(p - q).bit_length()
return bit_diff >= min_bit_diff
- 使用标准库而非自行实现 :
from Crypto.PublicKey import RSA
# 最安全的做法是使用经过严格验证的库
key = RSA.generate(2048)
public_key = key.publickey()
5.1 安全参数对照表
| 参数 | 不安全做法 | 安全做法 |
|---|---|---|
| 质数生成 | 使用nextPrime(p) | 独立随机生成 |
| 质数间距 | 可能非常接近 | 至少相差2^(n/2-100) |
| 质数大小 | 可能不足 | 至少2048位 |
| 公钥指数e | 可能使用不安全的值 | 使用65537(0x10001) |
| 私钥存储 | 明文存储 | 使用密码保护或HSM |
6. CTF中的RSA题目解题思路扩展
在CTF竞赛中,RSA题目变种繁多。基于babyRSA的经验,我们可以总结一些常见解题模式:
-
已知部分私钥信息 :
- 使用Wiener攻击或Boneh-Durfee攻击(当d较小时)
- 使用中国剩余定理优化计算
-
不正确的密钥生成 :
- 费马分解(p和q接近)
- Pollard's p-1分解(p-1或q-1光滑)
- 共模攻击(多个密文使用相同的n)
-
侧信道信息 :
- 计时攻击
- 错误分析攻击
以下是一个通用的RSA解题框架:
def solve_rsa_challenge(n=None, e=None, d=None, c=None, hints=None):
if n is None and e is not None and d is not None:
# 可能可以通过ed分解n
kphi = e*d - 1
# ... 分解逻辑
elif hints == "close_primes":
p, q = fermat_factor(n)
phi = (p-1)*(q-1)
d = inverse(e, phi)
m = pow(c, d, n)
return long_to_bytes(m)
# ... 其他情况处理
7. 深入理解RSA安全性的关键因素
通过babyRSA案例,我们可以提炼出RSA安全性的几个核心要素:
必须保证的条件:
- 大质数p和q必须足够大(现代标准推荐至少2048位)
- p和q必须独立随机生成,无任何数学关系
- p-1和q-1都应该有大质因子
- |p-q|应该足够大(至少大于2^(n/2-100))
常见实现陷阱:
- 使用弱随机数生成器导致质数可预测
- 重用相同的质数对
- 错误处理私钥导致信息泄露
- 使用不安全的填充方案(如教科书式RSA)
以下代码展示了如何全面检查RSA参数的安全性:
def check_rsa_security(p, q, e=0x10001):
"""全面检查RSA参数安全性"""
n = p * q
phi = (p-1)*(q-1)
checks = {
"p_is_prime": isPrime(p),
"q_is_prime": isPrime(q),
"p_bits": p.bit_length() >= 1024,
"q_bits": q.bit_length() >= 1024,
"prime_gap": abs(p - q).bit_length() >= p.bit_length()//2 - 100,
"e_coprime_phi": gcd(e, phi) == 1,
"p_safe": (p-1)//2 is Prime, # p是安全质数
"q_safe": (q-1)//2 is Prime # q是安全质数
}
return checks
在实际项目中,除了这些基本检查外,还应考虑:
- 使用OAEP等安全的填充方案
- 防范时序攻击等侧信道攻击
- 定期更换密钥
- 使用HSM等安全硬件存储私钥
更多推荐
所有评论(0)