湖湘杯2016-简单的RSA:从部分信息到完整攻破

admin 2025-12-26 01:46:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 湖湘杯2016RSA题给出原始n,e,d却用新模数new_n加密flag;作者先利用ed≡1modφ(n)随机分解得p,q,再发现destory函数仅用900位随机数与2048位质数异或致高1148位不变,遂以Coppersmith定理在56%已知比特下多项式时间恢复new_p,new_q,重算私钥解密获flag。文章详述RSA基础、概率分解、Coppersmith原理与Sage实现,指出短密钥XOR长数据的信息泄露陷阱并给出安全建议。 综合评分: 95 文章分类: CTF,漏洞分析,密码学,安全建设,实战经验


cover_image

湖湘杯2016-简单的RSA:从部分信息到完整攻破

原创

破镜安全

破镜安全

2025年12月25日 08:00 四川

湖湘杯2016-简单的RSA:从部分信息到完整攻破

0x01 题目概览

题目名称:简单的RSA(湖湘杯2016)

题目给出了一个Python脚本和三组关键数据:

  • 原始RSA参数:n, e, d
  • 修改后的模数:new_n
  • 加密的flag:enc_flag

乍一看,题目给出了私钥d,似乎可以直接解密。但仔细观察会发现,flag是用new_n加密的,而我们手头的d是基于原始n的私钥。这就是问题的关键所在。

0x02 题目代码分析

让我们先来看看题目的核心代码:

def destory(x, num):
    while True:
        dt = getRandomNBitInteger(num)
        r = x ^ dt
        if isPrime(r):
            return r

p = getPrime(2048)
q = getPrime(2048)
n = p*q
e = 0x10001
phi_n = (p-1)*(q-1)
d = invmod(e, phi_n)

# 破坏原始质因数
new_p = destory(p, 900)
new_q = destory(q, 900)
new_n = new_p * new_q
c = pow(flag, e, new_n)

代码的执行流程如下:

  1. 生成2048位的质数p和q
  2. 计算标准RSA参数n, e, d并输出
  3. 调用destory函数”破坏”p和q,得到new_p和new_q
  4. 用new_n = new_p * new_q作为新模数加密flag

题目的巧妙之处在于:给了我们原始的私钥d,但加密用的是新的模数new_n。要解密flag,我们必须找到new_n对应的私钥,而这需要我们能够分解new_n。

0x03 destory函数的秘密

destory函数是这道题的核心,让我们深入分析它的行为:

def destory(x, num):
    while True:
        dt = getRandomNBitInteger(num)  # 生成900位随机数
        r = x ^ dt                       # 与x进行异或
        if isPrime(r):                   # 如果结果是质数则返回
            return r

函数接收一个质数x(即p或q)和位数num(这里是900),然后:

  1. 生成一个900位的随机整数dt
  2. 计算r = x XOR dt
  3. 如果r是质数,返回r;否则重新生成dt

异或操作的比特级分析

这里的关键是理解异或(XOR)操作的性质。对于二进制数,异或操作是按位进行的:

  • 0 XOR 0 = 0
  • 0 XOR 1 = 1
  • 1 XOR 0 = 1
  • 1 XOR 1 = 0

当我们将2048位的p与900位的dt进行异或时,会发生什么?

p  (2048位): [高1148位][低900位]
dt (900位):  [000...000][低900位]

由于dt只有900位,它的高位(第901位及以上)都是0。在异或操作中:

  • p的高1148位与0异或,结果保持不变
  • p的低900位与dt的低900位异或,结果会发生变化

结论:new_p和p只在低900位不同,高1148位完全相同!

这个发现至关重要。我们可以通过p的高1148位来推测new_p的高1148位,因为它们是相同的。

0x04 RSA密码学基础回顾

在展开攻击之前,让我们快速回顾RSA的数学基础。

RSA加密系统

密钥生成

  1. 选择两个大质数p和q
  2. 计算n = p × q
  3. 计算欧拉函数φ(n) = (p-1)(q-1)
  4. 选择公钥指数e,满足gcd(e, φ(n)) = 1
  5. 计算私钥d = e^(-1) mod φ(n)

加密与解密

  • 加密:c = m^e mod n
  • 解密:m = c^d mod n

正确性证明: 根据欧拉定理,对于gcd(m, n) = 1,有: m^φ(n) ≡ 1 (mod n)

因为ed ≡ 1 (mod φ(n)),所以ed = 1 + kφ(n),因此: c^d = (m^e)^d = m^(ed) = m^(1+kφ(n)) = m · (m^φ(n))^k ≡ m · 1^k = m (mod n)

RSA的安全性

