☰
RSA低解密指数攻击:维纳攻击原理与Python实现
2026/10/20 9:48:43 网站建设 项目流程

很多开发者和安全工程师第一次听到“RSA低解密指数攻击”时,都会觉得这是离自己很远的学术名词。但我在实际做密钥审计时,已经不止一次用维纳攻击(Wiener Attack)在一眨眼之间就把公钥背后的两个大素数还原出来。这个攻击非常纯粹:它只需要公开的(e, n),不需要任何密文或签名样本,一旦私钥指数d满足条件,d就藏在e/N的连分数展开里。这篇文章会把数学原理到代码实现完整串一遍,最后用一个可以手动验算的实例带大家复现整条攻击链路。无论你是做安全测试、写支付接口的后端,还是正在学RSA的密码学入门者,读完都能拿到一套可以直接运行的脚本,以及一个判断密钥是否“裸奔”的硬标准。

1. 先聊清楚:为什么私钥指数d的大小会决定生死

1.1 RSA参数背后的基本关系

RSA能工作,核心依赖一组简单关系:取两个大素数p和q,计算n = p·q,再算欧拉函数φ(N) = (p-1)(q-1)。随后选一个与φ(N)互素的公开指数e,用扩展欧几里得算法求出私钥指数d,使得:

e·d ≡ 1 (mod φ(N))

公钥是(n, e),私钥是(n, d)。如果攻击者不知道φ(N)或p、q,理论上就无法从公开的公钥反推出d。整个RSA的安全性,就建立在这个“模逆关系里的d不可求解”的前提之上。

但这里有一个极其容易被人忽略的细节:RSA标准只要求e·d ≡ 1 (mod φ(N)),并没有限制d的取值范围。标准实现里,e固定为65537,d是通过模逆运算随机得到的大数,和N几乎同长度——2048位RSA的d通常就有2048位。可这不是数学上的“必须”,你完全可以主动选择一个几十位的小d,反算出一个很大的e,这样构造出的密钥依然能正常加解密,只是安全性已经烂到根了。

1.2 d小到什么时候会出事

直觉上,d的范围越小,私钥信息就越容易被“扫”出来。但维纳攻击比“扫描”更恐怖,它不做穷举,而是用连分数直接把d从公开数据里解出来。数学上的充分条件是:

d < (1/3) · N^(1/4)

这个条件看起来抽象,代入具体尺寸就明白了。对1024位RSA,N^(1/4)约为256位,也就是d只要小于2^256/3这个量级,维纳攻击就保证成功。对512位RSA,N^(1/4)是128位,攻击范围更宽。

需要强调的是,这只是一个充分条件,不是精确边界。实际测试中,d稍微超过这个阈值时,连分数方法仍有相当高的命中概率。所以“疑似小d”的公钥,都值得跑一遍脚本试试。我最早接触这个攻击是在一次模拟应急响应里,当时拿到一张e异常巨大的证书,公钥的e通常都是65537,突然看到一个几千位的e,第一反应就是“这密钥有问题”,跑了脚本后0.01秒内p、q就出来了,队友都愣住了。

1.3 哪些场景最容易产生小d密钥

根据我的审计经验,有三类场景最容易出现小d:

第一类,嵌入式设备或低功耗硬件为了提升签名速度,把模幂运算做了“优化”,有人直接把d缩短到128位甚至64位。签名计算确实变快了,但安全性荡然无存。

第二类,教程里的“极简RSA实现”为了展示方便,在示例代码中写了固定的d,比如d = 3或d = 65537,有的开发者直接CV到生产代码,所有密钥的d都一样。

第三类,主动构造的后门密钥。某些系统签发证书时会用特殊参数,只要d被人为设定得很小,掌握方法的人就能快速分解n,拿到整个证书体系的根权限。这种操作在黑产和灰色产业里并不罕见。

2. 数学原理:连分数是怎么揪出藏起来的d

2.1 从一个同余式到一组近似分数

维纳攻击的起点是前面那个同余式。因为ed - kφ(N) = 1,两边同时除以d·φ(N),就得到:

e/φ(N) - k/d = 1 / (d·φ(N))

右边是一个非常小的正数。这告诉我们一件重要的事:k/d是e/φ(N)的一个极高精度的有理近似。再看φ(N)本身,φ(N) = n - (p+q) + 1,而p+q作为两个大素数的和,在比值上只是n的“零头”,所以φ(N)和n的差距非常小。综合考虑,k/d也就极其接近e/N。

这里的逻辑链是:

  • 不知道d,但我们手里有公开的e和n
  • 知道k/d应该非常接近e/N
  • 问题变成:如何从e/N的有理近似序列里,认出哪一个恰巧是k/d

2.2 俄罗斯套娃一样的连分数

“连分数”可以理解为分数的俄罗斯套娃。任意一个实数x都能展开成这种形式:

x = a0 + 1 / (a1 + 1 / (a2 + 1 / (a3 + ...)))

