CTF中RSA攻击实战:从大素数分解到共模攻击的Python实现
2026/7/15 3:12:00 网站建设 项目流程

1. 项目概述:当RSA遇上CTF,从数学原理到Python实战

如果你玩过CTF(Capture The Flag)比赛,尤其是其中的密码学(Crypto)方向,那你一定绕不开RSA。这个在现实世界中守护着无数网络通信安全的非对称加密算法,在CTF赛场上却成了出题人“折磨”选手的利器。标题里的“大素数分解”和“共模攻击”,正是RSA在CTF中最经典、也最考验选手密码学功底和编程能力的两种攻击场景。很多人一看到数学公式和长长的密钥就头疼,觉得密码学高深莫测。但我想说,只要用Python把原理实现一遍,你会发现这些攻击思路其实非常直观,甚至有点“巧妙”。这篇文章,我就以一个老CTF选手和Python开发者的双重身份,带你亲手用Python代码,把这两个攻击场景从理论变成实战。我们不止要“解出flag”,更要弄懂背后的“为什么”,以及在实际操作中会遇到哪些坑。

2. RSA基础回顾与CTF中的常见设定

在深入攻击之前,我们必须统一语言,确保我们在同一个频道上讨论问题。RSA的安全性建立在“大数分解难题”之上,但CTF题目往往会人为地制造一些弱点,让分解成为可能。

2.1 RSA加密解密的核心流程

RSA算法围绕三个核心步骤展开:密钥生成、加密和解密。在CTF中,我们通常拿到的是公开的(N, e)和密文c,我们的目标是求出明文m

密钥生成:

  1. 选择两个大素数pq
  2. 计算模数N = p * q
  3. 计算欧拉函数φ(N) = (p-1)*(q-1)
  4. 选择一个整数e作为公钥指数,满足1 < e < φ(N),且gcd(e, φ(N)) = 1(即e与φ(N)互质)。常见取值为65537(0x10001)。
  5. 计算私钥指数d,满足e * d ≡ 1 (mod φ(N))。即deφ(N)的乘法逆元。

加密过程:对于明文m(需转换为整数,且m < N),加密得到密文cc ≡ m^e (mod N)

解密过程:使用私钥d解密密文cm ≡ c^d (mod N)

在CTF题目中,pq是绝对保密的。攻击的突破口,往往就隐藏在Nec的特定关系或不当选择中。

2.2 CTF中RSA题目的典型“套路”

CTF不是密码学的标准应用,它更像是一场“找茬”游戏。出题人会故意在以下环节设置漏洞:

  1. 模数N过小:这是最直接的弱点。如果N的位数(如512比特以下)不够大,现代计算机完全可以在可接受的时间内通过暴力或数据库(如factordb)直接分解出pq
  2. 素数生成不当pq不是真正的“随机”大素数。例如,它们可能过于接近、是光滑数(所有素因子都很小)、或者存在某种数学关系(如共用素数)。这为特殊的分解算法(如Pollard‘s p-1, Williams’s p+1)提供了可能。
  3. 公钥指数e选择不当:例如e非常小(如3),可能导致低加密指数攻击。或者,当相同的明文用不同的e加密时,可能适用中国剩余定理攻击。
  4. 共模攻击:多个用户使用相同的N但不同的e对同一明文加密。这是我们今天要重点攻克的场景之一。
  5. 侧信道与选择密文攻击:这类题目更偏向于协议逻辑漏洞,比如可以提交密文并获取解密结果(但不返回明文,只返回一些状态信息),从而推断出私钥信息。

理解这些“套路”,就像掌握了出题人的心理。我们接下来的实战,就是针对“大素数分解”(对应套路1和2)和“共模攻击”(对应套路4)的精准打击。

注意:在真实世界的密码学应用中,N的长度至少应为2048比特,pq必须由密码学安全的随机数生成器产生,且满足一系列严格的安全属性。CTF中的场景是特化的、弱化的,旨在教学和挑战,切勿将CTF中的技巧误用于实际安全评估。

3. 实战一:大素数分解攻击——当N不够“大”

“大素数分解”攻击的核心思想简单粗暴:既然RSA的安全基于N=p*q难以分解,那么如果我能分解N,一切就结束了。在CTF中,分解N的途径主要有两种:一是N本身太小;二是pq本身有弱点。

3.1 场景一:直接暴力分解与小N查询

这是最简单的情况。题目给出的N可能只有几十位或一百多位十进制数。对于这种规模的整数,利用本地数学库或在线分解数据库可以瞬间解决。

实战案例解析:假设我们拿到一个题目:

N = 322831561921859 e = 23 c = 0xdc2eeeb2782c

这个N只有15位十进制数,在现代计算机面前不堪一击。

Python实战步骤:

  1. 分解N:我们可以使用sympy库的factorint函数,或者更专业的gmpy2库。但最简单快捷的方式,是访问像factordb.com这样的网站。将N提交,立刻得到结果:322831561921859 = 13574881 * 23781539
  2. 计算私钥d:得到pq后,计算φ(N) = (p-1)*(q-1),然后求eφ(N)的逆元d
  3. 解密:计算m = c^d mod N
import gmpy2 from Crypto.Util.number import long_to_bytes # 题目数据 N = 322831561921859 e = 23 c = 0xdc2eeeb2782c # 分解N得到的p和q (来自factordb) p = 13574881 q = 23781539 # 验证 assert p * q == N # 计算私钥指数d phi = (p - 1) * (q - 1) d = gmpy2.invert(e, phi) # 求e模phi的乘法逆元 # 解密 m = pow(c, d, N) # 使用pow函数进行模幂运算更高效 print(f”解密后的整数m: {m}“) print(f”十六进制: {hex(m)}“) print(f”转换为字符串: {long_to_bytes(m).decode()}“)

执行结果与Flag提取:运行上述代码,你会得到m=0x33613559,转换为ASCII字符串是”3a5Y“。根据题目描述,这可能就是flag的一部分,或者需要进一步处理(如Base64解码、栅栏密码等)。CTF中,解密出的明文经常需要经过二次判断。

实操心得:

  • 第一反应:遇到RSA题,先把N丢到factordb.com试试。很多入门题就是考你这个。
  • 工具选择:对于本地分解,gmpy2是Python数论计算的事实标准,速度远超纯Python实现。安装可能稍麻烦(特别是Windows),但绝对值得。
  • long_to_bytesCrypto.Util.number中的这个函数是将解密后的大整数转换为字节串的利器,能自动处理0x开头的十六进制和补零问题,比手动hex(m)[2:]然后decode(‘hex’)更稳健。

3.2 场景二:利用素数弱点——Pollard‘s p-1算法

N很大(比如1024比特),无法直接暴力分解时,我们需要寻找pq的弱点。Pollard‘s p-1算法攻击的是这样一个场景:p-1是一个光滑数(B-smooth),即p-1的所有素因子都不大于某个边界B

算法原理浅析:算法的核心基于费马小定理。如果pN的一个素因子,且对于任意与p互质的整数a,有a^(p-1) ≡ 1 (mod p)。 如果我们能计算出一个数M,使得(p-1)整除M。那么根据费马小定理,a^M ≡ 1 (mod p)。这意味着a^M - 1p的倍数。 因此,计算gcd(a^M - 1, N),结果有很大的概率就是p(或者是N本身,如果q-1也整除M)。

如何构造这样的M呢?如果p-1是B-smooth的,那么我们可以令M为所有小于等于B的素数幂的乘积。这样p-1的每个素因子及其幂次都包含在M中,从而(p-1)整除M

Python实战实现:我们假设p-1是光滑的,但不知道边界B。可以采用迭代的方式,让M依次为2!, 3!, 4!, ...,直到成功分解。

import gmpy2 from math import gcd import random def pollard_p_minus_1(N, max_B=100000): ”“” 尝试使用Pollard‘s p-1算法分解N。 :param N: 待分解的合数 :param max_B: 光滑数边界B的最大尝试值 :return: N的一个非平凡因子,若失败则返回None ”“” # 随机选择一个基数a,通常取2 a = 2 # 迭代计算M = k!, k从2开始递增 for k in range(2, max_B): a = pow(a, k, N) # 计算 a^(k!) mod N, 采用迭代方式避免大数计算:a = (a^(k-1)!)^k mod N # 计算 gcd(a-1, N) p = gcd(a - 1, N) if p > 1 and p < N: print(f”在k={k}时成功分解“) return p elif p == N: # 如果gcd等于N,说明选的a或当前路径不好,可以换一个a重试 print(f”在k={k}时gcd等于N,尝试更换基数a“) a = random.randint(3, N-2) # 避免选0,1,N-1 return None # 示例:使用一个已知p-1光滑的N进行测试(此处为示例,实际N应很大) # 假设N = p * q, 其中 p-1 有很多小因子 # 这里用一个小的例子演示原理 N_test = 143 # 实际应使用CTF题目中很大的N result = pollard_p_minus_1(N_test, max_B=20) if result: print(f”找到因子: {result}“) print(f”另一个因子: {N_test // result}“) else: print(f”在max_B范围内未找到因子“)