RSA的安全性基于大整数分解的困难性。如果攻击者能够将n分解为p和q,就可以计算φ(n),进而求出私钥d。

对于2048位的RSA模数,目前没有已知的高效分解算法。但如果我们能获得关于p或q的部分信息,情况就会完全不同。

0x05 攻击策略构建

通过分析,我们发现了三个可利用的突破点:

突破点1:已知(n, e, d)可以分解n

虽然题目没有直接给出p和q,但给了我们完整的RSA参数(n, e, d)。这看似没什么用,但实际上存在一个经典的攻击方法。

数学原理

由于d是e在模φ(n)下的逆元,我们知道: ed ≡ 1 (mod φ(n))

这意味着: ed = 1 + kφ(n) = 1 + k(p-1)(q-1)

其中k是某个正整数(通常很小)。

设k’ = ed – 1,则k’是φ(n)的倍数。我们可以将k’写成: k’ = 2^t × s,其中s是奇数

利用这个性质,结合随机性测试,我们可以找到n的因子。

算法步骤

  1. 计算k’ = ed – 1
  2. 分解k’ = 2^t × s(s为奇数)
  3. 随机选择g ∈ [2, n-1]
  4. 计算x = g^s mod n
  5. 计算gcd(x-1, n)
  6. 如果结果既不是1也不是n,则找到了因子

为什么这个方法有效?

这个算法的理论基础来自于Miller-Rabin素性测试的思想。对于合数n = pq:

由于ed ≡ 1 (mod φ(n)),我们有: g^(ed-1) ≡ 1 (mod n)

根据中国剩余定理,这等价于: g^(ed-1) ≡ 1 (mod p) 且 g^(ed-1) ≡ 1 (mod q)

当我们计算g^s mod n时(其中s = k’/2^t),有较大概率使得:

  • g^s ≡ 1 (mod p) 但 g^s ≢ 1 (mod q),或者
  • g^s ≢ 1 (mod p) 但 g^s ≡ 1 (mod q)

此时gcd(g^s – 1, n)会恰好等于p或q。

理论分析表明,对于随机选择的g,这个算法的成功概率至少为1/2。

突破点2:已知new_p的高1148位

从destory函数的分析,我们知道new_p和p只在低900位不同。这意味着:

已知信息:new_p的高1148位 未知信息:new_p的低900位 已知比例:1148/2048 = 56.05%

当我们知道一个数的56%时,是否有办法恢复完整的数?答案是:有,使用Coppersmith定理。

突破点3:Coppersmith部分信息攻击

Coppersmith定理(1996年)是密码分析中的强大工具,它能够在已知部分信息的情况下恢复完整信息。

定理陈述(简化版)

设N是一个整数,f(x)是一个模N的单变量多项式。如果存在一个小根x0(|x0| < X),使得: f(x0) ≡ 0 (mod p)

其中p是N的一个未知因子,且p ≥ N^β,那么我们可以在多项式时间内找到x0。

理论条件:X < N^(β^2)

应用到本题

在我们的场景中:

  • N = new_n = new_p × new_q

  • 我们知道new_p ≈ N^0.5(因为new_p和new_q大小相近)

  • 我们可以将new_p表示为:new_p = p_high × 2^900 + x

  • p_high是已知的(从p的高1148位得到)

  • x是未知的(0 ≤ x < 2^900)

构造多项式: f(x) = p_high × 2^900 + x

这个多项式在模new_p下有根:x0 = new_p的低900位

检验Coppersmith条件:

  • β ≈ 0.5(因为new_p ≈ sqrt(new_n))
  • X = 2^900
  • N^(β^2) = new_n^0.25 ≈ 2^1024
  • X = 2^900 < 2^1024 = N^(β^2) ✓

条件满足!理论上可以使用Coppersmith方法恢复new_p。

Coppersmith算法的实现

Coppersmith算法基于LLL格基约化。虽然理论很深奥,但SageMath提供了现成的实现:small_roots方法。

使用时需要注意几个参数:

  • X:未知量的上界
  • beta:因子的大小,p ≈ N^beta
  • epsilon:调整参数,影响算法精度

在实践中,beta的理论值是0.5,但由于LLL是启发式算法,可能需要略微调小(如0.49)以提高成功率。

0x06 实战解题

有了理论基础,现在开始实际操作。

第一步:从(n, e, d)恢复质因数p和q

首先实现分解算法:

# 给定的参数
n = 0x73cec712124b33c0294e01eb52e8c3cd2fe9ddbcbf457b3b950360063dfae42c...
e = 0x10001
d = 0x37f2646fd190ad1e9f95c50d97cf4590a21e1c766bfd382cafafa2bb41442ce9...