每次“截断”这个无穷嵌套,就得到一个渐近分数,记作p_i/q_i。例如e/N的连分数前几项是[0; 1, 3, 1, 1, 2, ...],对应的渐近分数依次是0/1、1/1、3/4、4/5、7/9、18/23……它们会一级比一级更贴近原始数值。

连分数理论里有一条关键定理:如果某个有理数a/b非常接近x,满足|x - a/b| < 1/(2b²),那么a/b必然出现在x的连分数渐近展开中。维纳攻击的完整证明,本质上就是证明k/d满足这个精度条件。证明过程需要用到d < N^(1/4)/3这个关键条件来控制误差,细节可以在密码学教材的附录里找到,这里不展开。

我一直用这样的方式来理解连分数:它就是一台“能给出所有最优有理逼近”的机器。你不需要预先知道k/d在哪一层,把机器完整跑一遍,把输出的每个渐近分数都当候选去验证即可。这也是为什么实战中即使d略超理论边界,攻击依然可能成功——因为机器本身给出了大量候选,命中一个就够了。

2.3 从候选分数到分解n的验证设计

假设我们枚举到了一个候选渐近分数,分子记作k',分母记作d',怎么确定它真的就是我们要找的k和d?

第一关,整除性检查。真正的φ(N)是整数,所以(ed - 1)必须能被k整除。对候选值算一下(e·d' - 1) % k',如果除不尽,直接丢掉。

第二关,构造二次方程。通过候选值算出φ'(N) = (e·d' - 1) / k',再利用p+q = n - φ(N) + 1,得到p和q的“和”,而p·q = n是已知的。所以p、q应该是这个方程的两个根:

