Skip to content

Pwnable kr rsa_calculator

复制本地路径 | 在线编辑

首先需要了解程序所用到的全局变量: g_ebufg_pbuf ,分别对于加密内容和原文内容。我们假设加密就相当于没有加密,现在来讲一下加密解密这两个变量是什么内容。

首先要知道输入输出是什么,加密过程中,如果输入 AAAA ,最终输出为 4100000041000000...;解密过程中,如果想要输出 AAAA,输入需要为 4100000041000000..

现在来看这两个变量,还是以 AAAA 为例子。加密过程中,那么 g_pbuf0x41414141g_ebuf0x00000041, 0x00000041, 0x00000041... 解密过程中,如果想要得到 AAAA ,此时 g_pbufg_ebuf 和上述一样。

漏洞点分析

漏洞点 1

首先是 set_key ,模拟的是 RSA 选择秘钥,所以用到的 p 和 q 都是我们自己选的,所以我们就是上帝,即知道公钥也知道私钥。另外它的算法实现有点问题,他有些地方没有设置检测,所以我们可以设置公钥中的 e 和私钥中的 d 都是 1 ,这样相当于就没有加密解密。

漏洞点 2

然后是 RSA_decrypt ,里面有一个格式化输出漏洞,该语句是 printf(g_pbuf) 。根据开头的描述,我们构造好 g_pbuf 内容,可以推出解密函数的输入是什么。当然其实不需要那么麻烦,加入我们想要 g_pbufAAAA ,那我们把这个放到加密函数里面,得到它的输出,再放回解密函数,这样不就相当于最终能构造出想要的 g_pbuf 内容吗。

漏洞点 3

然后就是 RSA_decrypt 存在溢出了,代码如下。它的逻辑是这样的,对于输入 41000000 ,首先读取 41 ,此时 v10='4', v11='1' ,然后利用 sscanfsrc[0] 转换为 0x41 ,之后按照这样的逻辑处理 src[1]..

// len == 1024, data[1024], src[520]
while ( 2 * len > i ) {
    (v10, v11, v12) = (data[i], data[i+1], 0);
    sscanf((__int64)&v10, (__int64)"%02x", (__int64)&src[pos++]);
    i += 2;
}

但是它却让 i < 2*len ,看看最大可以到哪里,最大可以 data[2*1024-1],显然超过 data[1024] 的限制。最大可以进行多少轮?算一下: 2*1024 / 2 = 1024 ,而 src 为 520 字节,所以可以修改 src 后面的内容。其后面内容就是 canary返回地址 了。

漏洞点 4

然后 RSA_encrypt 也有比较离谱的溢出。简化后的代码如下。

// src[1030]
printf("how long is your data?(max=1024) : ");
scanf("%d", &v4);
if ( v4 <= 1024 ) {
    v5 = 0;
    puts("paste your plain text data");
    while ( 1 ) {
        src[v5++] = getchar();
    }
    memcpy(g_pbuf, src, v5);
}

比较离谱的就是它说了不要超过 1024 ,然后它不去检测是否超过 1024 ,你只要说你小于 1024 就行,就离谱...
这个比 RSA_decrypt 更加强的是,它不仅可以修改 src 之后的内容,而且可以修改 g_pbuf 之后的内容( 最后一句 memcpy(g_pbuf, src) ),而且可以修改 g_ebuf 之后的内容( 之后会将 g_pbuf 转换后存储到 g_ebuf 中 )。

其中 g_pbuf 之后内容是 pri ,存储公钥的地方。

其中 g_ebuf 之后内容是 func[0-3] ,也就是选择菜单各项函数的地址

做法一

  1. 首先要设置公钥私钥,这里就不用 1 了,而是让公钥为 58623 (asm(jmp rsp)=58623)
  2. 利用漏洞点 2 的格式化字符串漏洞,可以输出 canary
  3. 利用漏洞点 3 的溢出漏洞,将返回地址改为之前所设置的公钥的地址上,后面是 shellcode
  4. 这样就会执行 jmp rsp ,然后 rsp 之后是我们的 shellcode (注意我们修改(返回地址, 旧 ebp)改变的是 ebp ,而不会改变 esp)

PS: 我觉得可能用 RSA_encrypt 的溢出漏洞也是可以,这点没去实现。

做法二

  1. 首先设置公钥私钥,这里如果设置 1 后面就做不了,设置的是 1898009 ,为什么看后面
  2. 利用漏洞点 4 的溢出漏洞,溢出的是 g_pbuf ,改变后面的 func[0] ,改变方法是输入溢出 29,因为 pow(29, 1898009) = 0x602560 ,所以最后 func[0] = 0x602560
  3. 溢出的同时,保证 g_pbuf 前面是 shellcode ,即此时 g_pbuf = shellcode + xxx + 0x602560
  4. 这个 0x602560 地址就是 g_pbuf 地址,所以溢出后,选择 func[0] ,就会导致执行 g_pbuf ,即 shellcode

做法三

