第五章:RSA非对称加密
5.1 RSA算法原理
RSA是最经典的非对称加密算法,其安全性基于大整数分解的困难性。
密钥生成过程:
- 随机选取两个大素数 p 和 q
- 计算 n = p × q(模数)
- 计算 φ(n) = (p-1)(q-1)(欧拉函数值)
- 选取公钥指数 e,满足 1 < e < φ(n) 且 GCD(e, φ(n)) = 1(通常取65537)
- 计算私钥指数 d = e⁻¹ mod φ(n)(e的模逆元)
公钥: (n, e) 私钥: (n, d)
加密: c = me mod n
解密: m = cd mod n
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
# RSA密钥生成与加解密示例
p = getPrime(512) # 512位素数
q = getPrime(512)
n = p * q
phi = (p - 1) * (q - 1)
e = 65537
d = pow(e, -1, phi) # Python 3.8+ 支持
print(f"p = {p}")
print(f"q = {q}")
print(f"n = {n}")
print(f"e = {e}")
print(f"d = {d}")
# 加密
message = b"flag{rsa_basic_example}"
m = bytes_to_long(message)
c = pow(m, e, n)
print(f"\n密文 c = {c}")
# 解密
m_decrypted = pow(c, d, n)
plaintext = long_to_bytes(m_decrypted)
print(f"解密结果: {plaintext}")
5.2 RSA常见攻击方法
5.2.1 已知p, q直接解密
最基础的情况:题目直接给出n, e, c以及p和q(或通过其他方式可以获得p和q)。
from Crypto.Util.number import long_to_bytes
# 已知参数
p = 473398607161
q = 4511491
e = 17
n = p * q
c = 2135733555619 # 密文
# 计算私钥d
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
# 解密
m = pow(c, d, n)
plaintext = long_to_bytes(m)
print(f"明文: {plaintext}")
5.2.2 小公钥指数攻击(Low Public Exponent Attack)
适用条件: e 很小(如e=3),且明文m也较小,使得 m^e < n。
原理: 此时 c = m^e mod n 实际上等于 c = m^e(没有发生模运算),直接对c开e次方根即可。
import gmpy2
from Crypto.Util.number import long_to_bytes
# 题目给出的参数
n = 0x00b0bee5e3e9e5a7e8d00b493355c618fc8c7d7d03b82e409951c182f398dee3104580e7ba70d383ae5311475656e8a964d380cb157f48c951adfa65db0b122ca40e42fa709189b719a4f0d746e2f6069baf11cebd650f14b93c977352fd13b1eea6d6e1da775502abff89d3a8b3615fd0db49b88a976bc20568489284e181f6f11e270891c8ef80017bad238e363039a458470f1749101bc29949d3a4f4038d463938851579c7525a69a8a3ec7e066637687f5126d32ffcfb6e0fef02c986c0c85f2ae0ee1eaec6735b4db6ce7a7db040a0886b12fb4af7bac7ebcf1d35a4f63c413f2aab82d0c94915e9a7fd13bc62bb1da3e35aebf3a4a3b1b138ac9876b3fdd3e3f3
e = 3
c = 0x32c7cae5c6cf22e5ee1a7a3d9e93bc9c4ce6d942deb5b8bbff771e9bd32e3c3e6ab1d84d3f28bae9726e6d28bb3e2c26
# 直接对c开立方根
m, exact = gmpy2.iroot(c, e)
if exact:
print(f"攻击成功!")
print(f"明文: {long_to_bytes(int(m))}")
else:
# 如果不精确,可能需要加上k*n再开方
for k in range(10000):
m, exact = gmpy2.iroot(c + k * n, e)
if exact:
print(f"k={k}, 明文: {long_to_bytes(int(m))}")
break
5.2.3 共模攻击(Common Modulus Attack)
适用场景: 同一明文m使用相同的n但不同的公钥e1、e2加密,产生密文c1、c2。
条件: GCD(e1, e2) = 1
原理: 由扩展欧几里得算法找到s1, s2使得 e1×s1 + e2×s2 = 1,则:
m = c1s1 × c2s2 mod n
from Crypto.Util.number import long_to_bytes
from math import gcd
def common_modulus_attack(n, e1, e2, c1, c2):
"""共模攻击"""
assert gcd(e1, e2) == 1, "e1和e2必须互素"
# 扩展欧几里得算法
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
g, x, y = extended_gcd(b % a, a)
return g, y - (b // a) * x, x
_, s1, s2 = extended_gcd(e1, e2)
# 处理负指数:c^(-s) = (c^(-1))^s mod n
if s1 < 0:
c1 = pow(c1, -1, n)
s1 = -s1
if s2 < 0:
c2 = pow(c2, -1, n)
s2 = -s2
# 计算明文
m = (pow(c1, s1, n) * pow(c2, s2, n)) % n
return m
# 示例
n = 23562080109018038742778820483568982347827540520135171070210
e1 = 17
e2 = 65537
c1 = 10827832305829964204060891824808894890388408390698982394856
c2 = 15551873521651498272047612592927934446456498781204104392073
m = common_modulus_attack(n, e1, e2, c1, c2)
print(f"明文: {long_to_bytes(m)}")
5.2.4 Wiener攻击(d较小时)
适用条件: 私钥d < n^(1/4) / 3
原理: 当d较小时,e/n的连分数展开中的某个收敛子(convergent)的分母就是d。
def wiener_attack(e, n):
"""Wiener攻击:利用连分数逼近恢复私钥d"""
from Crypto.Util.number import long_to_bytes
def continued_fraction(num, den):
"""计算连分数展开"""
cf = []
while den:
q = num // den
cf.append(q)
num, den = den, num - q * den
return cf
def convergents(cf):
"""计算连分数的所有收敛子"""
convs = []
h_prev, h_curr = 0, 1
k_prev, k_curr = 1, 0
for a in cf:
h_prev, h_curr = h_curr, a * h_curr + h_prev
k_prev, k_curr = k_curr, a * k_curr + k_prev
convs.append((h_curr, k_curr))
return convs
cf = continued_fraction(e, n)
for k, d in convergents(cf):
if k == 0:
continue
# 检验:如果(e*d - 1)能被k整除
if (e * d - 1) % k != 0:
continue
phi = (e * d - 1) // k
# phi(n) = n - p - q + 1,所以 p + q = n - phi + 1
s = n - phi + 1
# p和q是 x^2 - s*x + n = 0 的根
discriminant = s * s - 4 * n
if discriminant < 0:
continue
from gmpy2 import isqrt, is_square
if is_square(discriminant):
sqrt_d = isqrt(discriminant)
p = (s + sqrt_d) // 2
q = (s - sqrt_d) // 2
if p * q == n:
return d, p, q
return None
# 使用示例
# result = wiener_attack(e, n)
# if result:
# d, p, q = result
# m = pow(c, d, n)
# print(long_to_bytes(m))
5.2.5 Fermat分解法(p和q接近时)
原理: 当p和q非常接近时(|p-q|较小),n可以用Fermat方法快速分解。
令 a = ⌈√n⌉,计算 b² = a² - n,如果b²是完全平方数,则 p = a + b, q = a - b。
import gmpy2
def fermat_factor(n):
"""Fermat分解法:适用于p和q接近的情况"""
a = gmpy2.isqrt(n)
if a * a == n:
return int(a), int(a)
a += 1 # 从ceil(sqrt(n))开始
while True:
b_squared = a * a - n
if gmpy2.is_square(b_squared):
b = gmpy2.isqrt(b_squared)
p = int(a + b)
q = int(a - b)
return p, q
a += 1
# 安全限制:避免无限循环
if a - gmpy2.isqrt(n) > 1000000:
return None, None
# 示例
n = 1000000007 * 1000000009 # p和q非常接近
p, q = fermat_factor(n)
print(f"p = {p}")
print(f"q = {q}")
print(f"验证: p*q == n? {p*q == n}")
5.2.6 Pollard p-1分解法
适用条件: p-1是光滑数(smooth number),即p-1的所有素因子都不超过某个较小的界B。
算法原理: 如果p-1的所有素因子的幂次都不超过B!,那么 (p-1) | B!。由费马小定理,a^(B!) ≡ 1 (mod p),于是 GCD(a^(B!) - 1, n) 可能给出p。
from math import gcd, log
def pollard_p1(n, B=100000):
"""Pollard p-1分解法"""
a = 2
for j in range(2, B + 1):
a = pow(a, j, n)
d = gcd(a - 1, n)
if 1 < d < n:
return d, n // d
return None, None
# 示例:p-1是光滑数的情况
# p, q = pollard_p1(n)
# print(f"分解结果: p={p}, q={q}")
5.2.7 Håstad广播攻击
适用场景: 相同的明文m使用相同的小e(如e=3)但不同的模数n1, n2, ..., ne加密后发送给多人。
原理: 利用中国剩余定理(CRT),从e组密文中恢复m^e mod (n1×n2×...×ne),由于m^e < n1×n2×...×ne,直接开e次方根即得m。
import gmpy2
from Crypto.Util.number import long_to_bytes
from functools import reduce
def chinese_remainder_theorem(remainders, moduli):
"""中国剩余定理"""
N = reduce(lambda a, b: a * b, moduli)
result = 0
for r, n in zip(remainders, moduli):
Ni = N // n
yi = pow(Ni, -1, n)
result = (result + r * Ni * yi) % N
return result
def hastad_attack(ciphertexts, moduli, e):
"""Hastad广播攻击"""
# 使用CRT合并
combined = chinese_remainder_theorem(ciphertexts, moduli)
# 开e次方根
m, exact = gmpy2.iroot(combined, e)
if exact:
return int(m)
return None
# 示例:e=3,三组密文
# m = hastad_attack([c1, c2, c3], [n1, n2, n3], 3)
# print(long_to_bytes(m))
5.2.8 Coppersmith攻击简介
适用场景: 已知明文的部分高位或低位信息,利用格基约化(LLL算法)恢复完整明文。
典型 CTF 应用:
- 已知flag的前缀(如"flag{"),padding后加密
- RSA中消息的高位已知(Stereotyped Message Attack)
- RSA中p的部分比特已知(Partial Key Exposure)
SageMath 实现:
# SageMath代码(在SageMath环境中运行)
# 场景:已知明文高位,恢复低位
# n, e, c = ...(题目给出)
# known_high_bits = ...(已知的高位信息)
# P.<x> = PolynomialRing(Zmod(n))
# f = (known_high_bits + x)^e - c
# roots = f.small_roots(X=2^64, beta=1.0)
# if roots:
# m = known_high_bits + int(roots[0])
# print(long_to_bytes(m))
5.2.9 factordb.com查询
factordb.com是一个在线大整数分解数据库。许多CTF题目中的n可能已经被收录。
# pip install factordb-python
from factordb.factordb import FactorDB
def factordb_query(n):
"""在factordb中查询n的分解"""
f = FactorDB(n)
f.connect()
status = f.get_status() # "FF"表示完全分解
factors = f.get_factor_list()
print(f"状态: {status}")
print(f"因子: {factors}")
return factors
# 示例
# factors = factordb_query(n)
# if len(factors) == 2:
# p, q = factors
# # 继续RSA解密...
5.3 RSA综合实战案例
案例1:基础RSA解密
|----------------------------------------|
| 📋 题目 已知RSA参数如下,请解密密文c,恢复flag。 |
from Crypto.Util.number import long_to_bytes
# 题目给出的参数
p = 285960468890451637935629440372639283459
q = 304535446137785155622451010547307234543
e = 65537
c = 52584702924206682637115973187691622109377186446486767779190105859467455678109
# 第一步:计算n
n = p * q
print(f"n = {n}")
# 第二步:计算欧拉函数φ(n)
phi = (p - 1) * (q - 1)
print(f"φ(n) = {phi}")
# 第三步:计算私钥d
d = pow(e, -1, phi)
print(f"d = {d}")
# 第四步:解密
m = pow(c, d, n)
print(f"m = {m}")
# 第五步:转换为字节串
flag = long_to_bytes(m)
print(f"flag = {flag}")
案例2:小指数攻击实战
|------------------------------------------------------------|
| 📋 题目 n = (一个很大的数), e = 3, c = (一个相对较小的数)。提示:明文较短。 |
import gmpy2
from Crypto.Util.number import long_to_bytes
# 分析:e=3,明文短 → m^3 可能小于n → 直接开立方根
n = 0xBDAECA1B370A7D2DD7A6F4B14E2C14E53C034CBAB7CC3
e = 3
c = 0x6D94F7E0BE6FE7B0F3BF0A7A # 密文远小于n
# 尝试直接开立方根
m, exact = gmpy2.iroot(c, 3)
if exact:
print(f"直接开方成功!")
print(f"m = {m}")
print(f"flag = {long_to_bytes(int(m))}")
else:
print("直接开方不精确,尝试加k*n...")
for k in range(100000):
m, exact = gmpy2.iroot(c + k * n, 3)
if exact:
flag = long_to_bytes(int(m))
if b'flag' in flag or b'ctf' in flag:
print(f"k={k}, flag = {flag}")
break
案例3:共模攻击实战
|----------------------------------------------------------------------------------------------------------|
| 📋 题目 Alice将同一条消息用两个不同的公钥 (n, e1) 和 (n, e2) 加密后分别发送。你截获了两份密文c1和c2。已知:n, e1=17, e2=65537, c1, c2。 |
from Crypto.Util.number import long_to_bytes
from math import gcd
# 题目参数
n = 22708078815885011462462049064339185898712439277226831073457888403129
e1 = 17
e2 = 65537
c1 = 19131871765043780989007785498026791271873838245298258088153517748283
c2 = 20912698415719344905648896668429947622856831909672000736438709258961
# 确认e1, e2互素
assert gcd(e1, e2) == 1, "e1和e2必须互素"
# 扩展欧几里得算法
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
g, x, y = extended_gcd(b % a, a)
return g, y - (b // a) * x, x
_, s1, s2 = extended_gcd(e1, e2)
print(f"s1 = {s1}, s2 = {s2}")
print(f"验证: e1*s1 + e2*s2 = {e1*s1 + e2*s2}") # 应该等于1
# 处理负指数
if s1 < 0:
c1_use = pow(c1, -1, n) # 计算c1的模逆元
s1 = -s1
else:
c1_use = c1
if s2 < 0:
c2_use = pow(c2, -1, n)
s2 = -s2
else:
c2_use = c2
# 恢复明文: m = c1^s1 * c2^s2 mod n
m = (pow(c1_use, s1, n) * pow(c2_use, s2, n)) % n
flag = long_to_bytes(m)
print(f"恢复的明文: {flag}")