【CTF-CRYPTO-教学-RSA】第八节:超大模数n反噬攻击,已知n、e、c求m(n超大,m的e次方仍小于n,c可直接开e次方得到结果)

超大 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 的位数关系:

  1. 如果 c < n(密文比模数小,这是必然的)
  2. 且 c 的比特数 / e ≈ 一个合理的明文长度(比如 100~1000 比特,对应几十~几百字节的 flag)
  3. 那么 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 次方"恢复明文 。因此本节采用一份统一代码,同时覆盖两类场景:

  1. 开方引擎用 gmpy2.iroot:基于 GMP 的高精度整数开方,对小数据(n 几千位)和超大数据(n 1600 万位)都快,避免第七节纯 Python 二分查找在大数上卡死的问题。
  2. 保留 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 让这份代码也能解第七节的题。
  3. 不依赖 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

题目

https://ctf2.dasctf.com/dashboard/practice/b9bbb32f-f186-458f-b90b-12440c0f6aea?tab=challenges&challenge=b9f0c382-d38c-4d65-a38a-becdcdae1b69

  • 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 标准公钥指数

关键判断:

  1. c < n ✓(14M < 16.7M)
  2. 估算 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 字符串的典型长度!
  3. 验证 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}

总结

攻击要点

  1. 核心条件 : m e < n m^e < n me<n,即明文的 e 次方仍然小于模数 n,加密时模运算不起作用,c = m^e 精确成立
  2. 触发方式有两种 :
    • 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 照样成立
  3. 判断方法 :检查 bits(c) / e 是否约为合理的明文长度(几十~几百字节),且 bits(c) < bits(n)
  4. 攻击方法 :直接计算 m = c e m = \sqrte{c} m=ec (用 gmpy2.iroot),若结果精确则攻击成功;若 m e m^e me 稍大于 n,用 Coppersmith 枚举 k 兜底
  5. 反直觉结论 :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 位),而不是盲目增大密钥
相关推荐
其实防守也摸鱼1 天前
CTF Web 6 题全解 Writeup
linux·sql·学习·算法·安全·ctf·xss
hengdonghui2 天前
Writeup 4 N1BOOK 第一章 Web 入门 · 粗心的小李
ctf·.git泄露
hengdonghui4 天前
Writeup 4 N1BOOK afr_2
ctf·目录穿越
print_Hyon5 天前
【CTF-CRYPTO-教学-RSA】第十三节:知道e、p和q的变形表达式,解密c
ctf
print_Hyon5 天前
【CTF-CRYPTO-教学-RSA】第七节:低加密指数攻击,已知n和e求c(e很小,m的e次方比n小,c可直接开方得到结果)
ctf
hengdonghui8 天前
Writeup 4 N1BOOK afr_1
ctf·伪协议·任意文件读取
hengdonghui8 天前
Writeup 4 N1BOOK 常见的搜集
ctf·信息搜集
hengdonghui9 天前
Writeup 4 N1BOOK SQL注入-1
ctf·sql注入·union·字符型注入
hengdonghui10 天前
Writeup 4 红帽杯 2021 WebsiteManger
web·ctf·ssrf·sql布尔盲注