这是我自己的做法,很不好,但是又很意外,因为就只用到了字符串漏洞,别的没用,HAHAHA...

首先看到字符串格式化漏洞,肯定是想要触发写漏洞。首先是改变哪里,选择的是改变 GOT 表,改变 sscanfsystem ,这样如果成功改变,那么我们在选择菜单选择 3 ,然后有一段会执行 sscanf ,这个输入我们可以控制,所以能成功。

例外我做法公钥和私钥都是 1。

常规方式的失败

利用漏洞点 3, 漏洞原因是 g_pbuf ,变量在堆上,所以要想到需要使用跳板。以前做过类似题目,要始终牢记:格式化字符串写漏洞改变的是栈上内容所指向的内容!!

以前方式跳板是使用当前函数的旧 ebp ,因为这个旧 ebp 指向 main 函数的旧 ebp ,所以算出偏移量,可以先通过它将 sscanf@plt 的地址写到了 main 函数的 ebp 上。然后也可以算出 main 函数旧 ebp 的偏移,从而通过它改变了 sscanf@plt 值。

但是这道题目不行,原因找了很久很久!原因处在 set_key() 设置公钥私钥这个方法里,看下面精简的代码。

int16 p; // [rsp+2Ch] [rbp-4h]
int16 q; // [rsp+2Eh] [rbp-2h]

puts("-SET RSA KEY-");
scanf((__int64)"%d", (__int64)&p);
scanf((__int64)"%d", (__int64)&q);

可以看到原来 p 和 q 是 16 bit ,但是它却按照 64 bit 输入,而它们下面就是 ebp ,所以这样回到 main 函数后, ebp 就跑到别的地方了,这样我们第一次写的是改变后的 ebp ,但是第二次算偏移量是原来的 ebp

看例子:如果原来 set_key 函数的旧 ebp0xffffffd4 ,然后因为上面的问题被改成了 0xffffff00 ,所以返回到 main 函数后 ebp 指向 0xffffff00 ,所以进入到 RSA_decrypt 后存储的旧 ebp0xffffff00 。现在我们第一次修改,是将 0xffffff00 地址写入了 sscanf 地址,但是第二次我们算的偏移量是 0xffffffd4 离我们输入的偏移多少,所以就出错了。

寻找新的方法

所以利用 ebp 失败,我就看看栈中有哪些地方可以利用。我发现 RSA_decrypt 函数返回地址下面的那一块(即 old ebp + 0x10) ,好像指向某个固定偏移位置。

所以利用这个我们第一次偏移量修改后,第二次的偏移量也是固定的,这样就可以修改成功了。但是本机成功后,远程失败了。

那个内容是什么时候加上的?是在加载的时候就已经栈上有这个内容了,这也是为什么本机可以,但是远程不行的原因,版本不一样,所以加载的偏移位置不一样。比如本机偏移是 240 ,但是远程偏移不是。

意外的成功

于是我看我本机使用 glibc2.32,那个位置差距是 0x100 ,换成缓冲区溢出格式应该是 240$lln 。我又看 glibc2.29 那个位置差距是 0x90,换成缓冲区溢出格式应该是 238$lln

所以我就想着应该就在 0x50--0x200 之间了,那就爆破吧。每次试一下,失败就重新启动程序。结果我尝试了 238$lln,结果一下就好了...哈哈,太尴尬了[捂脸]

成功后的反思

为什么偏移值是固定的?下面来分析分析

虽然说栈的地址是随机的,但是你不管怎么变,刚开始 loader 肯定是以这个栈的起始值开始执行程序。所以只要知道 loader 的版本,我们也就基本能获得程序每条语句执行时 rsp 的后四位。

loader 执行执行,会开始执行 rsa_calculator 程序的 _start 函数,这个函数也是程序的挂载点。然后它会触发 glibc 的 __libc_start_main 函数,然后这会触发程序自身的 _libc_csu_main 函数,最后执行 main 函数。

这道题执行流程及 rsp 寄存器如下。假设栈的起始地址为 xxxxxx00。
| 函数 | 语句 | 备注 |
|--------------------|-----------------------|--------------------------------------------------|
| loader | 首条语句 | 不用去管 rsp 初始值多少,看 _start 的 rsp 就行 |
| _start | 首条语句 | rsp = r13 = xxxxxx00 + 0x10040 |
| _libc_start_main | call _libc_csu_main | rsp = xxxxxx00 + 0x00970 r13 不变 |
| _libc_csu_main | lea [rsp-0x18], r13 | [xxxxxx00 + 0x00950] = xxxxxx00 + 0x10040 |
| main | .... | rsp = xxxxxx00 + 0x009?? |
| set_key | 输入 p,q 后 | rsp = xxxxxx00 + 0x00900 |
| RSA_decrypt | 开始利用前 | rbp = xxxxxx00 + 0x00940 |

所以我们可以明确知道 [rbp + 0x10] == xxxxxx00 + 0x10040 ,而这个值的偏移值也是固定的。