CTF真题思路还原(以”2017 SECCON very smooth“为例):这个题目的名字就是提示(very smooth)。给了一个HTTPS流量包,从中提取出SSL证书,证书里包含了RSA公钥(N, e)N是1024比特,直接分解几乎不可能。

  1. 提取公钥:用opensslbinwalk从pcap包中提取证书,得到Ne
  2. 判断攻击方式:题目名提示smooth,指向p-1p+1光滑。使用primefac(一个集成多种分解算法的Python库)或自己实现上述算法进行尝试。
  3. 实施攻击:实际解题中发现,使用Williams‘s p+1算法(针对p+1光滑)成功分解了N。这说明出题人构造的素数p满足p+1是光滑数。
  4. 构造私钥解密:得到pq后,计算d,生成私钥文件(PEM格式)。
  5. 导入Wireshark:将私钥导入Wireshark的SSL密钥列表,解密TLS流量,直接在HTTP流中看到包含flag的明文。

注意事项:

  • 算法效率:Pollard‘s p-1算法在p-1确实光滑时非常快,但如果p-1有一个稍大的素因子,算法就会失效或需要极大的B
  • a的选择:通常a=2开始。如果遇到gcd(a^M-1, N) = N,说明算法走到了“坏”的情况,可以尝试更换a(如3, 5, 7...)。
  • 与P+1算法:Williams‘s p+1算法是类似的思路,但针对p+1光滑的情况。在CTF中,如果p-1攻击无效,可以尝试p+1primefac库内置了这两种算法。

4. 实战二:共模攻击——一把锁配多把钥匙的隐患

共模攻击(Common Modulus Attack)是CTF中非常经典的场景。想象一下,一扇门(明文m)用同一把锁芯(模数N)配了两把不同的钥匙(公钥e1e2)来上锁(加密),得到了两个不同的锁具状态(密文c1c2)。如果你同时拥有这两个锁具状态,你有可能在不接触锁芯内部结构(私钥d)的情况下,直接把门打开。

4.1 攻击原理的数学推导

攻击成立的条件是:

  1. 相同的模数N
  2. 两个公钥指数e1e2互质(即gcd(e1, e2) = 1)。

设明文为m,加密过程为:c1 ≡ m^(e1) (mod N)c2 ≡ m^(e2) (mod N)

攻击者已知:N,e1,e2,c1,c2

因为e1e2互质,根据扩展欧几里得算法(Extended Euclidean Algorithm),一定存在两个整数st,满足:e1 * s + e2 * t = 1

注意,st中通常有一个是负数。假设s为负数,我们可以将等式改写为:e1 * (-s) + e2 * t = 1,其中-s为正数。 那么原式即为:e2 * t - e1 * s = 1(这里用s代表正数)。

现在,我们来看密文:c1^s ≡ (m^(e1))^s ≡ m^(e1 * s) (mod N)c2^t ≡ (m^(e2))^t ≡ m^(e2 * t) (mod N)

由于s是负数,c1^s在模运算中无法直接计算(指数为负需要求模逆元)。所以我们需要处理一下: 因为c1 ≡ m^(e1) (mod N),且gcd(m, N)在RSA中一般为1(m是明文,通常与N互质),所以c1在模N下存在逆元c1_inv。 那么c1^(-s) ≡ (c1_inv)^s (mod N)

最终,我们将两个式子相乘:(c1_inv)^s * c2^t ≡ m^(e2 * t - e1 * s) ≡ m^1 ≡ m (mod N)

这样,我们就绕过了私钥d,直接利用c1,c2,e1,e2N恢复出了明文m

4.2 Python代码实现与逐行解析

理论可能有点绕,但代码实现非常清晰。我们以一道经典例题的数据为例:

