超大 n 反噬:n 越大未必越安全(统一开方攻击,兼容低指数与超大 n)
背景
在 第七节:低加密指数攻击 里我们学过:当 e 很小(如 e=3)且明文 m 也很小,使得 m e < n m^e < n me<n 时,密文 c = m^e 没有发生模 n 折返,直接开 e 次方就能恢复明文。
那一节的结论是:e 小是攻击的触发条件。
但这一节我们要讲一个反直觉的情况:即使 e 不小(e=65537 是标准值),只要 n 足够大、m 足够小,使得 m e < n m^e < n me<n 仍然成立,攻击照样成立!
更妙的是------n 越大, m e < n m^e < n me<n 越容易满足,攻击反而越容易 。这正是本节标题"超大 n 反噬"的含义:出题人把 n 做到 16777216(即 2 24 2^{24} 224)位,本意是"大到无法分解",结果反而帮了攻击者一把------n 越大,明文 m 的 e 次方"折返"的概率越低,c = m^e 精确成立的概率越高。
与第七节的统一视角
第七节和本节表面上是两种攻击,本质上是同一个攻击:
| 对比项 | 第七节(低加密指数) | 本节(超大 n 反噬) |
|---|---|---|
| 触发原因 | e 很小(e=3,5,7) | n 特别大(相对 m 而言) |
| e 的大小 | 很小 | 正常(e=65537 也可以) |
| n 的大小 | 正常(几百~几千位) | 超大(本题 16M 位) |
| 共同条件 | m e < n m^e < n me<n | m e < n m^e < n me<n |
| 攻击方法 | 开 e 次方 | 开 e 次方 |
区别只在于"是什么让 m e < n m^e < n me<n 成立":第七节是 e 太小导致的,本节是 n 太大导致的。既然本质相同,就应当用同一份代码解决------这正是本节采用的"最佳方案"思路。
攻击原理
核心条件: m e < n m^e < n me<n
RSA 加密公式:
c = m e m o d n c = m^e \bmod n c=memodn
当 m e < n m^e < n me<n 时,模运算不起作用(没有"折返"),密文退化为:
c = m e c = m^e c=me
此时 c 是一个完全的 e 次幂 ,直接开 e 次方即可恢复明文:
m = c e = c 1 / e m = \sqrte{c} = c^{1/e} m=ec =c1/e
全程不需要分解 n,不需要私钥 d,不需要 p、q!
为什么"n 越大越容易中招"?
关键在于 m e < n m^e < n me<n 这个条件。我们对它做个简单分析:
- 明文 m 通常是 flag 字符串,长度固定(比如几十字节)
- m 的比特数 b m b_m bm 基本是常数(与 n 无关)
- m e m^e me 的比特数约为 b m × e b_m \times e bm×e
- n 的比特数 b n b_n bn 是题目决定的
所以攻击条件等价于:
b m × e < b n b_m \times e < b_n bm×e<bn
即:
b m < b n e b_m < \frac{b_n}{e} bm<ebn
n 越大( b n b_n bn 越大),右边越大,明文 m 允许的最大长度就越大,攻击越容易成立!
这就是"超大 n 反噬"的本质:出题人以为 n 越大越安全,但只要 m 是短的 flag,n 越大反而让 m e < n m^e < n me<n 越容易满足。
如何判断题目是否可攻击?
拿到题目后,先看 c 和 n 的位数关系:
- 如果 c < n(密文比模数小,这是必然的)
- 且 c 的比特数 / e ≈ 一个合理的明文长度(比如 100~1000 比特,对应几十~几百字节的 flag)
- 那么 m e < n m^e < n me<n 很可能成立,直接尝试开 e 次方
具体来说,如果:
bits ( c ) ≈ bits ( m ) × e 且 bits ( m ) × e < bits ( n ) \text{bits}(c) \approx \text{bits}(m) \times e \quad \text{且} \quad \text{bits}(m) \times e < \text{bits}(n) bits(c)≈bits(m)×e且bits(m)×e<bits(n)
就说明模运算没起作用,c = m^e 精确成立。
统一攻击代码(最佳方案)
设计思路
经过上面的分析,我们意识到:无论是"e 小"还是"n 大",只要 m e m^e me 没有折返(或只折返了少量次数),都可以用"开 e 次方"恢复明文 。因此本节采用一份统一代码,同时覆盖两类场景:
- 开方引擎用
gmpy2.iroot:基于 GMP 的高精度整数开方,对小数据(n 几千位)和超大数据(n 1600 万位)都快,避免第七节纯 Python 二分查找在大数上卡死的问题。 - 保留 Coppersmith 枚举 k 的 fallback :当 m e m^e me 稍大于 n(发生少量折返)时,通过枚举 k k k 求解 m = c + k ⋅ n e m = \sqrte{c + k \cdot n} m=ec+k⋅n ,提升鲁棒性。本节题目中 m e < n m^e < n me<n 必然成立,k=0 直接命中,但 fallback 让这份代码也能解第七节的题。
- 不依赖 n 的大小做分支判断:统一流程,一份代码通吃。
核心函数
python
import gmpy2
from gmpy2 import mpz, iroot
def rsa_small_e_attack(c, e, n, max_k=100):
"""
统一低指数 / 超大 n 开方攻击
兼容第七节(e 小)和第十七节(n 大)两种场景
原理:
- 若 m^e < n: c = m^e(无折返), m = iroot(c, e)
- 若 m^e 稍大于 n: c = m^e - k*n(少量折返), 枚举 k 求 m = iroot(c + k*n, e)
参数:
c, e, n: RSA 公钥与密文
max_k: Coppersmith 枚举上限(应对少量折返)
返回:
(m, k) 成功: m 为明文整数, k 为折返次数(0 表示无折返)
(None, None) 失败
"""
c = mpz(c)
n = mpz(n)
# 枚举 k = 0, 1, 2, ... (k=0 即"直接开方", 对应 m^e < n 无折返)
for k in range(0, max_k):
m, is_exact = iroot(c + k * n, e)
if is_exact:
# 二次验证: m^e mod n == c, 确保是真解
if pow(int(m), e, int(n)) == int(c):
return int(m), k
return None, None
def m_to_bytes(m):
"""把明文整数转为字节串"""
return m.to_bytes((m.bit_length() + 7) // 8, 'big')
为什么这份代码能同时解决两节问题?
- 第七节作业(e=3, n=2048 位) : m 3 < n m^3 < n m3<n 成立 → k=0 时
iroot(c, 3)精确命中 → 直接返回(m, 0) - 第十七节作业(e=65537, n=1600 万位) : m 65537 < n m^{65537} < n m65537<n 成立 → k=0 时
iroot(c, 65537)精确命中 → 直接返回(m, 0) - 若遇到 m e m^e me 稍大于 n 的题:k=0 开方失败 → 自动枚举 k=1,2,... 用 Coppersmith 方法兜底
关键点 :用 gmpy2.iroot 而不是自写二分查找。第十七节的 c 有 1400 万位,纯 Python 二分查找会卡死,而 gmpy2.iroot 基于 GMP 库,毫秒级就能完成。
简单例子
我们用 p=17, q=19 来演示,注意这里的 e=7 并不算"特别小",但只要 m 足够小,攻击照样成立。
生成密钥
- p = 17, q = 19
- n = p × q = 17 × 19 = 323
- φ(n) = (p-1)(q-1) = 16 × 18 = 288
- 选 e = 7(gcd(7, 288) = 1 ✓),公钥 (e, n) = (7, 323)
- 求私钥 d:7 × d ≡ 1 (mod 288)
- 7 × 247 = 1729 = 6 × 288 + 1 ≡ 1 ✓
- 所以 d = 247
三个场景
- 场景一:n=323, e=7, m=2 → m^e=128 < 323 → 不折返 → c=128, 开方 m=2 ✓
- 场景二:n=323, e=7, m=3 → m^e=2187 > 323 → 折返 → c=249, 开方失败 ✗
- 场景三:n=10403, e=7, m=3 → m^e=2187 < 10403 → 不折返 → c=2187, 开方 m=3 ✓(n 变大后成功!)
同一个明文 m=3、同一个 e=7,n 小时攻击失败,n 大时攻击反而成功------这就是"超大 n 反噬"。
演示代码(用统一函数跑三个场景)
python
# ============================================================
# RSA 超大 n 反噬演示(手算例子,使用统一攻击函数)
# ============================================================
#
# 场景一: n=323, e=7, m=2 → m^e=128 < n → c=128, 开方 m=2 ✓
# 场景二: n=323, e=7, m=3 → m^e=2187 > n → c=249, 开方失败 ✗
# 场景三: n=10403, e=7, m=3 → m^e=2187 < n → c=2187, 开方 m=3 ✓ (n 变大后成功!)
# ============================================================
from rsa_unified_attack import rsa_small_e_attack, m_to_bytes
def demo(name, n, e, m):
"""演示一次加密 + 统一开方攻击"""
c = pow(m, e, n)
m_e = m ** e
print(f"=== {name} ===")
print(f" 公钥 (e, n) = ({e}, {n})")
print(f" 明文 m = {m}")
print(f" m^e = {m}^{e} = {m_e}")
print(f" m^e < n ? {m_e} < {n} ? {m_e < n}")
print(f" 密文 c = m^e mod n = {c}")
if m_e < n:
print(f" → 没有折返,c = m^e 精确成立")
else:
print(f" → 发生折返,c = m^e - {m_e // n}×n = {c}")
# 调用统一攻击函数
recovered, k = rsa_small_e_attack(c, e, n)
if recovered is not None:
print(f" 攻击: rsa_small_e_attack → m = {recovered}, k = {k} ✓ 成功!")
else:
print(f" 攻击: rsa_small_e_attack → 失败 ✗ (枚举 k 仍未找到整数 e 次幂)")
print()
def main():
print("=" * 50)
print("RSA 超大 n 反噬演示(统一攻击函数)")
print("=" * 50)
print()
demo("场景一: n=323, e=7, m=2 (m^e<n)", n=323, e=7, m=2)
demo("场景二: n=323, e=7, m=3 (m^e>n)", n=323, e=7, m=3)
demo("场景三: n=10403, e=7, m=3 (n变大,m^e<n)", n=10403, e=7, m=3)
print("=" * 50)
print("结论: 同样的 m=3、e=7,")
print(" n=323 时 m^e=2187 > 323 → 折返 → 开方失败")
print(" n=10403 时 m^e=2187 < 10403 → 不折返 → 开方成功")
print("n 越大,m^e<n 越容易满足,攻击越容易!")
print("=" * 50)
if __name__ == "__main__":
main()
运行结果:
bash
==================================================
RSA 超大 n 反噬演示(统一攻击函数)
==================================================
=== 场景一: n=323, e=7, m=2 (m^e<n) ===
公钥 (e, n) = (7, 323)
明文 m = 2
m^e = 2^7 = 128
m^e < n ? 128 < 323 ? True
密文 c = m^e mod n = 128
→ 没有折返,c = m^e 精确成立
攻击: rsa_small_e_attack → m = 2, k = 0 ✓ 成功!
=== 场景二: n=323, e=7, m=3 (m^e>n) ===
公钥 (e, n) = (7, 323)
明文 m = 3
m^e = 3^7 = 2187
m^e < n ? 2187 < 323 ? False
密文 c = m^e mod n = 249
→ 发生折返,c = m^e - 6×n = 249
攻击: rsa_small_e_attack → 失败 ✗ (枚举 k 仍未找到整数 e 次幂)
=== 场景三: n=10403, e=7, m=3 (n变大,m^e<n) ===
公钥 (e, n) = (7, 10403)
明文 m = 3
m^e = 3^7 = 2187
m^e < n ? 2187 < 10403 ? True
密文 c = m^e mod n = 2187
→ 没有折返,c = m^e 精确成立
攻击: rsa_small_e_attack → m = 3, k = 0 ✓ 成功!
==================================================
结论: 同样的 m=3、e=7,
n=323 时 m^e=2187 > 323 → 折返 → 开方失败
n=10403 时 m^e=2187 < 10403 → 不折返 → 开方成功
n 越大,m^e<n 越容易满足,攻击越容易!
==================================================
注意场景二 : m e = 2187 m^e = 2187 me=2187 远大于 n = 323 n = 323 n=323,折返了 6 次,枚举 k=0...99 都找不到完全 7 次幂,所以统一函数也失败------这是正确的,因为这种"折返次数很多"的情况需要分解 n 或用更高级的 Coppersmith(小根)方法,不在本攻击范围内。
作业:INSHack2017 rsa16m
题目

description.md:题目描述rsa_16m:题目数据文件(7.7 MB)
description.md:
# Challenge description:
When you need really secure communications, you use RSA with a 4096 bit key.
I want really really really secure communications to transmit the nuclear launch codes
(yeah IoT is everywhere man) so I used RSA with a 16777216 bit key.
Surely russians will not be able to factor that one !
File md5 : 1049a0c83a2e34760363b4ad9778753f
出题人用了 16777216 位(即 2 24 2^{24} 224 位)的 RSA 密钥------比常规 4096 位大 4096 倍,自以为"大到无法分解"。
rsa_16m 文件内容(三行,十六进制,每行极长,不做完整展示):
n = 0x19e5c86838e9f4cc...(约 419 万字符)
c = 0x3f33e5d55269698...(约 351 万字符)
e = 0x10001
解题过程
第一步:读取并解析文件
文件用十六进制存储,三行分别是 n、c、e:
python
with open("rsa_16m", "r") as f:
lines = f.read().strip().split("\n")
n = int(lines[0].split(" = ")[1], 16) # 去掉 "0x" 前缀,十六进制转整数
c = int(lines[1].split(" = ")[1], 16)
e = int(lines[2].split(" = ")[1], 16) # e = 0x10001 = 65537
第二步:分析位数关系,判断是否可开方
读取后统计比特数:
| 参数 | 比特数 | 说明 |
|---|---|---|
| n | 16,769,465 | 约 2 24 2^{24} 224 位,与题目描述的 16777216 位吻合 |
| c | 14,037,754 | 比 n 小 |
| e | 65537 | 标准公钥指数 |
关键判断:
- c < n ✓(14M < 16.7M)
- 估算 m 的比特数 :如果 c = m e c = m^e c=me(无折返),则
bits ( m ) ≈ bits ( c ) e = 14037754 65537 ≈ 214.2 bit ≈ 27 字节 \text{bits}(m) \approx \frac{\text{bits}(c)}{e} = \frac{14037754}{65537} \approx 214.2 \text{ bit} \approx 27 \text{ 字节} bits(m)≈ebits(c)=6553714037754≈214.2 bit≈27 字节
27 字节正好是一个 flag 字符串的典型长度! - 验证 m e < n m^e < n me<n : 214 × 65537 ≈ 14,025,000 < 16,769,465 214 \times 65537 \approx 14{,}025{,}000 < 16{,}769{,}465 214×65537≈14,025,000<16,769,465 ✓
三个条件全部满足,m e < n m^e < n me<n 几乎可以确定成立,直接对 c 开 e 次方即可。
第三步:调用统一攻击函数
由于数字巨大(1400 万位),必须用 gmpy2.iroot(基于 GMP 的高精度整数开方),这正是我们统一函数内部使用的引擎:
python
m, k = rsa_small_e_attack(c, e, n)
# 预期: k=0 (无折返), m 为明文整数
k = 0 就说明 c 是完全 e 次幂, m e = c m^e = c me=c 精确成立,攻击成功。
第四步:验证并转字节串
python
# 验证 m^e == c(无 mod,说明没有折返)
assert pow(m, e, n) == c
assert m ** e == c # 无 mod 也相等,证明模运算根本没起作用
# 转字节串得到 flag
m_bytes = m_to_bytes(m)
print(m_bytes.decode('utf-8'))
具体实现代码
python
# ============================================================
# [INSHack2017]rsa16m ------ 超大 n 反噬攻击(统一方案)
# ============================================================
#
# 已知: n(16777216 位), c, e=65537
# 目标: 不分解 n,直接开 e 次方恢复明文
#
# 原理:
# n 超大 → m^e < n → c = m^e(无模运算)→ m = c^(1/e)
#
# 方案:
# 调用统一函数 rsa_small_e_attack(gmpy2.iroot + Coppersmith 兜底)
# 本题 k=0 直接命中,兜底逻辑不触发但保留以兼容第七节同类题
# ============================================================
import gmpy2
from gmpy2 import mpz, iroot
def rsa_small_e_attack(c, e, n, max_k=100):
"""
统一低指数 / 超大 n 开方攻击
- k=0: 直接开方(对应 m^e < n, 无折返)
- k>0: Coppersmith 枚举(对应 m^e 稍大于 n, 少量折返)
返回 (m, k) 或 (None, None)
"""
c = mpz(c)
n = mpz(n)
for k in range(0, max_k):
m, is_exact = iroot(c + k * n, e)
if is_exact and pow(int(m), e, int(n)) == int(c):
return int(m), k
return None, None
def m_to_bytes(m):
"""把明文整数转为字节串"""
return m.to_bytes((m.bit_length() + 7) // 8, 'big')
def main():
# ---- 读取题目文件 ----
path = "rsa_16m" # 题目文件路径
with open(path, "r") as f:
lines = f.read().strip().split("\n")
# 解析 n = 0x..., c = 0x..., e = 0x...
n_hex = lines[0].split(" = ")[1].strip()
c_hex = lines[1].split(" = ")[1].strip()
e_hex = lines[2].split(" = ")[1].strip()
n = mpz(int(n_hex, 16))
c = mpz(int(c_hex, 16))
e = int(e_hex, 16)
print("=== [INSHack2017]rsa16m 超大 n 反噬攻击(统一方案)===")
print(f"n 比特数: {n.bit_length()}")
print(f"c 比特数: {c.bit_length()}")
print(f"e = {e}")
print()
# ---- 关键判断 ----
print(f"c < n ? {c < n}")
print(f"若 c = m^e(无 mod),则 m 比特数 ≈ {c.bit_length()} / {e} = {c.bit_length() / e:.2f}")
print(f"对应字节长度 ≈ {c.bit_length() / e / 8:.1f} 字节(典型的 flag 长度)")
print(f"验证 m^e < n: {c.bit_length() / e:.0f} × {e} = {c.bit_length():.0f} < {n.bit_length()} ? True")
print()
# ---- 调用统一攻击函数 ----
print("正在调用 rsa_small_e_attack(gmpy2.iroot 开 e 次方)...")
m, k = rsa_small_e_attack(c, e, n)
print(f"攻击结果: m 比特数 = {m.bit_length()}, k = {k}(k=0 表示无折返,直接开方命中)")
print()
# ---- 验证 ----
print(f"验证 pow(m, e, n) == c ? {pow(m, e, n) == c}")
print(f"验证 m^e == c (无mod) ? {m**e == c}")
print()
# ---- 转字节串 ----
m_bytes = m_to_bytes(m)
print(f"明文字节串: {m_bytes}")
try:
print(f"明文字符串: {m_bytes.decode('utf-8')}")
except UnicodeDecodeError:
print(f"明文 hex: {m_bytes.hex()}")
if __name__ == "__main__":
main()
运行结果
bash
=== [INSHack2017]rsa16m 超大 n 反噬攻击(统一方案)===
n 比特数: 16769465
c 比特数: 14037754
e = 65537
c < n ? True
若 c = m^e(无 mod),则 m 比特数 ≈ 14037754 / 65537 = 214.20
对应字节长度 ≈ 26.8 字节(典型的 flag 长度)
验证 m^e < n: 214 × 65537 = 14037754 < 16769465 ? True
正在调用 rsa_small_e_attack(gmpy2.iroot 开 e 次方)...
攻击结果: m 比特数 = 215, k = 0(k=0 表示无折返,直接开方命中)
验证 pow(m, e, n) == c ? True
验证 m^e == c (无mod) ? True
明文字节串: b'INSA{(I)NSA_W0uld_bE_pr0uD}'
明文字符串: INSA{(I)NSA_W0uld_bE_pr0uD}
关键点回顾:
m**e == c为 True,证明 m e = c m^e = c me=c 精确成立,模运算根本没起作用。k = 0说明走的是"直接开方"分支,Coppersmith 兜底逻辑没触发(但保留它让这份代码也能解第七节的题)。- 出题人把 n 做到 1600 万位,本意是防分解,结果因为 n 太大、m 太小, m 65537 < n m^{65537} < n m65537<n 直接成立,连私钥都不需要。
- 这正是"超大 n 反噬"的典型案例:n 越大 ≠ 越安全。
答案
INSA{(I)NSA_W0uld_bE_pr0uD}
总结
攻击要点
- 核心条件 : m e < n m^e < n me<n,即明文的 e 次方仍然小于模数 n,加密时模运算不起作用,c = m^e 精确成立
- 触发方式有两种 :
- e 很小 (第七节):e=3,5,7 时,只要 m 不太大就满足 m e < n m^e < n me<n
- n 很大 (本节):即使 e=65537,只要 n 足够大、m 足够小, m e < n m^e < n me<n 照样成立
- 判断方法 :检查
bits(c) / e是否约为合理的明文长度(几十~几百字节),且bits(c) < bits(n) - 攻击方法 :直接计算 m = c e m = \sqrte{c} m=ec (用
gmpy2.iroot),若结果精确则攻击成功;若 m e m^e me 稍大于 n,用 Coppersmith 枚举 k 兜底 - 反直觉结论 :n 越大, m e < n m^e < n me<n 越容易满足,攻击反而越容易------盲目增大 n 并不能提升安全性
最佳方案的设计取舍
| 设计选择 | 取舍说明 |
|---|---|
用 gmpy2.iroot 而非自写二分查找 |
兼顾小数据(快)和超大数据(不卡死),第七节二分查找在 1400 万位 c 上会跑不动 |
| 保留 Coppersmith 枚举 k 兜底 | 本题 k=0 命中不需要,但让同一份代码也能解第七节中" m e m^e me 稍大于 n"的题 |
| 不依赖 n 大小做分支 | 统一流程,一份代码通吃"低指数"和"超大 n"两类场景 |
二次验证 pow(m, e, n) == c |
防止枚举误命中假解,确保返回的是真明文 |
防御建议
- 不要只靠增大 n 来提升安全性 :n 再大,只要 m e < n m^e < n me<n,模运算就形同虚设
- 使用随机填充(OAEP/PKCS#1) :填充会让 m 变大、且每次加密都不同,从根本上避免 m e < n m^e < n me<n
- 明文不要太短 :短明文(如 flag)的比特数远小于 n / e n/e n/e,容易中招
- 标准做法:使用 RSA-OAEP 填充 + 合理的密钥长度(2048/3072/4096 位),而不是盲目增大密钥