# 计算 ed - 1
k = e * d - 1

# 分解为 2^t * s 的形式
t = 0
while k % 2 == 0:
&nbsp; &nbsp; k = k // 2
&nbsp; &nbsp; t = t + 1

print(f"ed - 1 = 2^{t} * k")
# 输出:ed - 1 = 2^6 * k

# 随机选择基,寻找因子
found = False
while not found:
&nbsp; &nbsp; g = randint(2, n-1)
&nbsp; &nbsp; x = power_mod(g, int(k), n)

&nbsp; &nbsp; p = gcd(x - 1, n)
&nbsp; &nbsp; if 1 < p < n:
&nbsp; &nbsp; &nbsp; &nbsp; found = True
&nbsp; &nbsp; &nbsp; &nbsp; q = n // p
&nbsp; &nbsp; &nbsp; &nbsp; print(f"成功分解n!")
&nbsp; &nbsp; &nbsp; &nbsp; break

# 验证
assert p * q == n

运行结果

ed - 1 = 2^6 * k
经过1-2次尝试,成功分解n!
验证:p * q == n ✓

成功!我们恢复了原始的质因数p和q。虽然题目没有直接给出,但通过已知的(n, e, d),我们成功计算出来了。

为什么这么快?

这个算法每次尝试的成功概率约为1/2,因此期望只需要2次左右的尝试。这比暴力分解n(需要2^1024次运算)要高效得多。

第二步:使用Coppersmith方法恢复new_p和new_q

现在我们有了p,可以利用它来攻击new_p。

关键观察

p的二进制表示: &nbsp; &nbsp;[高1148位][低900位]
new_p的二进制表示: [高1148位][低900位']
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^^^^^^^^ &nbsp;这部分相同!

构造攻击

pbits = 2048 &nbsp;# p是2048位质数
kbits = 900 &nbsp; # 被修改的位数

# 获取p的高位(清除低900位)
p_high = p >> kbits

print(f"已知的高位:{pbits - kbits} = 1148位")
print(f"未知的低位:{kbits} = 900位")
print(f"已知比例:{(pbits - kbits) / pbits * 100:.2f}%")

# 构造多项式
# new_p = p_high * 2^900 + x,其中x是未知的低900位
PR.<x> = PolynomialRing(Zmod(new_n))
f = p_high * 2^kbits + x

# 使用Coppersmith方法
X = 2^kbits &nbsp;# x的上界
beta = 0.49 &nbsp;# 因子大小参数
epsilon = 0.05 &nbsp;# 精度参数

roots = f.small_roots(X=X, beta=beta, epsilon=epsilon)

if roots:
&nbsp; &nbsp; print(f"找到 {len(roots)} 个根")

&nbsp; &nbsp; for root in roots:
&nbsp; &nbsp; &nbsp; &nbsp; new_p_candidate = int(p_high * 2^kbits + root)

&nbsp; &nbsp; &nbsp; &nbsp; # 验证是否是new_n的因子
&nbsp; &nbsp; &nbsp; &nbsp; if new_n % new_p_candidate == 0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new_p = new_p_candidate
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new_q = new_n // new_p
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"成功恢复new_p和new_q!")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break

# 验证
assert new_p * new_q == new_n

运行结果

已知的高位:1148位
未知的低位:900位
已知比例:56.05%
尝试 beta = 0.5 ... 失败
尝试 beta = 0.49 ... 成功!
找到 1 个根
成功恢复new_p和new_q!
验证:new_p * new_q == new_n ✓

关于beta参数的说明

理论上,当new_p ≈ sqrt(new_n)时,beta应该设为0.5。但在实践中,LLL算法是启发式的,可能需要稍微调小beta值以提高成功率。

这就像是给算法留一些”安全边界”。beta=0.49意味着我们假设因子稍微小一点,这样算法会更加”努力”地寻找,成功率会提高。

计算复杂度

small_roots方法的复杂度主要取决于LLL格基约化,对于我们的参数,运行时间通常在数十秒到几分钟之间(取决于硬件)。这比直接分解2048位RSA模数(几乎不可能)要可行得多。

第三步:计算新私钥并解密flag

有了new_p和new_q,剩下的就是标准RSA解密流程:

enc_flag = 0x3b0487110b2d61683fc9e65717d6431281c25ceda38915c9c2ff1601...

# 计算新的欧拉函数
phi_new = (new_p - 1) * (new_q - 1)

# 计算新的私钥
d_new = inverse_mod(e, phi_new)

# 验证私钥正确性
assert (e * d_new) % phi_new == 1
print("私钥计算正确!")

