从CTF赛题babyRSA出发:利用已知私钥d与密文c逆向求解RSA模数n的实战解析

发布时间:2026/6/29 17:49:03
从CTF赛题babyRSA出发:利用已知私钥d与密文c逆向求解RSA模数n的实战解析 1. 从babyRSA赛题看残缺参数下的RSA破解第一次看到这个CTF题目时我盯着题目描述愣了几秒钟——只给私钥d和密文c却要破解RSA这就像给你一把锁的钥匙却不告诉你锁孔在哪里。但正是这种看似不可能的场景反而揭示了RSA算法中最精妙的数学关系。题目背景是这样的我们拿到了一个用RSA加密的flag密文c已知公钥指数e0x10001这是RSA中最常用的公钥指数值以及对应的私钥d。但最关键的模数n却不知所踪。更棘手的是题目提示素数q是通过nextPrime(p)生成的这意味着两个素数非常接近。这种场景在实际CTF比赛中相当典型比如著名的BUUCTF平台和NCTF2019赛事都出现过类似题型。为什么说这个场景特殊常规RSA破解通常需要知道n的因数分解或者通过侧信道攻击获取信息。但在这里我们手头的工具只有三个数字e、d、c。这就像玩拼图时只有三块碎片却要还原整幅画面。不过别担心数学会给我们指明方向。2. 逆向工程的核心数学原理2.1 从私钥d反推欧拉函数RSA算法的安全性建立在欧拉定理之上其中最关键的关系式是e * d ≡ 1 mod φ(n)这意味着e和d在模φ(n)下互为乘法逆元。我们可以将这个等式改写为e*d - 1 k*φ(n)这里的k就是一个我们需要确定的整数。由于φ(n) (p-1)(q-1)而n pq这就建立了已知量e,d与未知量p,q之间的桥梁。我曾在实际测试中发现k的取值通常不会太大。对于2048位的n即1024位的p和qk的范围一般在2^15到2^16之间。这是因为e*d-1的位数大约是2063~2064位φ(n)的位数是2048位因为(p-1)(q-1)≈pqn所以k的位数应该在15~16位左右2064-2048162.2 利用素数相邻特性缩小范围题目给出了一个重要线索q nextPrime(p)。这意味着两个素数非常接近我们可以利用这个特性来大幅缩小搜索范围。具体来说从等式φ(n) (e*d -1)/k出发由于p和q相邻φ(n) ≈ n ≈ (sqrt(n))^2因此可以先对φ(n)开平方然后在附近寻找素数在实际操作中我通常会这样做from sympy import prevprime, nextprime import gmpy2 # 假设我们已经计算出phi_n approx_p gmpy2.iroot(phi_n, 2)[0] # 对phi_n开平方 p_candidate prevprime(approx_p) # 取前一个素数 q_candidate nextprime(p_candidate) # 取后一个素数3. 实战破解步骤详解3.1 确定k的合理取值范围根据前面的分析我们需要枚举k的可能取值。在实际操作中我建议这样确定范围e 0x10001 d 192757789460378997180354554381755... # 题目给出的d ed_minus_1 e * d - 1 # 确定k的位数范围 for i in range(1000, 3000): if ed_minus_1 2**i and ed_minus_1 2**(i1): print(fed-1 is {i1} bits) break运行后会输出ed-1的位数据此可以估算k的范围。在我的测试中通常会从2^15开始尝试到2^16结束。3.2 暴力破解寻找正确的k值现在进入最关键的步骤——通过枚举k来找到正确的φ(n)。这里有个优化技巧因为(e*d-1)必须能被k整除所以我们可以from tqdm import tqdm # 进度条工具 for k in tqdm(range(2**15, 2**16)): if ed_minus_1 % k 0: phi_n ed_minus_1 // k # 继续后续验证步骤...每次找到符合条件的k时我们需要验证对应的p和q是否满足所有条件。完整的验证流程包括计算φ(n) (e*d-1)/k对φ(n)开平方得到近似p值找到相邻的素数p和q验证(p-1)*(q-1)是否等于φ(n)3.3 完整Python实现脚本结合以上所有步骤这是我在实际比赛中使用的完整破解脚本from Crypto.Util.number import long_to_bytes import gmpy2 import sympy # 题目给定参数 d 192757789460378997180354554381755... # 完整d值 c 538272316807382811069616855829420... # 完整c值 e 0x10001 ed_minus_1 e * d - 1 # 枚举k值 for k in range(2**15, 2**16): if ed_minus_1 % k ! 0: continue phi_n ed_minus_1 // k # 因为q nextPrime(p)所以p≈sqrt(phi_n) approx_p gmpy2.iroot(phi_n, 2)[0] # 在附近寻找素数 p sympy.prevprime(approx_p) q sympy.nextprime(p) # 验证条件 if (p-1)*(q-1) phi_n: n p * q m pow(c, d, n) print(Found flag:, long_to_bytes(m)) break这个脚本在我的测试环境中运行大约需要5-10分钟具体取决于k的范围大小和CPU性能。使用tqdm添加进度条可以更好地掌握破解进度。4. 关键问题与调试技巧4.1 如何处理k值范围判断错误在实际操作中最常遇到的问题就是k的范围估计不准确。如果脚本运行后没有输出flag可能需要扩大k的搜索范围比如尝试2^14到2^17检查ed_minus_1的计算是否正确验证素数生成逻辑是否准确我曾在一次比赛中因为k的范围少算了一位导致浪费了半小时。后来添加了以下调试代码print(fed-1 {ed_minus_1}) print(fed-1 bit length: {ed_minus_1.bit_length()})4.2 优化性能的实用技巧当处理大数运算时性能优化很重要。我发现以下几点特别有用使用gmpy2库代替Python原生大整数运算速度可以提升10倍以上对于素数检测sympy的isprime比gmpy2的is_prime稍慢可以预先计算一些中间值比如phi_n的平方根一个典型的优化后的素数检测代码段import gmpy2 def is_prime(n): return gmpy2.is_prime(n)4.3 验证结果的正确性得到flag后不要急于提交应该进行验证检查n的位数是否为2048位对于1024位的p和q验证e*d ≡ 1 mod φ(n)是否成立检查flag的格式是否符合比赛要求通常以flag{开头我曾经因为没验证flag格式错过了一个显而易见的正确解。现在我的脚本最后都会添加flag long_to_bytes(m) if bflag{ in flag: print(Verified flag:, flag)