RSA 复杂题目

2018 Tokyo Western Mixed Cipher

题目给的信息如下所示:

  • 每次交互可以维持的时间长度约为 5 分钟

  • 每次交互中中n是确定的 1024 bit,但是未知, e 为 65537

  • 使用 aes 加密了 flag,密钥和 IV 均不知道

  • 每次密钥是固定的,但是 IV 每次都会随机

  • 可以使用 encrypt 功能随意使用 rsa 和 aes 进行加密,其中每次加密都会对 aes 的 iv 进行随机

  • 可以使用 decrypt 对随意的密文进行解密,但是只能知道最后一个字节是什么

  • 可以使用 print_flag 获取 flag 密文

  • 可以使用 print_key 获取 rsa 加密的 aes 密钥

本题目看似一个题目,实则是 3 个题目,需要分步骤解决。在此之前,我們準備好交互的函數

def get_enc_key(io):
    io.read_until("4: get encrypted keyn")
    io.writeline("4")
    io.read_until("here is encrypted key :)n")
    c=int(io.readline()[:-1],16)
    return c

def encrypt_io(io,p):
    io.read_until("4: get encrypted keyn")
    io.writeline("1")
    io.read_until("input plain text: ")
    io.writeline(p)
    io.read_until("RSA: ")
    rsa_c=int(io.readline()[:-1],16)
    io.read_until("AES: ")
    aes_c=io.readline()[:-1].decode("hex")
    return rsa_c,aes_c

def decrypt_io(io,c):
    io.read_until("4: get encrypted keyn")
    io.writeline("2")
    io.read_until("input hexencoded cipher text: ")
    io.writeline(long_to_bytes(c).encode("hex"))
    io.read_until("RSA: ")
    return io.read_line()[:-1].decode("hex")

GCD attack n

第一步我们需要把没有给出的 n 算出来,因为我们可以利用 encrypt 功能对我们输入的明文 x 进行 rsa 加密,那么可以利用整除的性质算 n

我们可以构造足够多的 x,算出最够多的 x ^ e - c,从而计算最大公约数,得到 n。

可以利用加密进行 check

RSA parity oracle

利用 leak 的的最后一个字节,我们可以进行选择密文攻击,使用 RSA parity oracle 回复 aes 的秘钥

PRNG Predict

这里我们可以解密 flag 的16字节之后的内容了,但是前16个字节没有 IV 是解密不了的。这时我们可以发现,IV 生成使用的随机数使用了 getrandbits,并且我们可以获取到足够多的随机数量,那么我们可以进行 PRNG 的 predict,从而直接获取随机数

这里使用了一个现成的的 java 进行 PRNG 的 Predict

写了一个 python 直接调用 java

EXP

整体攻击代码如下:

2016 ASIS Find the flag

这里我们以 ASIS 2016 线上赛中 Find the flag 为例进行介绍。

文件解压出来,有一个密文,一个公钥,一个 py 脚本。看一下公钥。

这么小的一个 $N$,先分解一下。

再看给的 py 脚本。

逻辑很简单,读取 flag,重复 30 遍为密文。随机取 $p$ 和 $q$,生成一个公钥,写入 pubkey.pem,再用脚本中的 ext_rsa_encrypt 函数进行加密,最后将密文写入 flag.enc

尝试一下解密,提示密文过长,再看加密函数,原来当加密失败时,函数会跳到异常处理,以一定算法重新取更大的 $p$ 和 $q$,直到加密成功。

那么我们只要也写一个相应的解密函数即可。

拿到 flag

SCTF RSA1

这里我们以 SCTF RSA1 为例进行介绍,首先解压压缩包后,得到如下文件

尝试解压缩了一下 level1.zip 现需要密码。然后根据 level1.passwd.enc 可知,应该是我们需要解密这个文件才能得到对应的密码。查看公钥

发现虽然说是 2048 位,但是显然模数没有那么长,尝试分解下,得到

然后就可以构造,并且解密,代码如下

发现不对

这时候就要考虑其他情况了,一般来说现实中实现的 RSA 都不会直接用原生的 RSA,都会加一些填充比如 OAEP,我们这里试试,修改代码

果然如此,得到

得到解压密码。继续,查看 level1 中的公钥

似乎还是不是很大,再次分解,然后试了 factordb 不行,试试 yafu。结果分解出来了。

可以发现这两个数非常相近,可能是 factordb 没有实现这类分解。

继而下面的操作类似于 level0。只是这次是直接解密就好,没啥填充,试了填充反而错

得到密码 fA35ORI11TLoN_Att1Ck_cL0sE_PrI8e_4acTorS。继续下一步,查看公钥