# 解密flag
flag_int = power_mod(enc_flag, d_new, new_n)
flag = long_to_bytes(int(flag_int))

print(f"Flag: {flag}")

运行结果

私钥计算正确!
Flag: b'flag{6b1ff461dc15e5476cafe887189178e3924dad84}'

成功解密!

0x07 深入理解Coppersmith定理

Coppersmith定理是这道题的核心技术,值得我们深入探讨。

定理的直观理解

想象你在玩一个猜数字游戏:

  • 有一个2048位的秘密数字
  • 我告诉你这个数字的前1148位
  • 你需要猜出完整的数字

如果只是暴力穷举,你需要尝试2^900 ≈ 10^271次。这在计算上是不可行的。

但如果这个秘密数字恰好是某个已知大数N的因子,情况就不同了。Coppersmith定理告诉我们,在这种特殊情况下,我们可以在多项式时间内找到答案。

定理的数学基础

Coppersmith定理建立在以下数学工具之上:

1. Howgrave-Graham定理

如果多项式h(x)满足:

  • h(x0) = 0在整数上成立(不是模运算)
  • ||h(xX)|| < N / sqrt(d),其中d是多项式的次数

那么h(x0) ≡ 0 (mod N)也成立。

2. LLL格基约化算法

LLL算法可以找到格中的短向量。通过巧妙地构造格,我们可以找到满足Howgrave-Graham条件的多项式。

3. 构造格的艺术

Coppersmith的天才之处在于如何构造格。对于我们的多项式f(x) = p_high * 2^900 + x,我们构造一系列相关的多项式,形成一个格。LLL算法在这个格中找到短向量,这些短向量对应满足条件的多项式。

为什么需要beta参数?

beta参数表示因子p的大小:p ≈ N^beta

  • 如果p很大(接近N),那么分解N很容易(试除法就行)
  • 如果p很小,那么X可以更大(可以恢复更多未知位)
  • beta=0.5表示两个因子大小相近,这是最困难的情况

Coppersmith定理给出的界限是:X < N^(β^2)

在我们的例子中:

  • β ≈ 0.5
  • N^(β^2) = N^0.25 ≈ 2^1024
  • X = 2^900 < 2^1024 ✓

理论上可行,实践中也确实成功了。

epsilon参数的作用

epsilon是一个小的调整参数,用于在理论界和实际性能之间权衡:

  • 更小的epsilon:更接近理论界限,但计算时间更长
  • 更大的epsilon:偏离理论界限更多,但成功率可能下降

通常设置为0.01-0.05之间。

0x08 异或操作的密码学陷阱

这道题的另一个教训是:不要低估简单操作的安全影响。

异或操作的特性

异或(XOR)在密码学中有广泛应用,它有很多优秀的性质:

  • 可逆性:a ⊕ b ⊕ b = a
  • 速度快:直接的位运算
  • 均匀性:对于随机输入,输出也是随机的

但它也有一个致命缺陷:局部性

局部性导致的信息泄露

当用一个短的随机数与长数异或时,只有参与运算的位会改变:

