RSA安全实战:7种脆弱场景的Python攻击与防御指南
1. 项目概述:当RSA不再是“绝对安全”
在密码学的世界里,RSA算法长久以来都被视为非对称加密的基石,是构建现代互联网信任体系的关键组件。从HTTPS的握手到SSH的登录,再到数字签名,它的身影无处不在。很多开发者,甚至是一些安全工程师,都习惯于将其视为一个“黑盒”——生成一对密钥,公钥加密,私钥解密,只要密钥足够长(比如2048位),似乎就高枕无忧了。然而,在实际的渗透测试、CTF竞赛或是遗留系统的安全审计中,我们常常会遇到一些“非标准”的RSA实现。这些实现可能因为追求性能、兼容老协议,或是开发者对密码学理解不深,而引入了各种脆弱性。这时,RSA的“坚不可摧”就变成了一个危险的错觉。
这个项目,正是要撕开这个错觉。它不是一个教你如何“黑掉”正常RSA的指南,而是一份针对脆弱RSA实现场景的实战手册。核心目标很明确:当你手头只有一些零散的信息——可能是一个PEM格式的公钥文件、几条截获的密文、或者一些关于加密过程的边信道信息——如何利用Python这把瑞士军刀,抽丝剥茧,还原出明文或私钥。标题中的“7种场景”涵盖了从最基础的信息泄露(公钥提取参数)到需要一定数学技巧的攻击(如维纳攻击),形成了一个由浅入深的完整链条。对于安全研究人员、CTF选手以及对密码学实战感兴趣的程序员来说,掌握这些场景的识别与利用,意味着你能在看似铜墙铁壁的加密数据面前,找到那条隐藏的裂缝。
2. 核心攻击场景全景解析
RSA的安全性建立在“大数分解难题”和“模幂运算的逆运算困难”之上。但当算法在实现和应用过程中偏离了理想假设,攻击面便随之产生。下面我们系统性地拆解这七种经典攻击场景的内在逻辑与前置条件。
2.1 场景一:公钥信息提取与基础参数解析
这是所有攻击的起点,也是最常被忽略的一步。很多时候,我们拿到的不是一个直接的(n, e)对,而是一个PEM文件、一段Base64编码的字符串,或者内嵌在代码、配置文件中的密钥片段。
为什么这是突破口?RSA的公钥本质上就是两个数字:模数n(一个大素数的乘积)和公钥指数e。任何攻击都需要以这两个参数为基础。提取过程就是将这些各种格式的“外壳”剥掉,拿到核心的数学参数。
实操要点与工具选择:在Python中,我们有多种武器库。对于标准的PEM格式公钥,Crypto.PublicKey.RSA.import_key()是最直接的方法。但实战中更常见的是各种“变体”。例如,从SSH的authorized_keys文件中提取,其格式是ssh-rsa AAAAB3NzaC1yc2E...,这其实是经过特定编码的。这时,我们可以使用pycryptodome库,或者更底层的asn1crypto库来解析其中的ASN.1结构。
from Crypto.PublicKey import RSA # 场景1: 从PEM文件读取 with open('public.pem', 'r') as f: pem_data = f.read() pub_key = RSA.import_key(pem_data) n, e = pub_key.n, pub_key.e print(f"n = {n}\ne = {e}") # 场景2: 从Base64字符串解析(常见于网络传输或配置) import base64 from Crypto.Util.asn1 import DerSequence b64_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzX7p1wM9Q3J7..." der = base64.b64decode(b64_key) # 尝试解析PKCS#1格式 seq = DerSequence() seq.decode(der) if len(seq) == 2: # PKCS#1 格式: (n, e) n, e = seq[0], seq[1]注意:不是所有Base64字符串都是直接的DER编码。它可能是PEM去掉了头尾(
-----BEGIN PUBLIC KEY-----),也可能是其他格式(如OpenSSH格式)。解析失败时,需要结合上下文判断格式,并可能需要进行手动解析。
2.2 场景二:模数分解攻击(当n过小或存在缺陷)
这是最“正统”的攻击思路:既然RSA的安全性依赖于n=p*q难以分解,那么如果n不够大,或者p和q非常接近,或者存在其他数学缺陷,分解就变得可行。
核心原理与工具实战:对于较小的n(如小于512位),我们可以直接使用本地工具如yafu或在线数据库(如factordb.com)。在Python中,sympy库的factorint函数可以处理小整数的分解。但对于更大的n,我们需要更高效的算法。
import sympy from math import isqrt n = 1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139 # 方法1: 使用sympy(适用于较小n) factors = sympy.factorint(n) # 例如 {37975227936943673922808872755445627854565536638199: 1, 40094690950920881030683735292761468389214899724061: 1} p, q = list(factors.keys()) # 方法2: 费马分解法(当p和q接近时高效) def fermat_factorization(n): a = isqrt(n) + 1 b2 = a*a - n while not isqrt(b2)**2 == b2: a += 1 b2 = a*a - n b = isqrt(b2) return a - b, a + b p, q = fermat_factorization(n)为什么费马分解有效?如果p和q接近,那么它们的平均数(p+q)/2与sqrt(n)很接近。设a = (p+q)/2,b = (p-q)/2,则有n = a^2 - b^2。费马分解就是从a = ceil(sqrt(n))开始,逐步增加a,检查a^2 - n是否为完全平方数b^2。一旦找到,p = a+b,q = a-b。
2.3 场景三:共模攻击(Common Modulus Attack)
想象一个糟糕的场景:一个系统用同一对p, q生成了多个密钥对,即多个用户共享同一个模数n,但拥有不同的公钥指数e1, e2和对应的私钥d1, d2。这听起来很荒谬,但在一些老旧或设计不当的系统中确实存在。
攻击的数学基础:假设同一消息m用两个公钥(n, e1)和(n, e2)加密,得到密文c1 ≡ m^e1 mod n和c2 ≡ m^e2 mod n。如果e1和e2互素(通常都是素数,所以大概率互素),根据贝祖定理,存在整数s1, s2使得e1*s1 + e2*s2 = 1。那么,我们可以计算:(c1^s1 * c2^s2) mod n ≡ m^(e1*s1) * m^(e2*s2) mod n ≡ m^(e1*s1 + e2*s2) mod n ≡ m^1 mod n = m注意,s1或s2可能是负数。如果是负数,我们需要先计算模逆元。例如,若s1为负,则计算c1_inv = modinv(c1, n),然后计算(c1_inv^(-s1) * c2^s2) mod n。
Python实现:
from Crypto.Util.number import long_to_bytes, bytes_to_long import gmpy2 def common_modulus_attack(c1, c2, e1, e2, n): # 使用扩展欧几里得算法求系数s1, s2 gcd, s1, s2 = gmpy2.gcdext(e1, e2) if gcd != 1: raise ValueError("e1 and e2 必须互素") # 处理负数指数 if s1 < 0: c1 = gmpy2.invert(c1, n) s1 = -s1 if s2 < 0: c2 = gmpy2.invert(c2, n) s2 = -s2 # 计算 m = c1^s1 * c2^s2 mod n m = (pow(c1, s1, n) * pow(c2, s2, n)) % n return long_to_bytes(m) # 示例 n = 0x共同的模数... e1, c1 = 0x10001, 0x密文1... e2, c2 = 0x10003, 0x密文2... message = common_modulus_attack(c1, c2, e1, e2, n) print(f"还原的消息: {message}")2.4 场景四:低加密指数攻击(如e=3)
为了提升加密效率,一些系统会使用非常小的公钥指数e,比如3。当e很小,并且明文m也很小(满足m^e < n)时,加密过程c = m^e mod n实际上退化为c = m^e(因为m^e还没有模数n大,取模无效果)。此时,直接对密文c开e次方根即可得到明文。
攻击条件与扩展:
- 明文填充不当:如果明文
m没有经过随机填充,直接加密,且m较小,则攻击成立。 - 广播攻击(Hastad广播攻击):如果同一消息
m用相同的低指数e(如3)加密,但发送给e个不同的接收者(使用不同的模数n1, n2, n3),那么我们可以利用中国剩余定理(CRT)构造一个关于m^e的同余方程组,最终直接求解m^e的整数解,再开方得到m。这比单纯e=3且m^3<n的条件更常见。
低加密指数广播攻击Python示例:
import gmpy2 from functools import reduce def crt(remainders, moduli): """中国剩余定理实现""" total = 0 prod = reduce(lambda a, b: a*b, moduli) for r_i, n_i in zip(remainders, moduli): p = prod // n_i total += r_i * gmpy2.invert(p, n_i) * p return total % prod # 假设同一消息m用e=3加密,生成3组密文和模数 e = 3 ciphers = [c1, c2, c3] # 三个密文 moduli = [n1, n2, n3] # 三个不同的模数 # 利用CRT求解 m^3 mod (n1*n2*n3) m_cubed = crt(ciphers, moduli) # 由于 m^3 < n1*n2*n3 (通常情况),所以 m_cubed 就是精确的 m^3 m, exact = gmpy2.iroot(m_cubed, e) # 开三次方根 if exact: print(f"还原的消息: {long_to_bytes(int(m))}")2.5 场景五:维纳攻击(Wiener‘s Attack)
这是本项目的一个重点,也是一种非常精妙且在实际中可能遇到的攻击。它针对的是私钥指数d过小的情况。为了提高解密或签名速度,开发者可能会刻意选择一个较小的d。维纳攻击告诉我们,如果d < (1/3) * n^(1/4),那么攻击者可以仅从公钥(n, e)中高效地恢复出私钥d。
背后的数学原理(简述):RSA的密钥生成满足e*d ≡ 1 mod φ(n),其中φ(n) = (p-1)*(q-1)。这意味着存在一个整数k,使得e*d = k*φ(n) + 1。将等式两边同时除以d*φ(n),得到e/φ(n) - k/d = 1/(d*φ(n))。由于φ(n)非常接近n(因为p和q都很大),所以e/n是k/d的一个非常好的近似。数论中的连分数理论告诉我们,一个有理数的“好”的近似值,会出现在其连分数展开的收敛子中。因此,我们可以计算e/n的连分数展开,并依次尝试每一个收敛子k’/d’,检查d’是否就是正确的私钥d。
Python实现维纳攻击:
import gmpy2 from Crypto.Util.number import long_to_bytes def wiener_attack(e, n): """维纳攻击实现""" # 计算 e/n 的连分数展开 def continued_fractions(e, n): cf = [] while n: q = e // n cf.append(q) e, n = n, e - q * n return cf # 根据连分数展开计算收敛子 def convergents(cf): convergents = [] for i in range(len(cf)): num, den = 1, 0 for j in range(i, -1, -1): num, den = cf[j] * num + den, num convergents.append((num, den)) return convergents cf = continued_fractions(e, n) convergents_list = convergents(cf) for k, d in convergents_list: if k == 0: continue # 检查条件:ed ≡ 1 mod φ(n) 的近似 # 根据 ed - 1 = kφ(n),可以推导出 φ(n) = (ed-1)/k # 然后通过 φ(n) 求解 p, q if (e * d - 1) % k != 0: continue phi = (e * d - 1) // k # 根据 φ(n) = (p-1)(q-1) = n - (p+q) + 1,建立一元二次方程 # sum_pq = n - phi + 1 # product_pq = n # 方程: x^2 - sum_pq*x + n = 0 sum_pq = n - phi + 1 discriminant = sum_pq * sum_pq - 4 * n if discriminant < 0: continue sqrt_disc, exact = gmpy2.iroot(discriminant, 2) if not exact: continue p = (sum_pq + sqrt_disc) // 2 q = (sum_pq - sqrt_disc) // 2 if p * q == n: return d, p, q return None # 使用示例 e = 67994682464925517847561377525734815980381 n = 9516311845790656153499716760847001433441357 result = wiener_attack(e, n) if result: d, p, q = result print(f"攻击成功!私钥d: {d}") print(f"分解结果: p={p}, q={q}")实操心得:维纳攻击的实现对精度要求很高,必须使用大整数运算库(如
gmpy2)。在CTF中,题目往往会给出一个明显很大的e(因为d小,根据e*d ≡ 1 mod φ(n),e就会很大),这是一个强烈的提示信号。另外,攻击成功后不仅能得到d,还能直接分解n。
2.6 场景六:选择密文攻击与填充预言(Padding Oracle)
这种攻击针对的是实现层面的漏洞,而非数学难题。在许多使用RSA进行传输加密的场景中,会先对明文进行填充(如PKCS#1 v1.5),然后再加密。如果服务器在解密后,对于填充错误的密文和填充正确但内容错误的密文返回不同的错误信息(例如,一个返回“解密错误”,一个返回“格式错误”),那么它就泄露了一个“预言机”(Oracle)。攻击者可以利用这个侧信道信息,通过不断发送精心构造的密文并观察服务器响应,逐步推算出原始明文。
基本思想(Bleichenbacher攻击):假设我们有密文c,其对应的明文m符合PKCS#1 v1.5填充格式。攻击者可以不断构造新的密文c' = (s^e * c) mod n发送给服务器。因为Decrypt(c') = (c')^d = (s^e * c)^d = s * m mod n。通过观察服务器对Decrypt(c')的填充验证结果(成功或失败),攻击者可以逐步缩小m可能的数值区间,最终确定m。这个过程完全自动化,并且对于2048位的RSA,也只需要数十万次查询,在现代计算机上是可以完成的。
工具与实现:手动实现完整的Bleichenbacher攻击较为复杂,通常会使用像python-paddingoracle这样的库,或者CTF中的专用脚本。其核心是一个二分搜索的变种,利用Oracle的反馈来调整搜索空间。
2.7 场景七:侧信道与故障攻击(概念延伸)
这属于更高级的物理攻击范畴,但在一些特定场景(如分析智能卡、硬件安全模块)的题目中可能出现。侧信道攻击不直接攻击算法,而是通过分析设备执行加解密操作时的功耗、电磁辐射、时间差异等信息来推断密钥。故障攻击则是在计算过程中(例如签名时)故意引入故障(如电压毛刺、时钟抖动),导致设备输出错误的签名,利用这个错误签名和正确签名的关系来恢复私钥。
在CTF中的常见形式:可能会给你一个“有缺陷”的签名实现,或者提供多组在轻微扰动下产生的签名/密文,要求你恢复密钥。这类题目通常需要更深入的数学知识(如针对RSA-CRT的故障攻击分析)。
3. 实战演练:从一道CTF题看复合攻击
让我们结合热搜词中提到的那个具体场景来一次综合实战:“某个老旧的认证网关会把用户登录时生成的令牌写入审计日志。为了兼容两套接口,同一个用户的令牌会以两种格式出现:一种带有固定开头,另一种带有固定结尾。管理员只找到了两条被同一把rsa公钥加密后的日志,以及这两种格式的固定部分。请还原日志里真正变化的那段内容。”
1. 问题拆解:
- 已知:同一把RSA公钥
(n, e),两条密文c1, c2。 - 已知:明文有两种固定格式。设变化的令牌部分为
m,固定开头为prefix,固定结尾为suffix。 - 那么,两条明文可能是:
m1 = prefix + m(格式A:固定开头)m2 = m + suffix(格式B:固定结尾) - 目标:求
m。
2. 攻击思路分析:这既不是共模攻击(模数相同,但这里是同一公钥加密了不同明文),也不是低指数攻击。我们注意到,明文m1和m2共享了中间部分m,且prefix和suffix已知。这构成了一个已知部分明文的攻击场景。我们可以利用Coppersmith相关消息攻击或其变种。
更直接的方法是利用代数关系。将明文视为大整数。则有:c1 ≡ (P + M)^e mod n,其中P = bytes_to_long(prefix)c2 ≡ (M + S)^e mod n,其中S = bytes_to_long(suffix),M = bytes_to_long(m)
我们有两个多项式在模n下:f1(x) = (P + x)^e - c1f2(x) = (x + S)^e - c2我们寻找它们的公共根x = M mod n。由于e通常很小(如65537),我们可以计算这两个多项式的最大公因式(GCD)。如果M的长度(位数)小于n的位数,那么在整数域(而非模n域)上计算GCD,很可能就直接得到(x - M)这个因子。
3. Python实现:
from Crypto.Util.number import bytes_to_long, long_to_bytes import gmpy2 # 题目给出的数据 (示例值,需替换为实际值) n = 0xabcdef... # 模数 e = 65537 c1 = 0x密文1... c2 = 0x密文2... prefix = b"user=" # 示例固定开头 suffix = b"&role=guest" # 示例固定结尾 P = bytes_to_long(prefix) S = bytes_to_long(suffix) # 在整数环上定义多项式,使用sympy import sympy x = sympy.symbols('x') poly1 = sympy.Pow(P + x, e) - c1 poly2 = sympy.Pow(x + S, e) - c2 # 计算两个多项式的GCD gcd_poly = sympy.gcd(poly1, poly2) # GCD结果应该是一个线性多项式,如 (x - M) roots = sympy.solve(gcd_poly, x) for root in roots: if root.is_integer and root > 0: M = int(root) print(f"找到可能的令牌M: {long_to_bytes(M)}") break注意事项:这种方法在
e较大时,多项式展开会非常庞大,计算GCD可能内存不足。此时可以使用基于结式(Resultant)或使用Zmod(n)环上更高效的Coppersmith方法(通过SageMath实现更简单)。但在CTF中,如果e是常规的65537,且prefix和suffix不长,上述方法在本地往往可行。
4. 工具链搭建与调试技巧
工欲善其事,必先利其器。一个高效的RSA破解环境能让你事半功倍。
1. 核心Python库:
pycryptodome/Crypto:最常用的密码学库,用于标准的密钥导入、加密解密操作。注意,pycryptodome是pycrypto的维护分支。gmpy2:绝对必备。提供高性能的大整数(mpz)运算,支持开方、模逆、素数检测、GCD等,速度远超Python原生整数。维纳攻击、分解等操作依赖它。sympy:符号计算库,用于多项式运算、小整数分解、解方程等,在已知部分明文攻击中很有用。libnum:一个CTF密码学工具库,封装了很多常用函数,如bytes_to_long,long_to_bytes,gcd,invmod等,非常方便。
安装命令:
pip install pycryptodome gmpy2 sympy # libnum 有时需要从源码安装或使用 pip install libnum2. 分解工具:
- 本地:
yafu(功能强大的整数分解工具,支持多种算法)。 - 在线:
factordb.com(查询已知分解的数据库)。 - 网站:
alpertron.com.ar/ECM.HTM(提供基于ECM等算法的在线分解器,对于中等大小的数很有效)。
3. 调试与思考流程:
- 信息收集:拿到题目,首先提取所有数字。可能是十六进制、十进制、Base64。统一转为十进制大整数备用。
- 参数检查:
- 看
n的大小:尝试用factordb或yafu分解。 - 看
e的大小:如果e=3或e很小,考虑低加密指数攻击或广播攻击。 - 看
e是否巨大:如果e和n差不多大,提示d可能很小,考虑维纳攻击。 - 是否有多个
n或e:判断是共模、广播还是共享素数。
- 看
- 关系分析:分析密文与明文之间、多个密文之间、多个密钥之间是否存在数学关系(如线性关系、共用参数)。
- 选择工具:根据以上分析,选择对应的攻击脚本或编写代码。
- 结果验证:解密或恢复出的明文,是否是可读的字符串(如包含
flag{、CTF等),或者是否能进一步用于解密其他数据。
常见问题排查:
gmpy2安装失败:在Windows上,可能需要从Unofficial Windows Binaries for Python Extension Packages网站下载对应版本的.whl文件进行安装。- 脚本运行内存/时间爆炸:检查算法复杂度。例如,直接对
n进行暴力分解是不可行的。维纳攻击的连分数展开收敛很快,如果长时间没结果,可能不是维纳攻击的场景。 - 解密出来是乱码:可能是编码问题。尝试
long_to_bytes()的结果用utf-8、ascii、latin-1等多种编码解码,或者直接hex()查看是否为flag格式。
5. 防御视角:如何避免自己的RSA被破解
作为开发者,了解攻击手段是为了更好地防御。从这些攻击场景中,我们可以总结出以下安全实践:
- 使用足够长的密钥:目前推荐至少2048位,重要系统考虑3072或4096位。
- 使用标准的公钥指数:就使用
e=65537。它不大不小,既避免了低指数攻击,计算效率也高,且二进制表示中1的位数少,平方乘算法效率高。 - 使用安全的随机数生成器生成
p和q:确保p和q长度相同、差异很大,并且p-1和q-1都有大素因子,以抵抗特定的分解算法。 - 绝不重复使用模数
n:每个密钥对应独立的p和q。 - 务必使用标准的填充方案:如加密用OAEP,签名用PSS。绝对不要使用“教科书式RSA”(即无填充)。
- 私钥指数
d不能小:虽然能加速解密,但会引入维纳攻击的风险。密钥生成时应确保d的位数大约为n位数的一半。 - 实现上要恒定时间、无侧信道:确保加解密操作的时间不依赖于密钥或明文,避免提供Padding Oracle。
最后,记住密码学的一个基本原则:不要自己发明密码系统。使用经过广泛审查的成熟库,如Python的cryptography库,并严格按照其文档和最佳实践来使用。这些攻击之所以存在,往往是因为人们在实现或应用标准算法时,无意或有意地偏离了安全轨道。这份指南,正是为了帮助你识别并理解那些危险的偏离。