x² - (n - φ'(N) + 1)x + n = 0

如果候选正确,这个方程的判别式Δ一定是完全平方数,开方后就能直接得到p、q两个整数。

第三关,终极验证。即使判别式是平方数,也要检查p·q是否真的等于n。三道关卡层层过滤,基本不会误报。这也是维纳攻击实现起来特别干净的原因:没有随机扰动,没有概率迭代,每一步都是确定性的数学验证。

3. 三十行Python代码实现维纳攻击

3.1 第一步:连分数展开

先从辗转相除法写连分数展开函数:

def cf_expand(num, den): cf = [] while den: a = num // den cf.append(a) num, den = den, num - a * den return cf

这个函数本质就是广义的欧几里得算法,每轮的商a就是连分数的一项。对37213607 / 47564513运行,返回的列表开头是[0, 1, 3, 1, 1, 2, ...],后面会用到这个结果。

3.2 第二步:生成渐近分数序列

有了连分数项,就可以递推生成渐近分数:

def convergents(cf): convs = [] p_minus2, p_minus1 = 0, 1 q_minus2, q_minus1 = 1, 0 for a in cf: p = a * p_minus1 + p_minus2 q = a * q_minus1 + q_minus2 convs.append((p, q)) p_minus2, p_minus1 = p_minus1, p q_minus2, q_minus1 = q_minus1, q return convs

这里的初始边界(p_minus2, q_minus2) = (0, 1)和(p_minus1, q_minus1) = (1, 0)是数学递推的两块基石,保证第一项也能套用后面的递推公式。这份代码看起来简单,但边界写错会直接导致渐近分数序列错位,攻击自然失败。我见过好几版网上流传的实现,边界处理是错的,需要特别留意。

3.3 第三步:候选验证与主攻击函数

把前面的函数组合起来,加入三重验证:

from math import isqrt def wiener_attack(e, n): cf = cf_expand(e, n) for idx, (k, d) in enumerate(convergents(cf)): if k == 0: continue if (e * d - 1) % k != 0: continue phi_guess = (e * d - 1) // k b = n - phi_guess + 1 delta = b * b - 4 * n if delta < 0: continue s = isqrt(delta) if s * s != delta: continue if (b - s) % 2 != 0: continue p = (b - s) // 2 q = (b + s) // 2 if p * q == n: return {"d": d, "k": k, "p": p, "q": q, "convergent_index": idx} return None

3.4 使用方式和性能分析

调用方式非常直接:

result = wiener_attack(e, n) if result: print(result) else: print("未命中,d可能足够大")

性能方面,连分数展开本身是辗转相除级别的复杂度,而每个渐近分数只做几次乘法和开方。实测中,2048位的n,脚本运行时间在毫秒量级;就算不满足攻击条件,把全部渐近分数过一遍也很轻松。所以这个脚本非常适合放进CI或者密钥合规巡检任务里,只要输入公钥参数就能自动排查。

这里特别提醒一点:判别式是否是平方数的判断,一定要用integer版本的isqrt,不要用float的sqrt。大整数用float开方会因为浮点精度丢失,误判平方数,导致攻击脚本漏报或误报。

4. 实战复现:用一组可控弱密钥走完全程

4.1 构造一个满足条件的RSA实例

为了让大家能亲手验证,我特意构造了一组位数不大、但严格满足维纳条件的RSA参数:

  • p = 6833
  • q = 6961
  • n = p·q = 47564513
  • φ(N) = (p-1)(q-1) = 47550720
  • 选择d = 23,计算得e = d⁻¹ mod φ(N) = 37213607

验证一下攻击条件:N^(1/4)约等于83.2,除以3约等于27.7,d = 23完全落在范围内。

注意到没有?这里的e高达三千多万。这是小d RSA的典型外观特征:d越小,e往往越大。如果你在安全评估里看到一把公钥的e明显大得离谱,第一个念头就应该是跑维纳攻击。

4.2 手工连分数展开与命中

现在对e/N = 37213607 / 47564513做连分数展开,过程如下:

  • a0 = 0,因为37213607 < 47564513
  • 取倒数,整数部分a1 = 1
  • 继续展开,得到a2 = 3,a3 = 1,a4 = 1,a5 = 2

连分数序列为[0; 1, 3, 1, 1, 2, ...]。对应的渐近分数逐步逼近原始值:

  • 0/1
  • 1/1
  • 3/4
  • 4/5
  • 7/9
  • 18/23

第6个渐近分数是18/23,到这里就命中了:k = 18,d = 23。

接下来做验证。先看整除性:

(e·d - 1) / k = (37213607 × 23 - 1) / 18 = 855912960 / 18 = 47550720

结果恰好等于φ(N)。下一步,p + q = n - φ(N) + 1 = 47564513 - 47550720 + 1 = 13794。解方程:

x² - 13794x + 47564513 = 0

判别式Δ = 13794² - 4×47564513 = 190274436 - 190258052 = 16384,开根号等于128。于是:

p = (13794 - 128) / 2 = 6833 q = (13794 + 128) / 2 = 6961

分解成功。

4.3 用脚本验证并扩展到真实尺寸

把参数丢进前面的wiener_attack函数:

e, n = 37213607, 47564513 print(wiener_attack(e, n)) # 输出: {'d': 23, 'k': 18, 'p': 6833, 'q': 6961, 'convergent_index': 5}

和手工推算完全一致。拿到p和q之后,整个RSA体系就彻底失守了。你可以重新计算私钥、解密任意密文、伪造任意签名,一切都不再受控制。整个过程连一次模幂都没做,只是对e/N做了一次辗转相除。

如果你想测试更接近真实环境的参数,比如512位或1024位的RSA,只需要用标准库生成一个d很小的密钥对,再把这个脚本喂进去,代码一行都不用改。Python的大整数运算天然支持任意长度,实际审计中直接对接证书解析出来的(e, n)即可。

5. 边界之外与工程防护的完整认知

5.1 别以为过了维纳边界就安全

维纳攻击的理论边界是d < N^0.25/3,但密码学家的攻击没有止步于此。Boneh和Durfee在1999年把边界扩展到了d < N^0.292,用的工具是格子上的Coppersmith方法。这意味着即使d比维纳边界大不少,只要仍然明显小于N的0.292次方,攻击者依然可能通过格基约减恢复私钥指数。

这类格攻击的实现依赖sage数学环境中的LLL算法,代码量比维纳攻击大得多,但它告诉我们一个更重要的原则:私钥指数d的安全边界,不是“大于N的四分之一次方”,而是“和N基本同量级”。任何人为把d做小的尝试,都是在玩火。

5.2 一套可以落地的密钥自检与防护清单

把防护经验整理成一张表,方便直接对照执行:

防护措施具体做法说明
标准密钥生成使用系统库或OpenSSL,固定e=65537d由模逆自然得到,天然和N同量级
审计e的形态发现e异常巨大时,立即跑维纳攻击脚本e过大往往是d过小的信号
巡检进CI把wiener_attack封装成定时任务,扫描线上公钥速度快,毫秒级完成,适合全量检测
处理历史密钥对扫描命中的密钥一律下线重签不要尝试修补,直接替换最稳妥
控制密钥长度当前生产环境至少2048位,推荐4096位长度增加后,攻击条件指数级变宽
禁用自研RSA不在业务代码里手写RSA生成逻辑专业库已经规避了已知攻击面

这些不是纸上谈兵。我在做合规审计时,把维纳攻击脚本固化成了巡检工具,遇到过好几次线上密钥因为历史原因采用了短d,全部在1秒内识别出来并推动下线。其中最夸张的一个案例,某系统为了“性能优化”把d压缩到了128位,证书仍然通过正常签发流程发放,要不是这次巡检,这把备钥可能到现在还在生产环境里裸奔。

最后分享一个小小的实操体会:无论你多信任自己的密钥生成流程,都建议把维纳攻击脚本保留在工具箱里。它占不了多少代码量,却能在关键时候帮你发现最容易被忽略的“低级失误”。安全这件事,往往不是被多高深的技术攻破的,而是被最基础的设计死角击穿的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询