原始数据: [A][B][C][D][E][F][G][H]
异或掩码: [0][0][0][0][0][X][Y][Z]
结果: &nbsp; &nbsp; [A][B][C][D][E][F'][G'][H']
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^^^^^^^^^^^^^^^^ 这部分不变!

在本题中:

  • 原始数据:2048位的质数p
  • 异或掩码:900位的随机数
  • 结果:高1148位完全不变

这种信息泄露正是Coppersmith攻击的关键。

正确的密钥变换方式

如果题目想要真正”销毁”p,应该怎么做?

方案1:完全重新生成

def&nbsp;destory(x, num):
&nbsp; &nbsp;&nbsp;# 忽略x,直接生成新的质数
&nbsp; &nbsp;&nbsp;return&nbsp;getPrime(2048)

方案2:使用哈希函数

def&nbsp;destory(x, num):
&nbsp; &nbsp;&nbsp;# 使用密码学哈希函数
&nbsp; &nbsp; seed = sha256(str(x).encode()).digest()
&nbsp; &nbsp;&nbsp;# 基于seed生成新的质数
&nbsp; &nbsp;&nbsp;return&nbsp;generate_prime_from_seed(seed,&nbsp;2048)

方案3:充分混淆

def&nbsp;destory(x, num):
&nbsp; &nbsp;&nbsp;# 确保修改所有位
&nbsp; &nbsp; dt = getRandomNBitInteger(2048) &nbsp;# 与x等长
&nbsp; &nbsp; r = x ^ dt
&nbsp; &nbsp;&nbsp;if&nbsp;isPrime(r):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;r

关键是:任何密钥派生或变换操作,都必须充分混淆所有位,不能留下可预测的部分。

0x09 完整解题代码

将以上所有步骤整合,完整的解题脚本如下:

#!/usr/bin/env sage
# -*- coding: utf-8 -*-

from Crypto.Util.number import long_to_bytes

# 题目给定的数据
n = 0x73cec712124b33c0294e01eb52e8c3cd2fe9ddbcbf457b3b950360063dfae42cbbe9855bd986bcfea0948fadfb252f5e2ff3c982ff47afb6596a496636f1fc5ecfe9f5db7620b23fe9e30d230aa9299ab9a78bfb5e0630fd1149259b2b2104ea65d2e27b89785e4bf01d0594d9f94575cbcc3383f63c5aabe4d5b48eb761cce3ab21689b3f3155b5f15efee240d5ac149318ff80bd72a75fccdc57402aa197b472d98758019df8e9edb31bda82967dc66bcad845df824775eeb66ee304664d7929e8405122f9b0a5406887729dbe9760eb62dd7018087723c07c07082d1d51035fb211a9fc6d8fb5b3ee5bb91af5e3d0b89addce289041a5683a1fe7dc06a3bae283062ba3febdd987b5ac9b9a8ae4b8b02b804accc0a413bb144680fd8d0d8d8bebe176e5a9121f7653c31ede984d234ccce50e688f7048a0bfdfc84004c006ae912c4d4e514c200883e8dabaeb4bf57a5f628eb4bd2e6688d9b7688bc3eed4ce03831be5044dedbd5fddc43a3274b26c990a0e444fcf4a607de59c4906dfd1ea111920c38b4a365c5838e9cf1a22b146aa7afbc6e2e29ebe35aee4bf4d2fbe186c0f359c71f80b8f6298ad38619168d5986a857f558017c546d6df896c690896601aabd48398e957b77ef0e15d6cda339050b74cda3328c34c889306d089efc95ff467a4a775d3e104642cd9819f002b5db8c5f39b4e8d1a83007276b8a0b7

e = 0x10001

d = 0x37f2646fd190ad1e9f95c50d97cf4590a21e1c766bfd382cafafa2bb41442ce9839aac47944e288de6abfec1b17be4675f492a47f3e600f85a3823df9299d32f46c8a372f39d961f9471914e257f55cf1ef3d7878783fc34b61e1d61da332879c8d9597b0f0dac988916ac349e1d73b615cfbfef778ceeccee4f63dc32b1b7d7213c9199b6acb1d8a5141c94d777a29b89f8e0aea457788eaa9ca43626a24c74ebab355c89e3747626d4899745d148500c91416c782f2b30c9332f5cd32a4d3144d2a407ce9acc00f99dc619d425586285350cff734cdba9d4fad636d7fcbabfa382965005d8343e36ffe72604e557bae5044435ad990b6dca6d922e64387cee4abc0574d2eeeb42dda977be1e1064f6a6b00e78db75a7bf8e6e0c2ac3c6a52b2e6670cad3c907d990e53a7a311ef7b097c7644ff6f2bf96573e47d33031eeabf22620bdbc254a8fff8b0fa6f90d320c45e8d9094c26401f78560e16f77d6e09bc219c9849f1b68dc84ed36eb0cec7df16c4672c16a6b704ef2185ce04539f08688b72d48f9491e55be0095ee74f9622e8ae6835381e2efcd520cd02d1eeeb09bc11ab75bc903053102bc92718f2bf8691561a40026e53e950674f712aaef8cc69360df54c1af8b1f4aef997371b8a108e9b193a51002fe8d61f3991153e7ebd9593d68cd03b2f252c3d9d7a5bf802dcd150bd86028bd3b07cb415b767d716c1

new_n = 0x73cec712124b33c0294e01eb52e8c3cd2fe9ddbcbf457b3b950360063dfae42cbbe9855bd986bcfea0948fadfb252f5e2ff3c982ff47afb6596a496636f1fc5ecfe9f5db7620b23fe9e30d230aa9299ab9a78bfb5e0630fd1149259b2b2104ea65d2e27b89785e4bf01d0594d9f94575cbcc3383f63c5aabe4d5b48eb761cce3ab21689b3f3155b5f15efee240d5ac11cee2acbd019de7c06f607ea618b5cd735b5a6972d2b446a12ff58cf8314822fa5ea09d0963acd00441b2a1b37aca01d7f39052927db98a0bd5ca1c49a7ad67923e3aac30ecd33cc8b4b30a40cdb3acc721ee5da53a02977cee959affe672a668525eb78df96af0a14f4ac04fab68efa8eabe9535e1064a5fc2ff7cac9520210311db0c3bf91101bc55a67a81e4f69364c724ee6ad6bdc301df642c9392e9befa4ff0d65481adb6feac251cd207044587da9710809700246cb3c63e659a97249f5e7418568e37db2fb2c1115e719d6682bb2e89b4e23d40ba4c532f289e10e0b89a5647c486a09b9e376844171b229d74f871004d4945a702a391a04ac704f43809e972891e6ab33b3c0f03f0b6f9ae005b26be6e647a1865c727277423f59a595187ffbfea13501e23b6b57ef115eaa6febcb207a3112628652a39578847241c33989e84607b0f683b30ddf773348b07360b063d9120a397809591ca18a04cd32ad9cbfe0494ed3ae8d2c5b43fdb51cb

enc_flag = 0x3b0487110b2d61683fc9e65717d6431281c25ceda38915c9c2ff16013066e8cbe9d5f6c1f05988333d96e861f1b216f85d537aa70f603b3d20487a18c5e0d3a03556be88f984f508803f4bb25300149e805204c91f7aea038e545cf588a29bcb20113f81b16d9aacdbd820aa48615a5227ab2f329423da0a254c8f318dff4a7b75cf48e5ac18abc2975fee111ee56297fd7778b49320f6427227f21c08391d7429c49b399c366db38467a339229048e50735dff071bf89dbbab22997071a6b5a16935f5b65032f2cefb9c670cddd1470f6bf16d7c46d0fff22b82b2c1d867f4faa60e5daaad35a15ac0413a17beb5371a06f22c646336ae5e5738cfc5e997860056fb8bf1e02f27031ecd2b7983df510c4cdebbc644ea49e99e2376b89a90be8c89e4995c6689cc39d01fcc8ffef416e49684c0a01fdffab4613cb8eddbfa9e14d9b2daca6ad57c4e440a049d04b915ecfd64095c549bb9d22fb502a740a7373c1961c8cb7a58604bcbca3e5a97e28d5f498061db7cfbf07085ec84f47856e88992ab08870cbdb050431260861d0bcc9bbdf9a4164b8581f1f67e86ab854db1769418e300d73cf902509c76969473d0ee1b70c3b24085942fa9782507b359ffe049b18832610b4555b9bfe8eeae14cec53ab21881475f81069a6a33d6e38e3825eeb13874da8f0c91160712090a092286ea3838d88020e459f795783ad099367

print("[*] Step 1: 从 (n, e, d) 恢复质因数 p 和 q")
print("="*60)

# 利用 e*d - 1 = k*phi(n)
k = e * d - 1

# 分解为 2^t * k
t = 0
while k % 2 == 0:
&nbsp; &nbsp; k = k // 2
&nbsp; &nbsp; t = t + 1

print(f"[+] ed - 1 = 2^{t} * k")

# 随机基方法寻找因子
found = False
attempt = 0
while not found:
&nbsp; &nbsp; attempt += 1
&nbsp; &nbsp; g = randint(2, n-1)
&nbsp; &nbsp; x = power_mod(g, int(k), n)

&nbsp; &nbsp; p = gcd(x - 1, n)
&nbsp; &nbsp; if 1 < p < n:
&nbsp; &nbsp; &nbsp; &nbsp; found = True
&nbsp; &nbsp; &nbsp; &nbsp; q = n // p
&nbsp; &nbsp; &nbsp; &nbsp; print(f"[+] 经过 {attempt} 次尝试,成功分解 n!")
&nbsp; &nbsp; &nbsp; &nbsp; print(f"[+] 验证: p * q == n ? {p * q == n}")
&nbsp; &nbsp; &nbsp; &nbsp; break

print()
print("[*] Step 2: 使用 Coppersmith 方法恢复 new_p 和 new_q")
print("="*60)

pbits = 2048
kbits = 900

# 获取p的高位
p_high = p >> kbits

print(f"[+] p 的位数: {pbits}")
print(f"[+] 被修改的位数: {kbits}")
print(f"[+] 已知的高位部分: {pbits - kbits} 位")
print(f"[+] 已知比例: {(pbits - kbits) / pbits * 100:.2f}%")

# 构造多项式
PR.<x> = PolynomialRing(Zmod(new_n))
f = p_high * 2^kbits + x

X = 2^kbits
new_p = None
new_q = None

# 尝试不同的beta值
for beta_val in [0.5, 0.49, 0.48, 0.4]:
&nbsp; &nbsp; print(f"[*] 尝试 beta = {beta_val}")
&nbsp; &nbsp; try:
&nbsp; &nbsp; &nbsp; &nbsp; roots = f.small_roots(X=X, beta=beta_val, epsilon=0.05)

&nbsp; &nbsp; &nbsp; &nbsp; if roots:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"[+] 找到 {len(roots)} 个可能的根")

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for root in roots:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new_p_candidate = int(p_high * 2^kbits + root)

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if new_n % new_p_candidate == 0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new_p = new_p_candidate
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new_q = new_n // new_p
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"[+] 成功恢复 new_p!")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"[+] 验证: new_p * new_q == new_n ? {new_p * new_q == new_n}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if new_p:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; except Exception as ex:
&nbsp; &nbsp; &nbsp; &nbsp; print(f"[-] beta={beta_val} 失败")
&nbsp; &nbsp; &nbsp; &nbsp; continue

