选择明文攻击

适用情况:

对输入的任意明文服务器返回 RSA 加密结果。

可以通过选择明文攻击来获取 nn

原理

首先发送 22,让服务器进行加密,返回 c2=2emodnc_2 = 2^e \bmod n

继续发送 222^2,让服务器进行加密,返回 c4=4emodnc_4 = 4^e \bmod n

不妨设 2e=a+bn2^e = a + bn,有:

c2=(a+bn)modn=ac4=(a2+2abn+b2n2)modn=a2modn\begin{aligned} c_2 & = (a + bn) \bmod n = a \\ c_4 & = (a^2 + 2abn + b^2n^2) \bmod n = a^2 \bmod n \end{aligned}

所以 c22c_2^2c4c_4nn 同余

即:

c22c4=knc_2^2 - c_4 = kn

同理:

c23c8=tnc_2^3 - c_8 = tn

一般来说 a2a^2nn 大,所以 kk 不为 00

同理还可以构造更多的例子取他们的公因数,来更加确定地找 nn

代码

虽然有繁琐,但经过实践,nn的准确性很高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import gmpy2

def get_n():
nset = []
c2 = server_encode(2)
c4 = server_encode(4)
c8 = server_encode(8)
nset.append(c2 * c2 - c4)
nset.append(c2 * c2 * c2 - c8)
c3 = server_encode(3)
c9 = server_encode(9)
c27 = server_encode(27)
nset.append(c3 * c3 - c9)
nset.append(c3 * c3 * c3 - c27)
c5 = server_encode(5)
c25 = server_encode(25)
c125 = server_encode(125)
nset.append(c5 * c5 - c25)
nset.append(c5 * c5 * c5 - c125)
n = nset[0]
for x in nset:
n = gmpy2.gcd(x, n)
while n % 2 == 0:
n /= 2
while n % 3 == 0:
n /= 3
while n % 5 == 0:
n /= 5
print 'n =', n
return n

选择密文攻击

适用情况:

可以构造任意密文并获得对应明文。

通过选择密文攻击获得特定的明文。

原理

假设服务器创建了密文 C=MemodnC = M^e \bmod n,并且把 cc 发送给用户,用户可以发送任意密文服务器返回解密后的明文。可以通过下面方法求出 MM

  1. 选择任意的 XXnn 互素。
  2. 计算 Y=CXemodnY = CX^e \bmod n
  3. 由于可以进行选择密文攻击,那么可以求得 YY 对应的解密结果 Z=YdZ = Y^d
  4. Z=Yd=(CXe)d=CdX=MXmodnZ = Y^d = (CX^e)^d = C^dX = MX \bmod n。由于 XXnn 互素,很容易求得相应的逆元,进而可以得到 MM

代码

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import *

def get_M():
X = getPrime(5)
Y = (c * (X ** e)) % n
Z = server_decode(Y)
i = 0
while True:
M = (n * i + Z) / X
if 'flag' in long_to_bytes(M):
print long_to_bytes(M)
break

RSA Parity Oracle

适用情况:

可以选择密文并泄露最低位。

服务器会对一个给定的密文进行解密,并且会检查解密的明文的奇偶性,并根据奇偶性返回相应的值,比如 11 表示奇数,00 表示偶数。那么给定一个加密后的密文,我们只需要 log2nlog_2 n 次就可以知道这个密文对应的明文消息。

原理

假设:

C=MemodnC = M^e \bmod n

第一次时,构造密文:

C×2emodn=(2M)emodnC \times 2^e \bmod n = (2M)^e \bmod n

服务器解密后得到:

2Mmodn2M \bmod n

这里:

  • 2M2M 是偶数,它的幂次也是偶数。
  • nn 是奇数,因为它是由两个大素数相乘得到。

那么:

  • 服务器返回奇数,即 2Mmodn2M \bmod n 为奇数,则说明 2M>n2M > n,且减去了奇数个 nn,又因为 2M<2n2M < 2n,因此减去了一个 nn,即 n2M<n\frac{n}{2} ≤ M < n
  • 服务器返回偶数,则 2M<n2M < n,即 0M<n20 \leq M < \frac{n}{2}

第二次时,构造密文 C×4emodnC \times 4^e \bmod n,服务器解密后得到 4Mmodn4M \bmod n,判断其奇偶性可以知道 mmn4\frac{n}{4} 的大小关系。

以此类推,第 ii 次时,构造密文 C×2iemodnC \times 2^{ie} \bmod n,服务器解密后得到 2iMmodn2^i M \bmod n,判断其奇偶性可以知道 mmn2i\frac{n}{2^i} 的大小关系。

所以我们就有了一个二分算法,可以在 log2nlog_2 n 次内将 mm 的范围逼近到一个足够狭窄的空间。

假设服务器返回 b,那么可以归纳为:

1
2
3
4
5
6
7
L = 0
R = n
for i in range(1024):
if b:
L = (L+R) / 2
else:
R = (L+R) / 2

代码

