0ctf 2019 pwn题的第一道
感觉0ctf好难啊,一道都没有做出来,而且docker一直在update,环境一直有问题,这篇是根据大佬的writeup学习的过程,想记录下来。
题目描述
这道题有三个功能,add、delete和一个特殊的函数sub_165A(),可以创建、释放任意id的task,在创建时题目使用AES算法的CBC模式对数据块进行加密,其中数据块的大小自己看的时候没有找到漏洞,用户自己定义,0x0<size<0x1000。在函数sub_165A()创建了一个新的线程,对指定id的task的数据块进行加解密操作。
题目开的保护情况:
在整个逻辑中有一个结构体,这里命名为task,具体结构如下:
1 | 0x0 data_addr #用户定义的数据块的地址,长度为0x8 |
加解密过程
该题目使用OpenSSL中的EVP库完成加解密过程,具体可以参考这篇博客,程序中涉及到了以下函数:
1 | EVP_CIPHER_CTX_new() #创建EVP_CIPHER_CTX |
整个加解密过程涉及到两个基本的数据结构,EVP_CIPHER与EVP_CIPHER_CTX,具体定义如下:
1 | #EVP_CIPHER_CTX |
在每一个task中,程序为其分配了0xb0的堆块以存储EVP_CIPHER_CTX结构:
1 | #EVP_CIPHER |
在每一个task中,程序为其分配了0x110的堆块以存储该结构,其实在EVP_CIPHER_CTX结构的第一个成员中,我们也可以得到EVP_CIPHER结构体的地址:
可以看到,EVP_CIPHER是EVP_CIPHER_CTX结构体的成员,在EVP_CIPHER中,有很多指向函数的虚表指针,在漏洞利用过程中就是伪造了上面两个结构体,将EVP_CIPHER结构体中的函数指针指向我们的one_gadget。
程序漏洞
自己在做的时候没有找到漏洞,看了别人的writeup才知道是条件竞争的漏洞,虽然条件竞争在操作系统里学过,但第一次遇到这种漏洞和利用方式。在第三个功能“Go”中调用了函数sub_165A(),该函数中创建了一个新的线程,对对应task_id的数据块进行加密操作,然后输出,但是在进行这些操作之前有一个sleep(2),这就导致条件竞争漏洞,在线程sleep的时间可以释放这个task_id的数据块,然后再malloc,可以进行地址的泄露和伪造数据块。伪造EVP_CIPHER是EVP_CIPHER_CTX结构体,将EVP_CIPHER结构体中的函数指针指向我们的one_gadget。
利用过程
初始分配4个task:
1 | key = "1"*0x20 |
利用条件竞争漏洞,进入“Go”功能3,在加密线程sleep(2)期间释放task_0和task_2,重新分配一个与task_0大小相同的task,这里是task_1,一直输入至数据块大小,不输入数据,这时指定进行加密操作的task_0经历释放至unsortedbin中又被malloc为task_1,但未进行数据填充,堆块的fd和bk中分别存储着main_arena+88的地址和heap_addr。至于为什么还要分配0x510的堆块,这里没有什么作用,不分配也可以。
1 | p.sendlineafter("Choice: ", "3") |
由于输出是加密之后的数据块,我们只需用相同的key和iv进行解密即可得到libc和heap的地址,这里有一个问题是程序密文的输出长度不定,有时不是16的整数倍,然后exp会报错,没有找到原因,多试几次就可以了,后续调试中为了避免这个问题我就把地址随机化给关了,然后在exp中把libc_base和heap_leak写死。
1 | #task 0 is task 1,fd is heap_addr,bk is main_arena+88 |
另外在调试时发现如果在加密线程中attach时gdb的符号表已经没有了,看不了堆的分布情况,这个时候使用命令“add-symbol-file”手动添加符号表时,gdb显示符号表添加成功了,但是main_arena的地址错误,只有偏移,没有加libc的基址,至今没有找到解决方法。但是可以在线程结束之后再attach就可以了,但这样就是看不了线程中堆的分布。
接下来就是伪造两个结构体,在task_7中伪造了EVP_CIPHER_CTX结构体,第一个成员为EVP_CIPHER的地址,指向task_8,在task_8中伪造了EVP_CIPHER结构体,将结构体中前2个虚表指针修改为one_gadget,原来这两个指针指向的函数分别完成初始化初始化密钥和加/解密的功能,所以在“Go”功能3中完成加解密操作时一定会调用这两个函数。
1 | add(4, 1, key, iv, 0x10, "4"*0x10) #0x20 |
最后再利用题目的条件竞争漏洞,对task_4进行加密操作,在加密线程sleep(2)里释放task_4和task_5,再申请task_4,data_size为0x70,堆块的大小是0x80,大小正好是task结构体的大小,task_4其实伪造了一个task的结构,在堆的分布上说,正好伪造了task_4的task结构。因为释放了task_4和task_5,相应task结构体的堆块被释放,进入tcache 0x80中,这时趁着线程sleep又分配一个task_4,新的task_4的task结构体需要一个0x80的堆块,分配了原来task_5释放的task结构体相应的堆,task_4大小又是0x80,正好分配了原来task_4释放的task结构体相应的堆。在新的task_4中偏移0x58处是原来task_4的EVP_CIPHER_CTX结构体地址,我们将其修改为前面伪造的fake EVP_CIPHER_CTX,也就是task_7的数据块地址。这样在线程休眠结束后对原来的task_4进行加密时根据fake EVP_CIPHER_CTX找到fake EVP_CIPHER,调用fake EVP_CIPHER虚表指针,即one_gadget,即可获得shell。
1 | p.sendlineafter("Choice: ", "3") |
参考
https://e3pem.github.io/2019/03/27/0ctf-2019/0ctf2019-zerotask/