if new_p and new_q:
&nbsp; &nbsp; print()
&nbsp; &nbsp; print("[*] Step 3: 计算新的私钥 d' 并解密")
&nbsp; &nbsp; print("="*60)

&nbsp; &nbsp; # 计算新的欧拉函数值
&nbsp; &nbsp; phi_new = (new_p - 1) * (new_q - 1)

&nbsp; &nbsp; # 计算新的私钥
&nbsp; &nbsp; d_new = inverse_mod(e, phi_new)

&nbsp; &nbsp; # 验证
&nbsp; &nbsp; assert (e * d_new) % phi_new == 1
&nbsp; &nbsp; print(f"[+] 验证: e * d' ≡ 1 (mod phi(new_n)) ? True")

&nbsp; &nbsp; # 解密flag
&nbsp; &nbsp; flag_int = power_mod(enc_flag, d_new, new_n)
&nbsp; &nbsp; flag = long_to_bytes(int(flag_int))

&nbsp; &nbsp; print(f"[+] Flag: {flag}")
&nbsp; &nbsp; print()
&nbsp; &nbsp; print("="*60)
&nbsp; &nbsp; print("[*] 解题完成!")
else:
&nbsp; &nbsp; print("[-] 恢复失败")

运行脚本:

sage solve.sage

