
1. 项目概述与背景在CTFCapture The Flag竞赛的密码学赛道上RSA加密算法是当之无愧的“常青树”。它结构清晰原理公开但围绕它设计的题目却千变万化其中“共模攻击”是一种经典且极具教学意义的攻击场景。很多刚接触CTF密码学的朋友在理论层面理解了“当两个用户使用相同的模数N、不同的公钥指数e加密同一明文时可以通过扩展欧几里得算法恢复明文”这一原理后往往卡在最后一步如何将这一系列数学推导转化为一个能稳定运行、处理各种边界情况的Python脚本看着题目给出的两对(N, e1, c1)和(N, e2, c2)知道大概方向却不知从何下手编码实现。这正是本文要解决的问题。我们不只讲理论更聚焦于“实战”。我将从一个真实的CTF赛题改编自XMan夏令营练习出发手把手带你编写一个健壮的、能够自动破解RSA共模攻击的Python脚本。你会看到从读取数据、计算模逆、处理负数指数到最终拼接出flag每一个环节都有需要注意的“坑”。我将分享我在这类题目中反复调试总结出的经验确保你写的脚本不是“玩具”而是真正能在赛场上“开箱即用”的利器。无论你是CTF新手想夯实基础还是有一定经验的选手想优化自己的工具库这篇文章都将提供清晰的路径和可直接复用的代码。2. 核心原理为什么共模攻击能够成立在深入代码之前我们必须彻底理解共模攻击的数学基础。这不仅是通过考试的要求更是写出正确脚本的前提。如果你对其中任何一步感到模糊那么脚本中一个符号的错误就可能导致满盘皆输。RSA加密的核心过程是对于明文m使用公钥(N, e)计算密文c ≡ m^e (mod N)。解密则是用私钥d计算m ≡ c^d (mod N)其中d是e关于φ(N) (p-1)(q-1)的模逆元。共模攻击的场景非常特殊两个不同的用户使用了相同的模数N但各自拥有不同的公钥指数e1和e2并且用各自的公钥加密了同一份明文m。于是我们得到了两组密文c1 ≡ m^e1 (mod N)c2 ≡ m^e2 (mod N)攻击者手头有N,e1,e2,c1,c2。我们的目标是恢复m。攻击成立的关键在于一个数论定理如果两个整数e1和e2互质即gcd(e1, e2) 1那么一定存在两个整数r和s使得r*e1 s*e2 1。这个等式就是扩展欧几里得算法Extended Euclidean Algorithm的直接结论。现在我们来看神奇的推导根据密文定义我们有c1 ≡ m^e1 (mod N)和c2 ≡ m^e2 (mod N)。对这两个式子分别取r次方和s次方c1^r ≡ (m^e1)^r ≡ m^(e1*r) (mod N)c2^s ≡ (m^e2)^s ≡ m^(e2*s) (mod N)将上面两个同余式相乘c1^r * c2^s ≡ m^(e1*r) * m^(e2*s) ≡ m^(e1*r e2*s) (mod N)代入关键等式r*e1 s*e2 1c1^r * c2^s ≡ m^1 ≡ m (mod N)至此理论上我们已经找到了恢复明文m的方法计算c1^r * c2^s mod N。整个攻击过程完全不需要分解大整数N也无需计算私钥d它巧妙地利用了公钥指数之间的关系和同一明文被多次加密的漏洞。注意这里有一个至关重要的前提就是e1和e2必须互质。在CTF题目中出题人几乎总是会给出互质的e1和e2比如常见的 3 和 65537或者像 773 和 839 这样的大质数。如果你的脚本计算结果不对第一件事就是检查gcd(e1, e2)是否等于 1。3. 实战拆解从题目到可运行脚本的完整流程我们以一道经典的题目作为蓝本它清晰地展示了共模攻击的所有要素n 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249 e1 773 c1 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349 e2 839 c2 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535任务根据上述已知条件求出明文m。3.1 环境准备与工具选择工欲善其事必先利其器。对于CTF中的密码学题目尤其是涉及大整数运算的RSAPython是首选语言因为它有成熟的大数支持和高精度计算库。核心库gmpy2这是我们的“核武器”。Python原生的int类型虽然支持大整数但在进行模幂运算pow(a, b, c)时当指数b或模数c极大时纯Python实现可能会非常慢。gmpy2是著名的GMPGNU Multiple Precision Arithmetic Library库的Python接口其底层由C实现对大整数的模幂、模逆、最大公约数等运算进行了极致优化速度比原生Python快几个数量级。在CTF比赛中时间就是分数使用gmpy2是基本操作。安装非常简单在命令行中执行pip install gmpy2如果安装遇到困难特别是在Windows上可以尝试从 Unofficial Windows Binaries for Python Extension Packages 下载对应Python版本的.whl文件进行安装。备用方案pycryptodome或原生pow如果因为环境限制实在无法安装gmpy2Python自带的pow(a, b, c)函数也能完成模幂计算math.gcd可以计算最大公约数。pycryptodome库也提供了丰富的密码学工具。但对于本文的共模攻击gmpy2的gcdext扩展欧几里得函数能直接返回我们需要的r和s最为方便。3.2 脚本编写逐步实现与深度解析下面我们一步步构建攻击脚本并解释每一行代码背后的意图和可能遇到的陷阱。第一步导入库并定义题目参数import gmpy2 # 题目给出的已知条件 n 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249 e1 773 c1 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349 e2 839 c2 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535这里直接以十进制大整数的形式赋值。有时题目会以十六进制0x...或Base64形式给出你需要先将其转换为Python的整数。例如十六进制字符串可以用int(hex_str, 16)转换。第二步使用扩展欧几里得算法求系数 r 和 s这是整个攻击的核心计算。# 计算扩展欧几里得系数使得 r*e1 s*e2 gcd(e1, e2) # 因为e1和e2互质所以gcd(e1, e2) 1 gcd, r, s gmpy2.gcdext(e1, e2) print(fgcd(e1, e2) {gcd}) print(f系数 r {r}, s {s})gmpy2.gcdext(a, b)返回一个三元组(g, s, t)满足a*s b*t g其中g是a和b的最大公约数。这里ae1,be2所以返回的s对应我们公式中的rt对应公式中的s。注意变量名的对应关系。运行这部分代码你会得到类似输出gcd(e1, e2) 1 系数 r 62037, s -57122果然r和s中有一个是负数这里是s。这是扩展欧几里得算法的正常结果。负数指数在模运算中意味着我们需要计算模逆元。第三步处理负数指数这是新手最容易出错的地方。我们不能直接计算c2^(-57122) mod N因为负指数没有直接的模运算定义。我们需要利用模逆元的概念 如果s是负数设s -|s|那么c2^s ≡ (c2^{-1})^{|s|} (mod N)。 也就是说我们需要先计算c2在模N下的逆元inv_c2然后计算inv_c2^{|s|}。同理如果r是负数也需要对c1做同样处理。# 处理负数指数如果指数为负则计算密文的模逆元并将指数转为正数 if r 0: r -r # 指数取绝对值 c1 gmpy2.invert(c1, n) # 计算c1关于模n的逆元 if s 0: s -s c2 gmpy2.invert(c2, n) print(f处理后的系数: r {r}, s {s}) print(f处理后的c1如需: {c1}) print(f处理后的c2如需: {c2})gmpy2.invert(a, n)计算的是整数a在模n下的乘法逆元即找到一个数x使得(a * x) % n 1。前提是a和n互质。在RSA中密文c是m^e mod N而m和N互质因为m通常小于N且N是质数乘积所以c也与N互质逆元一定存在。第四步计算明文 m现在我们有了非负的指数r,s和对应的底数可能是原密文或其逆元。直接套用公式m ≡ c1^r * c2^s (mod N)。# 计算明文 m c1^r * c2^s mod n # 使用gmpy2.powmod进行模幂运算效率远高于 pow(c1, r) % n m1 gmpy2.powmod(c1, r, n) m2 gmpy2.powmod(c2, s, n) m (m1 * m2) % n print(f计算得到的明文整数: {m})这里使用了gmpy2.powmod(a, b, c)它等价于pow(a, b, c)但通常更快。我们分别计算c1^r mod N和c2^s mod N然后在模N下相乘。注意最后一步的乘法也要取模。运行至此我们得到了一个巨大的整数m。它就是被加密的原始信息但通常不是直接可读的文本。3.3 明文解码从整数到Flag在CTF中m很少直接是ASCII字符串对应的整数。出题人通常会对其进行编码。最常见的编码方式有两种将明文文本的每个字符转换为其ASCII码然后拼接成一个长整数。例如“flag”会被转换成0x666c6167十六进制或1718378855十进制。将明文文本先进行Base64编码或十六进制编码再将编码后的字符串按ASCII码转换成整数。我们的任务是将这个整数m逆向转换回去。一个非常实用的方法是先将整数转换为十六进制字符串然后尝试将其解释为字节数据。# 将整数m转换为十六进制字符串并去掉开头的0x hex_str hex(m)[2:] print(f明文的十六进制表示: {hex_str}) # 尝试将十六进制字符串转换为字节ASCII try: # 如果十六进制字符串长度为奇数在前面补一个0 if len(hex_str) % 2 ! 0: hex_str 0 hex_str bytes_data bytes.fromhex(hex_str) text bytes_data.decode(ascii) print(f直接解码为ASCII: {text}) except (ValueError, UnicodeDecodeError): print(直接ASCII解码失败可能包含非打印字符或使用了其他编码。)对于我们的例题运行这段代码hex(m)的结果会是一个很长的十六进制数直接解码为ASCII很可能是一堆乱码。这是因为题目采用了第一种编码方式将每个字符的ASCII码直接拼接。例如字符f的ASCII码是102l是108a是97g是103。它们拼接成的数字是10210897103...而不是十六进制0x666c6167。因此我们需要另一种解码策略将整数按数字位拆分每2位或3位数字对应一个ASCII字符。观察发现ASCII码中可打印字符的范围是32-126大部分是两位数如a97但超过99的如f102就是三位数。所以我们需要一个简单的解析器def decode_long_int(num): 将按ASCII码直接拼接的长整数解码为字符串 plain_str str(num) i 0 result [] while i len(plain_str): # 如果当前位是1尝试取三位数字因为ASCII码100-199都是三位数 if plain_str[i] 1 and i 2 len(plain_str): # 取三位数字如 102 ascii_code int(plain_str[i:i3]) # 检查是否在合理的ASCII范围可打印字符或常见控制字符 if 32 ascii_code 126 or ascii_code 10 or ascii_code 13: result.append(chr(ascii_code)) i 3 continue # 否则尝试取两位数字如 97 代表 a if i 1 len(plain_str): ascii_code int(plain_str[i:i2]) if 32 ascii_code 99: # 两位数且是可打印字符的起始范围 result.append(chr(ascii_code)) i 2 continue # 如果都不符合说明可能解析有误或编码方式不同 # 尝试回退到一位数极少见或报错 ascii_code int(plain_str[i]) result.append(chr(ascii_code)) # 这可能产生乱码 i 1 return .join(result) decoded_text decode_long_int(m) print(f通过数字拆分解码: {decoded_text})运行这个函数我们终于得到了可读的明文flag{whenwethinkitispossible}。这正是我们寻找的Flag。实操心得明文解码是CTF密码学题目的最后一步也是最容易卡住的一步。除了上述两种方法还可能是Base64编码解码后可能仍需二次处理、直接是十六进制字符串hex(m)[2:]直接可读、甚至是JSON或某种自定义格式。养成习惯在得到整数m后依次尝试1. 转十六进制看是否有666c6167flag的hex模式2. 转字节看能否decode(utf-8)或decode(latin-1)3. 用上面的数字拆分法4. 将字节数据用binascii.unhexlify或base64.b64decode处理。一个通用的调试技巧是打印出hex(m)[2:30]看看开头几个字节往往能发现线索。4. 完整脚本与健壮性优化将上述所有步骤整合我们就得到了一个基础版的共模攻击脚本。但一个健壮的脚本应该能处理更多边界情况并给出清晰的输出。下面是我在实战中打磨后的版本#!/usr/bin/env python3 RSA Common Modulus Attack Solver 适用于CTF中N相同e1和e2不同加密同一明文的场景。 import gmpy2 import binascii import argparse def rsa_common_modulus_attack(n, e1, c1, e2, c2, verboseTrue): 执行RSA共模攻击。 参数: n: 公共模数 e1, c1: 第一组公钥指数和密文 e2, c2: 第二组公钥指数和密文 verbose: 是否打印详细过程 返回: 解密得到的整数明文 m以及可能的文本解码结果。 if verbose: print([*] 开始RSA共模攻击) print(f N {n}) print(f e1 {e1}, c1 {c1}) print(f e2 {e2}, c2 {c2}) # 1. 检查e1和e2是否互质 gcd_val gmpy2.gcd(e1, e2) if gcd_val ! 1: print(f[!] 错误e1({e1}) 和 e2({e2}) 的最大公约数为 {gcd_val}不互质。共模攻击可能不适用或需要调整。) # 某些特殊情况下如果gcd(e1, e2)g且m^g N仍可攻击但更复杂。 return None, None if verbose: print(f[] e1 和 e2 互质 (gcd 1)满足共模攻击条件。) # 2. 使用扩展欧几里得算法求系数 # 计算 r, s 使得 r*e1 s*e2 1 gcd, r, s gmpy2.gcdext(e1, e2) if verbose: print(f[] 扩展欧几里得结果: gcd{gcd}, r{r}, s{s}) # 3. 处理负数指数计算密文的模逆元 if r 0: if verbose: print(f[*] r 为负数({r})计算 c1 关于模 N 的逆元。) r -r c1 gmpy2.invert(c1, n) if s 0: if verbose: print(f[*] s 为负数({s})计算 c2 关于模 N 的逆元。) s -s c2 gmpy2.invert(c2, n) # 4. 计算明文 m c1^r * c2^s mod N # 使用powmod进行模幂运算避免中间结果过大 if verbose: print(f[*] 计算 m c1^{r} * c2^{s} mod N) m1 gmpy2.powmod(c1, r, n) m2 gmpy2.powmod(c2, s, n) m (m1 * m2) % n if verbose: print(f[] 攻击成功恢复的明文整数 m {m}) print(f[] 十六进制表示: {hex(m)}) return m, hex(m)[2:] def try_decode_as_text(hex_str): 尝试将十六进制字符串解码为文本。 results {} # 方法1: 直接 fromhex 解码为 ASCII/UTF-8 try: # 确保十六进制字符串长度为偶数 if len(hex_str) % 2 ! 0: hex_str 0 hex_str bytes_data bytes.fromhex(hex_str) # 尝试常见编码 for encoding in [ascii, utf-8, latin-1]: try: text bytes_data.decode(encoding) if any(c.isprintable() or c in \n\r\t for c in text): results[fbytes.fromhex - decode({encoding})] text except UnicodeDecodeError: pass except ValueError as e: results[fromhex Error] f无效的十六进制字符串: {e} # 方法2: 将整数作为十进制字符串按2/3位数字拆分针对ASCII码拼接 try: # 这里我们假设传入的hex_str是原始整数m的十六进制需要先转回整数 # 但函数接收的是hex_str所以我们需要从外部传入m pass # 此方法在另一个函数中实现 except Exception as e: pass return results def decode_long_int(num): 专门处理将长整数按ASCII码十进制拼接的解码。 plain_str str(num) i 0 result_chars [] while i len(plain_str): # 优先尝试取三位数因为ASCII码100-126是三位数 if i 2 len(plain_str) and plain_str[i] 1: three_digit int(plain_str[i:i3]) if 100 three_digit 126: # 可打印字符的高位区 result_chars.append(chr(three_digit)) i 3 continue # 尝试取两位数ASCII 32-99 if i 1 len(plain_str): two_digit int(plain_str[i:i2]) if 32 two_digit 99: result_chars.append(chr(two_digit)) i 2 continue # 如果都不匹配尝试取一位数很少见可能为0-9或不可打印控制符 one_digit int(plain_str[i]) # 只添加可打印字符或常见控制符 if 10 one_digit 13 or one_digit 9: # LF, CR, TAB等 result_chars.append(chr(one_digit)) elif 32 one_digit 126: result_chars.append(chr(one_digit)) else: # 无法解码保留原始数字并用?标记 result_chars.append(f{one_digit}) i 1 return .join(result_chars) def main(): parser argparse.ArgumentParser(descriptionRSA Common Modulus Attack Solver) parser.add_argument(-n, typeint, helpCommon modulus N) parser.add_argument(-e1, typeint, helpFirst public exponent) parser.add_argument(-c1, typeint, helpFirst ciphertext) parser.add_argument(-e2, typeint, helpSecond public exponent) parser.add_argument(-c2, typeint, helpSecond ciphertext) parser.add_argument(--hex, actionstore_true, helpTreat ciphertext inputs as hex strings) parser.add_argument(-q, --quiet, actionstore_true, helpSuppress verbose output) args parser.parse_args() # 如果未通过命令行参数输入则使用硬编码的示例数据 if not all([args.n, args.e1, args.c1, args.e2, args.c2]): print([*] 未提供完整参数使用示例数据XMan夏令营题目。) n 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249 e1 773 c1 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349 e2 839 c2 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535 else: n, e1, c1, e2, c2 args.n, args.e1, args.c1, args.e2, args.c2 if args.hex: c1 int(c1, 16) if isinstance(c1, str) else c1 c2 int(c2, 16) if isinstance(c2, str) else c2 verbose not args.quiet m, hex_str rsa_common_modulus_attack(n, e1, c1, e2, c2, verbose) if m is not None: print(\n *50) print([*] 尝试解码明文...) print(f整数 m: {m}) print(f十六进制: {hex(m)}) # 尝试方法1: 直接十六进制转字节 decode_results try_decode_as_text(hex_str) for method, text in decode_results.items(): print(f方法 {method}: {text[:100]}... if len(text) 100 else f方法 {method}: {text}) # 尝试方法2: 数字拆分针对ASCII码拼接 decoded_by_split decode_long_int(m) print(f数字拆分解码: {decoded_by_split}) # 检查是否有明显的flag格式 combined_text decoded_by_split for _, txt in decode_results.items(): combined_text txt if flag in combined_text.lower() or ctf in combined_text.lower(): print(\n[!] 发现疑似Flag的字符串) # 可以尝试用正则表达式提取flag{...}格式 import re flag_pattern re.compile(rflag\{[^}]\}, re.IGNORECASE) match flag_pattern.search(combined_text) if match: print(f[] 提取到Flag: {match.group()}) print(*50) if __name__ __main__: main()这个脚本的优化点包括参数化输入支持命令行参数方便快速测试不同题目。健壮性检查验证e1和e2是否互质。详细的调试输出通过verbose参数控制便于理解每一步发生了什么。多重解码尝试自动尝试多种常见的编码方式提高解出Flag的成功率。Flag自动识别尝试匹配flag{...}或ctf{...}等常见格式。你可以将题目数据直接替换脚本中的示例部分或通过命令行运行python rsa_common_attack.py -n N -e1 e1 -c1 c1 -e2 e2 -c2 c25. 常见问题、调试技巧与扩展场景即便有了完整的脚本在实际解题中你仍可能遇到各种问题。下面是我在多次实战中总结的排查清单和进阶技巧。5.1 攻击失败的可能原因与排查步骤前提条件不满足检查点gcd(e1, e2) 1是否成立如果不成立标准共模攻击无效。可能需要更复杂的攻击如共享素数攻击如果N不同但有一个公因数。检查点两个密文c1和c2确实是使用相同模数N和相同明文m加密的吗仔细阅读题目描述。数据格式错误检查点你输入的N,e1,c1,e2,c2是十进制整数吗有时题目给出的是十六进制以0x开头或Base64编码。务必先正确转换。一个快速检查方法是打印它们的类型和长度print(type(n), n.bit_length())。一个1024位的RSAn的比特长度应该在1024左右。检查点密文c是否可能大于模数N在正确的RSA加密中c必须满足0 c N。如果c N说明数据可能有问题。解码失败现象计算出的m是一个很大的整数但转换成十六进制或字节后是乱码。排查首先打印hex(m)[2:20]看看前几个字节。如果看到666c6167那就是flag的十六进制说明明文就是十六进制字符串。尝试long_to_bytes(m)如果安装了Crypto.Util.number或m.to_bytes((m.bit_length()7)//8, big)。明文m可能不是文本而是其他数据如图片、压缩包的头。尝试将字节数据写入文件用file命令或十六进制编辑器查看。题目可能对明文m进行了填充如PKCS#1 v1.5。这时直接解码得到的结果开头会有一些非文本字节。你需要识别并去除填充。一个特征是解密出的字节串开头可能是\x00\x02...。脚本计算错误验证你可以用得到的结果m手动验证一下。计算pow(m, e1, n)和pow(m, e2, n)看是否等于题目给出的c1和c2。如果不等说明攻击过程或解码有误。调试在计算c1^r和c2^s之前打印出处理后的r,s,c1,c2确保负数指数处理正确。5.2 进阶场景与脚本扩展多组密文e1, e2, e3... 如果题目给出了三组或更多使用相同N加密的密文且所有公钥指数e1, e2, e3两两互质理论上可以对其中任意两组进行共模攻击。但更一般的情况是如果gcd(e1, e2, e3) 1你可以找到一组系数a, b, c使得a*e1 b*e2 c*e3 1然后计算c1^a * c2^b * c3^c mod N。这需要解一个多元一次丢番图方程可以使用格基规约LLL算法求解已超出本文范围。N不完全相同但有公因数 这是“模不互素”攻击不是共模攻击。如果两个用户的模数N1和N2不互质那么gcd(N1, N2)很可能就是其中一个质数p。你可以用gmpy2.gcd(N1, N2)快速分解出p进而计算出q和私钥d。在CTF中如果看到多个N先尝试两两求gcd也许有意外收获。e1和e2不互质但gcd较小 如果g gcd(e1, e2) 1但明文m满足m^g N即m的g次方没有超过模数那么仍然可以攻击。因为此时c1 ≡ m^e1 ≡ (m^g)^(e1/g) mod N对c2同理。令m m^g则问题转化为对m的共模攻击指数变为e1/g和e2/g它们很可能互质。解出m后再开g次方根得到m。开方可以使用gmpy2.iroot(m_prime, g)。5.3 性能优化与注意事项大整数运算始终使用gmpy2.powmod(a, b, n)而不是pow(a, b) % n或a ** b % n。后者会先计算巨大的a^b可能导致内存溢出而powmod使用模幂算法中间结果始终小于n。内存与时间对于非常大的指数r和s可能几十万powmod运算仍然需要时间。如果你的脚本运行时间过长检查指数大小。在共模攻击中通过扩展欧几里得算法求出的r和s通常与e1、e2同数量级不会过于夸张。编码陷阱Python中hex()产生的字符串不带0x前缀但长度可能是奇数。bytes.fromhex()要求长度为偶数所以需要补零。long_to_bytes函数来自Crypto.Util.number会自动处理这个问题是更安全的选择。6. 从解题到出题深入理解共模攻击的威胁通过编写这个攻击脚本我们不仅学会了解一道CTF题更重要的是理解了RSA在实际应用中的一个重要安全准则绝对不要在不同用户之间共享模数N。在早期的RSA实践中有些系统为了节省密钥生成的计算量因为寻找大素数很耗时让多个用户使用同一个N只分配不同的e和d。这看起来是高效的但共模攻击彻底打破了这种幻想。攻击者只需要截获两个用户加密同一消息的密文比如一条广播消息或一个常用指令就能在不分解N、不知道任何私钥的情况下恢复明文。因此现代RSA的最佳实践是每个用户必须独立生成自己的一对素数p和q从而拥有独一无二的模数N。公钥指数e可以相同通常都选65537这没有问题。即使使用相同的e只要N不同共模攻击就无法进行。在CTF出题中共模攻击常作为密码学的入门题因为它完美结合了数论扩展欧几里得算法、模运算和编程实现。为了增加难度出题人可能会将e1和e2设置得非常大使得r或s为负数且绝对值很大考验你对模逆元的处理。对明文m进行复杂的编码或填充增加解码难度。将共模攻击作为整个挑战的一部分需要你先从流量包、内存镜像或网络通信中提取出N,e,c等参数。掌握了本文的脚本和思路你就能从容应对这类基础题目。更重要的是你将RSA从一个黑盒算法变成了可以亲手剖析和攻击的对象这种对原理的深刻理解是你在CTF密码学道路上走得更远的关键。下次看到“相同的N不同的e”你就能会心一笑知道flag已经近在咫尺了。