
聊到 RSA 加密算法攻击类型很多人第一反应是“RSA 是不是已经被攻破了”。做了几年安全评估和密码学相关开发之后我的真实感受是真正能在实战里复现出来的攻击绝大多数并不是因为大数分解这个数学问题被推翻而是因为参数选取出问题、实现有漏洞、协议设计不严谨。RSA 目前依然是使用最广泛的公钥密码算法之一但一套系统的安全性从来不取决于算法名字本身而取决于你怎么用它。这篇文章适合三类人看做渗透测试和代码审计的同行、刚入门密码学的学生、以及打算在自己系统里用 RSA 做加密或签名的开发者。我会把 RSA 攻击的常见类型按照数学层、协议层、实现层、量子威胁四条线拆开每一类都讲清楚原理、攻击条件、复现思路以及对应的防御手段。1. RSA攻击类型全景先搞清楚攻击者到底在打什么1.1 从密钥生成看RSA的数学根基与三个天然薄弱点要理解攻击类型先要把 RSA 的骨架画出来。密钥生成时选择两个大素数 p 和 q计算模数 n p * q然后计算欧拉函数 φ(n) (p-1)(q-1)。公钥指数 e 需要与 φ(n) 互素私钥指数 d 由 e*d ≡ 1 (mod φ(n)) 解得。加密时 c m^e mod n解密时 m c^d mod n。这套结构的安全根基只有一个在 n 足够大且 p、q 生成方式正确的前提下由 n 反推 p、q 在计算上是不可行的。因此 RSA 攻击领域的绝大多数数学攻击最终目标都是拿到 p 和 q或者绕过大数分解直接恢复私钥 d。从这个根基出发RSA 系统天然有三个薄弱点参数空间自由度太高。p、q 的生成方式、距离关系、素数性质e 的大小d 的长度填充方案的选择任何一处没做好都可能让本来安全的数学难题变成纸糊的墙。运算过程可以被外部观测。模幂运算的耗时、设备功耗、缓存访问行为都会泄露内部状态侧信道攻击正是从这里下手。解密方会对外界输入产生可观测的响应。密文是非法的还是合法的、填充是否正确这些布尔信息会被攻击者以“预言机”的形式反复套用。用人话说n 像是保险柜p 和 q 是锁芯的销钉。你把锁做得再坚固只要钥匙胚太简单、锁芯装配方式有问题、或者开锁时发出的声音有规律攻击者不砸锁也能把柜子打开。1.2 按攻击者能力拆解四类攻击路径安全评估里习惯按攻击者能力给攻击分类RSA 的攻击类型也可以这样画分线。攻击者能力典型攻击路径攻击目标仅持有公钥和密文小公钥指数攻击、共模攻击、模数分解攻击恢复明文或私钥可提交密文并观察解密行为填充预言攻击逐步恢复明文可物理接触设备侧信道攻击、错误注入攻击提取私钥或未知内部状态拥有未来量子计算能力量子多项式时间分解在多项式时间内分解大整数这个视角特别重要。因为很多开发者以为 RSA 攻击就是拿着大整数分解工具硬算越大的密钥越安全。但实际上如果攻击者拿不到足够大的算力他就会转向更软肋的地方你的填充没做对、你的解密错误信息太明确、你的幂模运算时间差异太明显。理解这一点才能理解为什么下面每一类攻击都值得逐条细看。2. 参数选取不当引发的数学攻击最容易复现也最容易被忽视2.1 小公钥指数攻击当密文比模数还小时小公钥指数攻击是最直观的 RSA 攻击类型之一。很多系统为了提升验签和加密速度会把 e 取成 3。如果明文 m 很小满足 m^3 n那么密文 c m^3 mod n 实际上就等于 m^3根本没有经过模运算的“包装”。攻击者只需要对 c 开三次方根就能拿回明文整个过程不需要任何密钥信息。更经典的是跨用户场景同一个明文 m 被发送给三个不同接收方三人的公钥指数都是 e3但模数分别是 n1、n2、n3。此时攻击者得到三个密文c1 m^3 mod n1c2 m^3 mod n2c3 m^3 mod n3。因为 m 小于每个模数所以 m^3 一定小于 n1n2n3于是可以用 CRT 把三个同余方程合并成一个整数 M再对 M 开三次方根即可。这一段数学关系还可以说得更明白CRT 之所以在这里有效是因为实际的真值 m^3 没有超过三个模数的乘积范围所以它在 CRT 合并后的结果就是真值而不是被某个单独模数“遮罩”后的值。换句话说小指数让明文的增长速度变慢了模数还没来得及起作用明文就已经暴露了。防御手段并不复杂e 直接固定取 65537这是实践中验证过的安全选择对明文做带随机数的填充让相同明文在不同时刻产生不同密文不要为了一点点性能去降低 e 的取值。我在实际评估某模拟项目 X 时就见过一个内部消息系统为了省 CPU 把 e 设成了 3结果几个固定格式的报文直接被开方解了出来。这类漏洞在代码审计里属于一眼就能发现的问题但因为它太基础反而容易被忽略。2.2 共模攻击共用模数等于把私钥交出去共模攻击的前提很具体两个不同用户共用同一个 n但分别使用不同的公钥指数 e1 和 e2而且同一个明文 m 分别被两个公钥加密。攻击者拿到两个密文 c1、c2以及公钥 (n, e1)、(n, e2)只要 e1 和 e2 互素就能恢复明文。攻击的数学核心是扩展 GCD 算法。因为 gcd(e1, e2) 1可以找到整数 s1、s2 使 e1s1 e2s2 1。随后计算m c1^s1 * c2^s2 mod n如果 s1 或 s2 是负数先计算对应密文在模 n 下的逆元再把指数取绝对值即可。这个式子能成立的原因很朴素c1^s1 * c2^s2 m^(e1s1 e2s2) m^1 m。实操里这个小脚本只需要几十行 Pythondef extended_gcd(a, b): if b 0: return a, 1, 0 g, x, y extended_gcd(b, a % b) return g, y, x - (a // b) * y def modinv(a, n): g, x, _ extended_gcd(a, n) if g ! 1: raise ValueError(no inverse) return x % n # c1 m^e1 mod n, c2 m^e2 mod n g, s1, s2 extended_gcd(e1, e2) if g ! 1: raise ValueError(e1 and e2 must be coprime) 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从防御角度说最核心的一条是同一套系统里每个用户必须使用独立生成的 n。多人共用模数是历史上真实出现过的错误做法常见于早期的一些应用场景中一旦两个公钥指数互素明文就是别人的了。另外随机填充同样能缓解共模攻击因为即使共用模数相同明文在不同随机填充下也会生成不同密文。2.3 低解密指数攻击私钥太小几乎等于裸奔私钥指数 d 的大小不能随意选。有人为了加速解密会把 d 选得很小这直接打开了一扇非常危险的门。低解密指数攻击的核心思路是用连分数逼近通过公钥 e 和模数 n 构造一个分数然后逐层逼近直到找出满足条件的 d 候选值。为什么连分数能逼近 d因为 ed ≡ 1 (mod φ(n))所以存在整数 k 使得 ed - kφ(n) 1。两边同时除以 dφ(n) 得到e/φ(n) - k/d 1/(d*φ(n))右边是一个非常小的数这说明 k/d 是对 e/φ(n) 的一个很好的逼近。实际计算中攻击者没有 φ(n)但可以用 e/n 代替因为 n 与 φ(n) 很接近这个逼近关系依然成立。通过展开 e/n 的连分数逐个尝试渐近分数如果能找到一个合法的 k、d 候选就可以反推出 φ(n)再由 pq n - φ(n) 1 解出 p 和 q。下面是这个思路的完整检测代码def wiener_attack(e, n): # 生成 e/n 的连分数 cf [] a, b e, n while b: cf.append(a // b) a, b b, a % b # 渐近分数 p_i/q_i 的递推 p_prev2, p_prev1 0, 1 q_prev2, q_prev1 1, 0 for ai in cf: p ai * p_prev1 p_prev2 # k 候选 q ai * q_prev1 q_prev2 # d 候选 if q and (e * q - 1) % p 0: phi_candidate (e * q - 1) // p s n - phi_candidate 1 # p q disc s * s - 4 * n if disc 0: r int(disc ** 0.5) if r * r disc and (s r) % 2 0: p_possible (s r) // 2 q_possible (s - r) // 2 if p_possible * q_possible n: return p_possible, q_possible p_prev2, p_prev1 p_prev1, p q_prev2, q_prev1 q_prev1, q return None这个攻击能成立的条件可以粗略描述为当 d 小于 n 的四分之一次方除以三时连分数逼近非常可靠。更进一步的扩展方法还能把攻击阈值扩展到 d 小于 n 的 0.292 次方这意味着即使 d 看起来不小也可能在攻击范围内。防御角度来说标准做法是生成 d 之后主动检查它的位数如果 d 太短就重新生成密钥对公钥指数取较大的值如 65537也能从概率上避免生成出过小的 d。我在实际做代码审计时最喜欢先从公钥文件里看 d 的 bit 长度一旦发现 d 短得可疑后续测试必须先跑一遍连分数攻击因为这条路径性价比太高了。2.4 模数分解攻击p和q的每一个坏习惯都在帮忙模数分解攻击是 RSA 数学攻击的“主力”方向目标是直接对 n 做因子分解。这里要区分两个层次一个是利用 p、q 的选取缺陷做针对性分解另一个是暴力程度更高的通用分解算法。先说选取缺陷。如果 p 和 q 距离非常近那么 n 可以被平方差方法分解。因为 n pq若 p 和 q 接近则 (pq)/2 和 (p-q)/2 都接近 √n攻击者只需从 √n 开始逐个尝试 a计算 a^2 - n一旦结果是平方数就能立即得到 p 和 q。这条思路实现起来没有任何难度几行循环就能复现。如果 p-1 或 q-1 只包含小素因子可以尝试基于小因子池的分解策略选取一个底数对指数不断做累乘幂再与 n 求最大公约数一旦某些特定条件满足就能暴露 n 的一个因子。这类方法本质上是在“赌”素数的特殊结构。防御方式是生成大素数时做一次特殊结构检查确保 p-1、q-1 中包含足够大的素数因子。比上述更泛化的是通用分解算法。比如二次筛、数域筛法这类算法的能力边界决定了 RSA 实际安全模长。过去几年里已经有 768 位模数被成功分解的记录计算资源投入极其巨大但确实证明了这类算法对短模数是实实在在的威胁。这也是为什么密码学界普遍建议 RSA 模长至少 2048 位、鼓励向 3072 位上迁移。从实际防御角度看除了保证模长足够长还必须保证 p、q 的生成方式满足两个素数独立随机生成、位数接近但数值差距大、随机源质量可靠。我在评估不少自研系统时发现很多开发者直接拿固定种子生成素数或者从同一个伪随机序列里取 p 和 q这种做法会让 n 的生成方式失去随机性给各种针对生成逻辑的攻击留下空间。3. 协议与实现层面的攻击不碰数学难题也能拿到明文3.1 填充预言攻击把解密过程变成提问机器RSA 直接对明文做裸指数运算是危险的所以实际系统里必须加填充。老一代的填充标准在实践中暴露了一个严重问题当接收方解密数据时如果填充格式不合法会返回不同的错误信息。这种看似无害的反馈在攻击者眼里是一个“预言机”。填充预言攻击的经典场景是这样的服务端使用 PKCS#1 v1.5 填充方式解密后检查填充头是否符合规定然后返回“填充合法”或“填充非法”。攻击者截获一个目标密文后不断修改密文并发送给服务端通过观察返回结果逐步逼近。这个过程相当于把解密过程变成了一个能回答“当前密文是否满足某种格式”的提问机器经过大量查询后攻击者可以恢复出完整明文。这个攻击最大的意义在于它根本不尝试分解 n也不需要拿到私钥 d纯粹靠解密行为的外泄信息做事。后续研究发现连解密耗时的微小差异都能被利用攻击者甚至不用通过应用层错误信息只需要观测响应时间就能判断填充是否合法。防御上有几条明确的路径。首选是使用 OAEP 填充方案它的结构设计能从根本上阻断这类攻击其次是统一错误处理不管填充是否合法都返回同样的错误不暴露任何差异最后是在比较填充数据时使用恒定时间比较避免时间差异成为新的侧信道。更深层的建议是尽量采用混合加密RSA 只加密一个随机会话密钥而非直接加密业务数据这样即使密文被反复试探攻击者拿到的也只是无意义的会话密钥片段。3.2 侧信道攻击让密钥在功耗和时间里现形侧信道攻击关注的是 RSA 在物理设备里的执行过程。最典型的例子是模幂运算的时间差异RSA 的模幂运算是按二进制位逐个处理的处理到指数位为 1 时通常会多做一次乘法处理到指数位为 0 时少做一步。如果把私钥或中间状态的指数位与耗时关联起来攻击者通过大量统计测量就能逐步还原出私钥的每一位。功耗分析是另一条路子。智能卡、安全芯片这类设备在运算时的电流变化受数据和操作影响功耗曲线里藏着指数位的走向。简单功耗分析直接看单条曲线就能发现规律差分功耗分析则通过大量采样做统计处理把噪声抹掉、把真实信号找出来。缓存攻击更隐蔽如果 RSA 实现使用了预计算表不同的表项访问会在处理器缓存里留下痕迹攻击者在同一台物理机器上共享 CPU 缓存时就可以通过计时测量偷走信息。防御侧信道没有“加一个开关”这么简单而是从实现层面做防护。恒定时间实现要求代码在所有路径上消耗的时间一致蒙哥马利阶梯结构可以保证无论二进制位是 0 还是 1都执行相同的乘法流程。盲化技术则更巧妙每次加密前给输入乘一个随机数让攻击者观测到的内部数值与真实数值错开。更完整的芯片级防护还需要功耗均衡和电磁屏蔽。这些内容听起来偏硬件但软件开发者也必须懂。因为现在很多安全功能跑在云端虚拟机上攻击者和你共享同一台宿主机缓存侧信道直接跨虚拟机生效。我在做某跨平台系统的威胁建模时就把“是否使用了具备恒定时间特性的底层密码库”列为必查项这是代码层面最容易出问题的地方。3.3 错误注入攻击故意制造小故障来套出素数因子错误注入攻击是主动物理攻击的一种攻击者不再只是被动观察而是主动破坏设备的正常运算过程利用错误结果反推出密钥。RSA 解密和签名经常会用到 CRT 优化也就是把大指数的模幂运算拆成模 p 和模 q 两个较短的模幂最后再合并。这样做性能很好但也引入了一个经典风险。如果攻击者能在其中一个模幂运算期间通过电压毛刺或射线干扰让运算结果出错那么得到的就是一个部分正确、部分错误的签名或解密结果。这个错误结果有一个非常有趣的数学性质它与正确结果做差后和 n 求最大公约数很可能直接得到 p 或 q。原因是错误只污染了模 p 或模 q 其中一个分支另一分支仍然正确所以正确与错误之差只包含其中一个素数因子。防御思路不复杂但执行必须到位签名生成之后先验签验签通过再输出解密过程可以采用冗余计算算两遍做对比。对于关键应用双模冗余是最可靠的每次运算用独立的两套硬件或两套计算流程结果一致才输出。这个攻击提醒我一点评估 RSA 实现时不只是看数学参数还要看运算流程里有没有校验环节。很多纯软件实现根本没有物理故障注入的防护意识会直接暴露在风险下。3.4 量子威胁总有一天对数论难题不再为难量子计算对 RSA 的威胁来自多项式时间的整数分解算法。传统算法分解大整数需要指数级别的计算量而某些量子算法在理论上可以把复杂度降到多项式级别。这意味着一旦具备足够规模的低噪声量子计算机RSA 的数学安全根基就会从根本上动摇。目前量子硬件能力距离威胁 R-SA-2048 还有相当远的距离实际量子比特数和纠错能力都远远不够。但从工程角度看威胁不是“今天会不会发生”而是“我的系统数据在 10 年、20 年后是否还需要保密”。很多数据今天截获、明天解密同样有危害所以密码学社区已经在积极推动后量子迁移也就是把公钥加密和签名算法切换为基于格等新型数学结构的抗量子算法。在迁移路径上我自己的建议是不要等到最后一刻。可以先在系统里加入算法协商机制让客户端和服务端同时支持经典算法与后量子算法再逐步把默认方案切换过去。对于只做短期数据保护的系统风险不大对于证书体系、长期签名这类生命周期长的场景必须提前行动。RSA 本身不会在明天消失但“还安全”的判断应该基于整个使用周期的数据价值来做。4. 实操演练从确认参数到复现攻击的完整流程4.1 第一步提取并检查公钥参数拿到一个公钥文件后不要急着跑攻击脚本先做参数体检。使用常见的证书解析工具可以提取出模数 n 和公钥指数 e。检查时优先看三件事n 的实际位数是否达到 2048 位e 是否为 65537 或过小的值公钥文件里是否包含多余信息可供进一步分析。这一步的价值在于快速筛掉最弱的目标。如果发现 e3小指数攻击已经被写进大概率命中名单如果 e 很小且多个公钥共享 n共模攻击路线优先如果 n 看起来位数不短但来源可疑可能出自不正确的随机数生成后续可以做更深层次的分解尝试。参数体检是最便宜的攻击成本每个安全评估人员都应该把这个环节做扎实。4.2 第二步小指数与共模攻击的Python复现小指数攻击的复现先判断密文与模数关系。如果 c n并且 c 是某个整数的三次方则直接开立方方根如果拿到多个密文且各个模数独立就用 CRT 合并。实际操作里小指数攻击的成功率不取决于 RSA 本身难不难而是取决于明文是否足够小。共模攻击更常见代码在前面已经给出。值得注意的坑是扩展 GCD 求出的 s1 或 s2 是负数时不能直接把负指数放进 pow 函数必须先对密文做模逆。这个细节我在第一次写攻击脚本时踩过当时以为 pow(c1, -1, n) 在旧版本里也能运行结果报错后来统一在代码里判断符号后才稳定下来。4.3 第三步低解密指数攻击的连分数实现低解密指数攻击的代码前面已经给出实际使用时有两个容易出错的细节。第一连分数展开时要注意循环终止条件e 和 n 都是大数展开项数可能很长但判断条件只需检查渐近分数是否满足方程关系不需要跑完所有项。第二解二次方程时判别式一定要为完全平方数并且 p、q 必须都是整数且乘积等于 n否则就是误判继续尝试下一组渐近分数。我跑这个攻击时习惯把每个候选 d 的位数打印出来。如果某个候选 d 的位数明显偏小而且成功反推出了 p 和 q基本可以确认攻击成立。实战中建议先构造一个 d 故意很小的测试密钥对再跑这段代码验证环境正确然后再对目标做检测避免因为代码 bug 产生误判。4.4 第四步给出可落地的工程防御清单攻击复现最终要落到防御侧。把上面所有攻击类型整理成一张表可以让每次代码评审和安全巡检都有明确的参考维度。层面检查项推荐要求参数层模数长度至少 2048 位重要场景 3072 位参数层公钥指数固定使用 65537参数层私钥指数长度生成后检查过短必须重新生成参数层素数生成独立随机生成p-1、q-1 含大素因子填充层加密填充方案使用 OAEP不使用裸 RSA协议层错误反馈解密失败时统一报错不区分原因实现层时间行为使用恒定时间比较和模幂实现实现层签名流程签名后验签关键场景双模冗余迁移层算法演进预留向后量子算法协商的通道这张清单不解决所有问题但它能把 RSA 攻击类型对应的防御基线画清楚。对于大多数业务系统先固化参数层和填充层就能挡住绝大多数能在实战中复现的攻击。5. 常见问题排查与避坑心得5.1 典型问题速查表现象可能原因排查手段小指数攻击脚本跑不出明文m^3 n明文不够小检查明文长度改用填充分析思路共模攻击结果乱码e1、e2 不互素先算扩展 GCD 确认结果等于 1共模攻击报负指数错误Python 版本不支持负指数求逆手动扩展 GCD 求逆或升级解释器版本连分数攻击找到候选但不满足方程私钥 d 超过攻击阈值攻击不适用改用其他思路模数看起来很长但被快速分解p、q 距离太近或 p-1 有特殊结构检查平方差分解可行性检查特殊结构服务端解密时间差异明显填充校验分支不同使用恒定时间实现统一错误信息这张表是我汇总了多次测试经历后整理的。最典型的还是第一行小指数攻击不是每次都生效它强烈依赖明文长度。如果明文已经做了压缩或者本身就是大块数据小指数这条路线基本走不通。5.2 我踩过几次坑后总结的几条经验第一评估 RSA 系统时先做参数体检再上攻击脚本。很多看起来“无法复现”的攻击实际是目标根本不满足攻击前提。先看公钥位长、指数取值、填充方案能省掉大量无用功。第二防御动作的优先级要按攻击条件来排。小公钥指数是因为实现图省事共模攻击是因为架构设计偷懒低解密指数是因为性能优化过度。这些攻击的根源都不是数学原本不够安全而是人在选参数时选了不安全的路径。先在开发规范里禁止这些参数就能从源头切断大部分风险。第三侧信道和错误注入不是实验室里的玩具。很多云场景和硬件设备场景下攻击者真的能拿到运行环境。给密码库做选型时恒定时间特性应该当成硬指标而不是加分项。第四也是我最近体会最深的一点RSA 攻击类型的完整图谱很庞大但最终落到代码层面你会发现 80% 的问题集中在参数选取和填充处理上。把这两层做好系统在面对绝大多数真实攻击者时会非常硬。最后再分享一个小习惯每拿到一套新的 RSA 密钥我都会先把公钥指数的值写进测试报告如果看到 3 或者 5我会直接判定为高风险。这个习惯帮我避免了好几次上线的安全事故希望你也能用上。