输出:

[*] Step 1: 从 (n, e, d) 恢复质因数 p 和 q
============================================================
[+] ed - 1 = 2^6 * k
[+] 经过 1 次尝试,成功分解 n!
[+] 验证: p * q == n ? True

[*] Step 2: 使用 Coppersmith 方法恢复 new_p 和 new_q
============================================================
[+] p 的位数: 2048
[+] 被修改的位数: 900
[+] 已知的高位部分: 1148 位
[+] 已知比例: 56.05%
[*] 尝试 beta = 0.5
[*] 尝试 beta = 0.49
[+] 找到 1 个可能的根
[+] 成功恢复 new_p!
[+] 验证: new_p * new_q == new_n ? True

[*] Step 3: 计算新的私钥 d' 并解密
============================================================
[+] 验证: e * d' ≡ 1 (mod phi(new_n)) ? True
[+] Flag: b'flag{6b1ff461dc15e5476cafe887189178e3924dad84}'

============================================================
[*] 解题完成!

0x10 知识点总结

通过这道题,我们学到了以下关键知识点:

1. 从已知(n,e,d)分解RSA模数

核心思想:利用ed ≡ 1 (mod φ(n))的关系,通过随机性测试找到n的因子。

适用场景

  • 已知完整的RSA公私钥对
  • 需要恢复质因数p和q

算法复杂度:期望O(log n)次尝试,每次尝试成功概率≥1/2

实际应用

  • CTF题目中常见
  • 实际系统中,私钥泄露通常意味着系统已完全被攻破

2. Coppersmith部分信息攻击

核心思想:当知道RSA质因数的部分比特时,可以使用格基约化算法恢复完整的因数。

理论条件

  • 已知信息比例需要超过某个阈值(与beta相关)
  • 对于beta=0.5(两个因子大小相近),需要知道约25%以上的比特

实现工具:SageMath的small_roots方法

参数调优

  • beta:因子大小,理论值vs实际值
  • epsilon:精度与性能的权衡
  • X:未知量的上界

实际应用

  • 侧信道攻击(如缓存攻击)可能泄露部分密钥比特
  • 错误的密钥派生方案
  • 密钥重用场景

3. 异或操作的密码学特性

优点

  • 可逆性
  • 计算速度快
  • 某些情况下的随机性

缺点

  • 局部性:只影响参与运算的位
  • 可能导致信息泄露