由于此处有大量整除运算,所以最好用 decimal 库进行精确计算,否则最后结果很可能会出错。decimal.getcontext().prec 用来设定精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Util.number import *
import decimal

def get_flag():
k = n.bit_length()
decimal.getcontext().prec = k
L = decimal.Decimal(0)
R = decimal.Decimal(int(n))
for i in range(k):
c = (c * pow(2, e, n)) % n
recv = server_decode(c)
if recv == 1:
L = (L + R) / 2
else:
R = (L + R) / 2
print long_to_bytes(int((R)))

RSA Byte Oracle

适用情况:

可以选择密文并泄露最后一个字节。

服务器会对一个给定的密文进行解密,并且会给出明文的最后一个字节。那么给定一个加密后的密文,我们只需要 log256nlog_{256}n 次就可以知道这个密文对应的明文消息。

原理

这是 RSA Parity Oracle 的扩展,既然可以泄露出最后一个字节,那么按道理获取密文对应明文的次数应该可以减少。

假设:

C=MemodnC = M^e \bmod n

第一次时,构造密文:

C×256e=(256M)emodnC \times 256^e = (256M)^e \bmod n

服务器解密后得到:

256Mmodn256M \bmod n

这里:

  • 256M256M 是偶数。
  • nn 是奇数,因为它是由两个大素数相乘得到。

由于 M=CdmodnM = C^d \bmod n,所以 MM 小于 nn,那么:

256Mmodn=256Mkn,(k<256)256M \bmod n = 256M − kn, \quad (k < 256)

而且对于两个不同的 k1k_1k2k_2,有:

256Mk1nmod256256Mk2nmod256256M − k_1 n \bmod 256 \neq 256M − k_2 n \bmod 256

同时 256Mkn256M − kn 的最后一个字节其实就是 kn−kn 在模 256256 的情况下获取的。

那么,我们可以首先枚举出 02550 - 255 情况下的最后一个字节,构造一个 kk 和最后一个字节的映射表 map。

当服务器返回最后一个字节,那么我们可以根据上述构造的映射表得知 kk,即减去了 kknn,即 kn256M(k+1)nkn \leq 256M \leq (k+1)n

以此类推,第 ii 次时,构造密文:

C×256ieC \times 256^{ie}

服务器解密后得到:

256i×Mmodn=256i×Mkn256^i \times M \bmod n = 256^i \times M − kn

即:

kn256i×M<n+knkn256iM<(k+1)n256ikn \leq 256^i \times M < n+kn \\ \frac{kn}{256^i} \leq M < \frac{(k+1)n}{256^i}

那么,可以设初始范围为:

Mn[L0,R0],(L0=0,R0=1)\frac{M}{n} \in [L_0,R_0], \quad (L_0 = 0, R_0 = 1)

第i次迭代后:

Li=Li1+ki256iRi=Li1+ki+1256iL_i = L_{i-1} + \frac{k_i}{256^i} \\ R_i = L_{i-1} + \frac{k_i + 1}{256^i}

还有最后一个问题,就是迭代的次数。由于明文的最后一个字节可以直接由服务器解密得到,那么只需要限定 MM 的范围至:

MmaxMmin<256M_{max} - M_{min} < 256

即:

n×(RiLi)=n256i<256n \times (R_i - L_i) = \frac{n}{256^i} < 256

由于 nn 是由 512 bit 的 ppqq 相乘得到,所以 n<21024n < 2^{1024},那么由上式可得:i128i \geq 128

一般对于这样的 Oracle,最多需要 log2bitsn\log_{2^{bits}} n 次迭代即可确定明文,其中 bits 为泄露的明文 bit 数。RSA Parity Oracle 为 1,RSA Byte Oracle 为 8。

综上,假设服务器返回了b,那么可以归纳为:

1
2
3
4
5
6
7
L = 0
R = 1
for i in range(128):
k = mab[b]
L = L + k / 256**(i+1)
R = L + (k+1)/ 256**(i+1)
M = L * n

代码

如果不知道 ee 但服务器提供任意明文加密服务,可以让服务器加密 256256,得到 256emodn256^e \bmod n
由于有大量除法运算,为保证精度,将中间过程用 Fraction 库保存为分数。
最后一字节的数据不准确要减掉,从服务器返回精确的最后一字节数据。
其实只要求出 L 下限即可,无需求出 R 上限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import *
from fractions import Fraction

def get_flag():
map = {}
for i in range(0, 256):
map[-n * i % 256] = i

cipher256 = server_encode(256)
backup = c

L = Fraction(0, 1)
R = Fraction(1, 1)
for i in range(128):
c = c * cipher256 % n
b = server_decode(c)
k = map[b]
L, R = L + Fraction(k, 256**(i+1)), L + Fraction(k+1, 256**(i+1))
m = int(L * n)
print long_to_bytes(m - m % 256 + server_decode(backup))

参考文章

https://ctf-wiki.github.io/ctf-wiki/crypto/asymmetric/rsa/rsa_chosen_plain_cipher/