import gmpy2 from Crypto.Util.number import long_to_bytes # 题目数据:相同的N,不同的e和c N = 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249 e1 = 773 e2 = 839 c1 = 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349 c2 = 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535 # 第一步:使用扩展欧几里得算法求 s 和 t,使得 e1*s + e2*t = gcd(e1, e2) # gmpy2.gcdext(a, b) 返回 (g, s, t),其中 g=gcd(a,b),且满足 a*s + b*t = g gcd, s, t = gmpy2.gcdext(e1, e2) print(f”gcd(e1, e2) = {gcd}“) print(f”s = {s}, t = {t}“) # 因为e1和e2互质,所以gcd应为1。验证:e1*s + e2*t 是否等于1 assert e1 * s + e2 * t == gcd == 1 # 第二步:处理s或t为负数的情况 # 如果s为负,我们需要计算 c1 的模逆元,然后对 -s 次方 # 如果t为负,我们需要计算 c2 的模逆元,然后对 -t 次方 if s < 0: # s为负,计算 c1 模 N 的逆元 c1_inv = gmpy2.invert(c1, N) # 将负指数转为正指数计算 part1 = gmpy2.powmod(c1_inv, -s, N) # 计算 (c1_inv)^(-s) mod N part2 = gmpy2.powmod(c2, t, N) # 计算 c2^t mod N else: # 此时t必然为负(因为s为正,和为1,t必为负) c2_inv = gmpy2.invert(c2, N) part1 = gmpy2.powmod(c1, s, N) part2 = gmpy2.powmod(c2_inv, -t, N) # 第三步:恢复明文 m = (part1 * part2) mod N m = (part1 * part2) % N print(f”恢复的明文整数 m = {m}“) # 第四步:将整数m转换为可读字符串 # 这里得到的m是一个大整数,需要转换为字节。通常有两种情况: # 1. m直接是ASCII码拼接的十进制数。 # 2. m的16进制表示对应字节。 # 我们先尝试直接转换为字节 try: flag_bytes = long_to_bytes(m) print(f”直接转换字节: {flag_bytes}“) # 如果看起来像乱码,可能m是ASCII码十进制拼接 # 例如 m=10210897103...,每2或3位数字对应一个ASCII字符 m_str = str(m) flag_ascii = ”“ i = 0 while i < len(m_str): # 如果以‘1’开头,取3位(因为ASCII 100-127是三位数) if m_str[i] == ‘1’: char_code = int(m_str[i:i+3]) i += 3 else: # 否则取2位(ASCII 32-99是两位数) char_code = int(m_str[i:i+2]) i += 2 flag_ascii += chr(char_code) print(f”作为ASCII十进制拼接解析: {flag_ascii}“) except Exception as e: print(f”转换出错: {e}“) # 也可以尝试hex解码 hex_str = hex(m)[2:] # 去掉‘0x‘前缀 if len(hex_str) % 2 != 0: hex_str = ‘0’ + hex_str try: print(f”尝试Hex解码: {bytes.fromhex(hex_str).decode(‘utf-8’, errors=‘ignore’)}“) except: print(“Hex解码失败”)

代码执行与结果分析:运行这段代码,gcd结果为1,确认e1e2互质。计算出的st一正一负。程序会自动处理负数指数的情况。 最终计算出的m是一个很长的整数:1021089710312311910410111011910111610410511010710511610511511211111511510598108101125。 直接long_to_bytes会得到乱码,因为这里的m实际上是ASCII码的十进制数字直接拼接而成的字符串。例如,102对应字符‘f’,108对应‘l’,97对应‘a’,103对应‘g’。 按照代码中的解析逻辑(三位数‘1xx’或两位数),我们可以成功提取出flag:flag{whenwethinkitispossible}