发现私钥 e 和 n 几乎一样大,考虑 d 比较小,使用 Wiener's Attack。得到 d,当然也可以再次验证一遍。

这时我们解密密文,解密代码如下

利用末尾的字符串 wIe6ER1s_1TtA3k_e_t00_larg3 解密压缩包,注意去掉 B。至此全部解密结束,得到 flag。

2018 WCTF RSA

题目基本描述为

这个题目现在已经没有办法在线获取 binary 了,现在得到的 binary 是之前已经下载好的,我们当时需要登录用户的 admin 来下载对应的 generator。

通过简单逆向这个 generator,我们可以发现这个程序是这么工作的

  • 利用用户给定的 license(32 个字节),迭代解密某个固定位置之后的数据,每 32 个字节一组,与密钥相异或得到结果。

  • 密钥的生成方法为

    • $k_1=key$

    • $k_2 =sha256(k_1)$

    • ...

    • $k_n=sha256(k_{n-1})$

其中,固定位置就是在找源文件 generator 中第二次出现 ENCRYPTED 的位置,然后再次偏移 32 个字节。

同时,我们需要确保生成的文件的校验对应的哈希值恰好为指定的值,由于文件最后是一个 exe 文件,所以我们可以认为最后的文件头就是标准的 exe 文件,因此就不需要知道原始的 license 文件,进而我们可以编写 python 脚本生成 exe。

在生成的 exe 中,我们分析出程序的基本流程为

  1. 读取 license

  2. 使用 license 作为 seed 分别生成 pq

  3. 利用 p,q 生成 n,e,d。

其漏洞出现在生成 p,q 的方法上,而且生成 p 和 q 的方法类似。

我们如果仔细分析下生成素数的函数的话,可以看到每个素数都是分为两部分生成的

  1. 生成左半部分 512 位。

  2. 生成右半部分 512 位。

  3. 左右构成 1024 比特位,判断是不是素数,是素数就成功,不是素数,继续生成。

其中生成每部分的方式相同,方式为

只有 v9 会有所变化,但是它的范围却是固定的。

那么,如果我们表示 p,q 为

$p=a*2^{512}+b$

$q=c*2^{512}+d$

那么

$n=pq=ac*2^{1024}+(ad+bc)*2^{512}+bd$

那么

$n \equiv bd \bmod 2^{512}$

而且由于 p 和 q 在生成时,a,b,c,d 均只有 1000000007 种可能性。

进而,我们可以枚举所有的可能性,首先计算出 b 可能的集合为 S,同时我们使用中间相遇攻击,计算

$n/d \equiv b \bmod 2^{512}$

这里由于 b 和 d 都是 p 的尾数,所以一定不会是 2 的倍数,进而必然存在逆元。

这样做虽然可以,然而,我们可以简单算一下存储空间

$64*1000000007 / 1024 / 1024 / 1024=59$

也就是说需要 59 G,太大了,,所以我们仍然需要进一步考虑

$n \equiv bd \bmod 2^{64}$

这样,我们的内存需求瞬间就降到了 8 G左右。我们仍然使用枚举的方法进行运算。

其次,我们不能使用 python,,python 占据空间太大,因此需要使用 c/c++ 编写。

枚举所有可能的 d 计算对应的值 $n/d$ 如果对应的值在集合 S 中,那么我们就可以认为找到了一对合法的 b 和 d,因此我们就可以恢复 p 和 q 的一半。

之后,我们根据

$n-bd=ac*2^{1024}+(ad+bc)*2^{512}$

可以得到

$\frac{n-bd}{2^{512}} = ac*2^{512}+ad+bc$

$\frac{n-bd}{2^{512}} \equiv ad+bc \bmod 2^{512}$

类似地,我们可以计算出 a 和 c,从而我们就可以完全恢复出 p 和 q。

在具体求解的过程中,在求 p 和 q 的一部分时,可以发现因为是模 $2^{64}$,所以可能存在碰撞(但其实就是一个是 p,另外一个是q,恰好对称。)。下面我们就求得了 b 对应的 v9。

注意:这里枚举出来的空间大约占用 11 个 G(包括索引),所以请选择合适的位置。

其实,我们在真正得到 p 或者 q 的一部分后,另外一部分完全可以使用暴力枚举的方式获取,因为计算量几乎都是一样的,最后结果为

最后我们便拿到 flag 了。

详细的利用代码请参见 ctf-challenge 仓库。

相关编译指令,需要链接相关的库。

参考

  • https://upbhack.de/posts/wctf-2018-writeup-rsa/

Last updated