正确使用

  • 一次一密(OTP):密钥长度≥明文长度
  • 流密码:使用密码学安全的伪随机数生成器
  • 不要用短密钥XOR长数据

错误使用(本题):

  • 用900位随机数XOR 2048位数据
  • 导致高位信息泄露

4. RSA密钥管理的安全实践

密钥生成

  • 使用密码学安全的随机数生成器
  • 确保p和q的质量(不要太接近,不要有小因子等)

密钥存储

  • 私钥d必须严格保密
  • 使用硬件安全模块(HSM)存储关键密钥

密钥更新

  • 定期更换密钥
  • 旧密钥与新密钥必须完全独立
  • 不要重用任何密钥材料

密钥销毁

  • 彻底覆写内存中的密钥数据
  • 不要通过简单的位运算”修改”密钥

0x11 安全建议

基于本题的分析,我们可以得出以下安全建议:

对于密码系统设计者

  1. 不要低估部分信息泄露的危害
  • 即使只泄露密钥的一部分,也可能导致完全破解
  • Coppersmith定理表明,泄露25-50%的密钥比特就足以恢复完整密钥
  1. 密钥派生必须使用密码学哈希
  • 不要使用XOR、移位等简单运算
  • 使用HKDF、PBKDF2等标准密钥派生函数
  1. 密钥更新应该完全独立
  • 新密钥和旧密钥之间不应有任何数学关系
  • 最好的方式是完全重新生成
  1. 实施纵深防御
  • 不要依赖单一的安全假设
  • 即使某个参数泄露,系统仍应保持安全

对于CTF选手和安全研究员

  1. 熟悉经典攻击技术
  • 从已知(n,e,d)分解n
  • Coppersmith部分信息攻击
  • 侧信道攻击
  • 格基约化应用
  1. 掌握必要的工具
  • SageMath:强大的数学工具
  • Python密码学库:PyCrypto、cryptography
  • 在线资源:factordb.com、CyberChef等
  1. 培养密码分析思维
  • 寻找非预期的信息泄露
  • 分析算法的数学性质
  • 思考”如果我知道X,能否推导出Y?”
  1. 理解理论与实践的差距
  • 理论安全≠实际安全
  • 注意实现细节(如beta参数的调整)
  • 多实践,积累经验

0x12 拓展思考

如果修改的位数更多会怎样?

假设destory函数修改的不是900位,而是1100位:

  • 已知位数:2048 – 1100 = 948位
  • 已知比例:948/2048 ≈ 46.3%

此时Coppersmith攻击可能失败,因为已知信息不足。攻击难度会显著增加。

如果只给了e和new_n呢?

如果题目不给d,只给(e, new_n, enc_flag),这道题会变得困难得多:

  • 无法直接分解原始n得到p和q
  • 无法获得new_p的高位信息
  • 可能需要寻找其他突破点(如小公钥指数攻击、低加密指数广播攻击等)

Coppersmith定理的其他应用

Coppersmith定理不仅可以攻击RSA,还有很多其他应用:

  • 攻击低加密指数的RSA(e=3时的Hastad广播攻击)
  • 分解特殊形式的大整数
  • 求解模方程
  • 密码分析中的各种场景

0x13 总结

这道题虽然名为”简单的RSA”,但实际上涵盖了多个深入的密码学知识点:

  1. RSA的数学基础:欧拉定理、模运算、质因数分解
  2. 经典攻击技术:从(n,e,d)恢复因子的概率算法
  3. 现代密码分析:Coppersmith定理及其应用
  4. 实现细节:参数调优、算法实现

通过层层分析,我们成功地:

  1. 从给定的(n, e, d)恢复了原始质因数p和q
  2. 利用destory函数的漏洞(XOR的局部性)获得new_p的部分信息
  3. 应用Coppersmith定理恢复完整的new_p和new_q
  4. 计算新私钥并解密flag

这个过程充分展示了现代密码分析的思路:观察、分析、建模、攻击。

密码学的魅力在于,看似简单的操作可能隐藏着深刻的数学原理,而看似安全的设计可能存在致命的漏洞。只有深入理解密码学的数学基础,才能真正评估系统的安全性。

最后,引用Bruce Schneier的名言: “Anyone can invent an encryption algorithm they themselves can’t break; it’s much harder to invent one that no one else can break.”

(任何人都能发明一个自己无法破解的加密算法;但要发明一个别人也无法破解的算法,就困难得多了。)

这道题完美地诠释了这句话的含义。


Flag: flag{6b1ff461dc15e5476cafe887189178e3924dad84}


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:破镜安全 破镜安全《湖湘杯2016-简单的RSA:从部分信息到完整攻破》

评论:0   参与:  0