实操心得与避坑指南:

  1. gmpy2.gcdext是关键:这个函数一次性求出最大公约数和系数st,比自己实现扩展欧几里得算法方便得多。
  2. 负数指数的处理:这是共模攻击代码中最容易出错的地方。一定要判断st的符号,并对负指数项求其底数的模逆元,再对正指数进行模幂运算。gmpy2.invert(a, N)用于求aN的逆元。
  3. 明文的编码:解密得到整数m后,如何将其变为flag字符串是最后一道关卡。常见的编码有:
    • 直接转为字节(long_to_bytes)。
    • 整数是ASCII码十进制拼接(如本题)。
    • 整数是ASCII码十六进制拼接(先hex(m)[2:],然后每两位一组转字符)。
    • Base64编码(需要将整数先转为字节,再解码)。
    • 其他自定义编码。多尝试几种,观察输出中是否有flag{CTF{等常见格式。
  4. 攻击的前提:务必确认gcd(e1, e2) == 1。如果不互质,这个攻击方法无效。
  5. 为什么现实中少见:在真实的公钥基础设施(PKI)中,每个用户都会生成自己独立的(p, q)对,从而得到独一无二的N。共模攻击的场景通常只出现在CTF题目或极不规范的系统设计中。

5. 工具链、调试技巧与进阶学习方向

掌握了核心攻击原理和代码实现,你已经有能力解决大部分基础的RSA CTF题目了。但要成为高手,还需要顺手的工具和调试技巧。

5.1 Python密码学与数学计算库推荐

  1. gmpy2/gmpy2-cffi:这是最重要的库。它提供了GMP(GNU多精度算术库)的Python接口,支持高速的大整数运算、模运算、数论函数(如gcdext,invert,powmod,next_prime,is_prime等)。几乎所有涉及大数的CTF密码学题目都离不开它。
    • 安装pip install gmpy2。如果安装失败,可能需要从非官方whl文件或源码编译,这是新手最大的门槛之一。
  2. pycryptodome/Crypto:一个功能强大的密码学工具包。我们主要用它的Crypto.Util.number模块,里面的long_to_bytesbytes_to_long函数在整数和字节串之间转换非常方便。getPrime函数可以用来生成测试用的素数。
  3. sympy:一个纯Python的符号数学库。它的factorint函数可以进行整数分解(对于较小的数),isprime用于素数测试,还有很多解方程、求模逆的函数,在探索思路时很有用。
  4. primefac:一个集成了多种因数分解算法(包括Pollard‘s p-1, Williams’s p+1, ECM等)的库。在遇到“光滑数”类题目时,可以直接调用,省去自己实现的麻烦。安装:pip install primefac(注意,它依赖gmpy2)。

5.2 CTF解题中的通用调试技巧

  1. 从小处着手,验证逻辑:在编写攻击脚本时,先用自己生成的、小参数的RSA密钥进行测试。确保你的脚本能正确解密你自己加密的消息。这能快速排除代码逻辑错误。
  2. 善用打印和断言:在关键步骤后打印中间变量,如计算出的pqphidst等。使用assert语句验证条件(如p*q == N,e*d % phi == 1),让程序在出错时立刻停止,而不是给出一个莫名其妙的结果。
  3. 处理编码陷阱:解密出整数m后,如果直接转换不对,思考可能的编码。写一个通用的尝试函数:
    def try_decode(m_int): results = [] # 1. 直接转bytes try: results.append((”bytes“, long_to_bytes(m_int).decode(‘utf-8’, errors=‘ignore’))) except: pass # 2. 作为十进制ASCII拼接 s = str(m_int) # ... 实现之前提到的2/3位分割逻辑 # 3. 作为十六进制ASCII拼接 hex_str = hex(m_int)[2:] # ... 补零并尝试每两位分割 # 4. Base64 import base64 try: results.append((”base64“, base64.b64decode(long_to_bytes(m_int)).decode())) except: pass for fmt, val in results: if ‘flag’ in val.lower() or ‘ctf’ in val.lower(): print(f”Found in {fmt}: {val}“) return val return None
  4. 利用在线工具辅助
    • 分解N:首先访问 factordb.com 。对于不大的N,它是神器。
    • 数论计算: wolframalpha.com 可以计算模逆、最大公约数等,用于验证你的结果。
    • 编码转换: cyberchef 是一个万能的数据格式转换、编解码、加解密网站,当你对明文的编码方式不确定时,可以在这里手动尝试各种组合。

5.3 从入门到精通:其他常见RSA攻击模式简介

掌握了分解和共模攻击,你已经打开了RSA CTF的大门。门后的世界更广阔,以下是一些进阶的攻击类型,建议你以此为路线图继续深入学习:

  1. 低加密指数攻击(e很小,如e=3):如果e很小,且明文m满足m^e < N,那么加密过程c = m^e就没有起到模运算的作用,直接对ce次方根即可得到m
  2. 低解密指数攻击(d很小,Wiener攻击):如果私钥d相对于N来说过小,可以通过连分数展开的方法在多项式时间内恢复出d。这要求d < (1/3) * N^(1/4)
  3. 模不互素:如果两个不同的RSA公钥N1N2共享一个素因子(即gcd(N1, N2) != 1),那么直接计算最大公约数就能分解这两个N,从而破解两个密钥。这在密钥生成器使用劣质随机数时可能发生。
  4. Coppersmith相关攻击:这是一类非常强大的攻击,用于在已知明文部分比特、或已知pq的部分比特等情况下,恢复出完整的密钥。它基于LLL格基约化算法,是CTF中高难度RSA题的常客。
  5. 选择密文攻击:例如Bleichenbacher攻击(针对PKCS#1 v1.5填充),允许攻击者通过向服务器提交大量精心构造的密文,并根据服务器的错误反馈(如填充是否正确)来逐步推算出明文。

学习这些攻击,最好的方式依然是找到对应的CTF题目(在BUUOJ、CTFHub、攻防世界等平台有很多分类题库),理解原理后自己动手用Python实现一遍。密码学的魅力在于,看似坚不可摧的数学堡垒,往往因为一个细微的实现疏忽或参数选择不当而土崩瓦解。而CTF,正是寻找和利用这些疏忽的绝佳战场。

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

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

立即咨询