IO_FILE Summary

_IO_FILE与 vtable

_IO_FILE

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中(stdin, stdout, stderr在libc.so的数据段里,并且会在程序开始时被创建)。我们常定义一个指向 FILE 结构的指针来接收这个返回值。

FILE 结构定义在 libio.h 中,关键源码如下:

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
};
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
  /* Wide character stream stuff.  */
  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;

  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

在libc上的stdin, stdout, stderr的symbol分别如下:

_IO_2_1_stdin_
_IO_2_1_stdout_
_IO_2_1_stderr_

所有的_IO_FILE对象由 _chain彼此连接形成了一个单链表,表头指针是 _IO_list_all

_IO_list_all初始状态是指向 _IO_2_1_stderr_ 的。

vtable

常规文件流的vtable是一种_IO_jump_t类型的指针,在vtable中保存有许多IO函数会调用的函数指针。

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
};

在libc中定义的vtable有_IO_file_jumps, _IO_str_jumps, _IO_cookie_jumps等。

_IO_FILE_plus

_IO_FILE_plus是对 _IO_FILE和vtable的整合

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

在x86中,file和vtable的偏移是0x94,在x64中的偏移一般是0xd8

(x64下_IO_FILE_plus结构体内的偏移:)

0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable

IO函数的调用链

分析

这里先用fread和fwrite为例来分析一下

fread

fread的函数原型:

size_t fread(void *buffer, size_t size, size_t count, FILE *stream)

buffer:存数据的缓冲区

size:指定每次读的数据项长度

count:数据项的个数

stream:目标文件流

返回值:成功读取到缓冲区的数据项个数

fread指向的函数名为_IO_fread,它会调用 _IO_sgetn

bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);

而_IO_sgetn会调用 _IO_XSGETN

_IO_size_t
_IO_sgetn (fp, data, n)
     _IO_FILE *fp;
     void *data;
     _IO_size_t n;
{
  return _IO_XSGETN (fp, data, n);
}

他实际上就是vtable中的函数指针,默认指向_IO_file_xsgetn。

检查如下:

  if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

fwrite

fwrite的函数原型:

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)

buffer:要写入的地址

size:要写入的每个数据项大小

count:写入多少个数据项

返回值:成功写入的数据项个数

fwrite指向的函数名为_IO_fwrite,它会调用 _IO_sputn

written = _IO_sputn (fp, (const char *) buf, request);

_IO_sputn会调用 _IO_XSPUTN,指向 _IO_new_file_xsputn,同时也会调用vtable中的 _IO_OVERFLOW。

 /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)

_IO_OVERFLOW默认指向 _IO_new_file_overflow

if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);
  if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;

在_IO_new_file_overflow中进一步调用系统函数write

常见的调用链

  • printf/puts -> _IO_file_xsputn

  • scanf/gets -> _IO_file_xsgetn

  • fwrite -> _IO_new_file_xputn

  • fread -> _IO_file_xgetn

  • fclose -> _IO_FILE_FINISH

还有经常用于攻击的一个调用链:

  • exit -> _IO_flush_all_lockp -> _IO_overflow

攻击手段

伪造vtable

libc-2.23

程序使用fopen打开的文件流是会存在堆上的,位于libc.so数据段的vtable不可写,但是我们可以在可控内存(比如堆)上伪造一个vtable,然后让fp+0xd8指向那个伪造的vtable,在vtable上布置好相应会调用的函数指针从而劫持程序流程。

一个比较典型的例题是HCTF2018 the_end,可以参考我这篇博客的分析

https://kotoriseed.github.io/post/HCTF2018the_end(exit%20hook)/

原题环境下是libc-2.23,漏洞是任意修改5个byte,由于可写入的内容有限,所以直接在_IO_2_1_stdin_ (或者另外两个也行)附近来伪造vtable就好了(因为这样可以直接改低地址几位,将就现有的值),

2个byte用来修改_IO_2_1_stdin_->vtable,3个byte用来修改 _setbuf指针,使其指向one_gadget的地址。

libc-2.24

很不幸的是,在libc-2.24中,加入了对vtable劫持的检测,会在调用之前检查vtable地址的合法性

 uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
 uintptr_t ptr = (uintptr_t) vtable;
 uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
 if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();

即,vtable需要在 __start__libc_IO_vtables__stop__libc_IO_vtables之间,不然就会调用 _IO_vtable_check()进行进一步的检查。

所以之前伪造vtable的手段很难实现。

但是_IO_str_jumps这个vtable是可以通过检查的,我们可以利用它的__IO_str_finish或者__IO_str_overflow来达到攻击的目的。

但是老版本的libc中并没有_IO_str_jumps这个symbol,没办法直接定位,我们就需要用到一些相关函数,例如__IO_str_underflow来辅助了,

def get_IO_str_jumps:
	IO_file_jumps = libcbase + libc.symbols['_IO_file_jumps']
	IO_str_underflow_offset = libc.symbols['_IO_str_underflow']
	for temp in libc.search(p64(IO_str_underflow_offset)):
        IO_str_jumps = libcbase + (temp - 0x20) #_IO_str_underflow-0x20就是_IO_str_jumps
        if IO_str_jumps > IO_file_jumps:
            return IO_str_jumps

有了__IO_str_jumps之后,就可以选择以下方式来getshell了

_IO_str_jumps -> _IO_str_finish

void _IO_str_finish (FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & 1))
    ((void (*)(void))fp + 0xE8 ) (fp->_IO_buf_base); // call qword ptr [fp+E8h]
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}

可以看到,_IO_str_finish以fp->_IO_buf_base为参数调用了fp+0xE8处的函数

需要满足:

  1. fp->_IO_buf_base != 0
  2. fp->_flags为偶数

这条链是exit来触发的,所以还需要满足_IO_flush_all_lockp的检查:

1. fp->_IO_write_ptr > fp-> _IO_write_base
2. fp-> _mode <= 0

所以我们构造起来就非常简单:

1. fp->_flag = 0
2. fp->_IO_write_base = 0
3. fp->_IO_write_ptr = 1
4. fp->_IO_buf_base = str_binsh_addr
5. fp->_mode = 0
6. fp+0xE8 = system_addr

然后将目标文件流的vtable指向_IO_str_jumps-0x8来调用 _IO_str_finish(因为原本要调用的是 _IO_str_overflow,减去0x8即可指向 _IO_str_finish)

_IO_str_jumps -> _IO_str_overflow

v2 = fp->_flags;
if ( fp->_flags & 8 )
  return (unsigned int)-(a2 != -1);
if ( (fp->_flags & 0xC00) == 0x400 )
{
  v4 = fp->_IO_read_ptr;
  v11 = fp->_IO_read_end;
  BYTE1(v2) |= 8u;
  LODWORD(fp->_flags) = v2;
  fp->_IO_write_ptr = v4;
  fp->_IO_read_ptr = v11;
}
else
{
  v4 = fp->_IO_write_ptr;
}
v6 = (char *)fp->_IO_buf_end - (char *)fp->_IO_buf_base
if ( (char *)v4 - (char *)fp->_IO_write_base >= v6 + (a2 == -1) )
{
  if ( v2 & 1 )
    return 0xFFFFFFFFLL;
  v7 = 2 * v6 + 100;
  if ( v6 > v7 )
    return 0xFFFFFFFFLL;
  v8 = ((__int64 (__fastcall *)(unsigned __int64)) fp + 0xE0)(2 * v6 + 100); // call
  v9 = v8;
  .........
}

这一条链子就比finish那条分析起来稍微麻烦了一点,他最终是以

2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100为参数调用fp+0xE0处的函数。

绕过条件需要满足:

1). fp->_flags & 8 == 0, (fp-> _flags & 0xC00) == 0x400, fp-> _flags & 1 = 0
2). fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base

所以我们需要构造

_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_base = 0
_IO_buf_end = (binsh_in_libc_addr -100) / 2 

_mode = -1

vtable = _IO_str_jumps - 0x18

FSOP

主要利用的就是前面提到过的 __IO_flush_all_lockp -> _IO_overflow这个调用链。

因为__IO_flush_all_lockp不需要我们手动调用,在以下情况

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

会自动调用_IO_list_all中的每一个文件流的_IO_overflow

int
_IO_flush_all_lockp (int do_lock)
{
  ...
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
       ...
       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }
        ...
  }
}

需要满足的条件也很简单:

1. fp->_mode <= 0
2. fp->_IO_write_ptr > fp->_IO_write_base

我们一般是将伪造的vtable中的_IO_overflow指向one_gadget或者system。

指向system的时候,因为默认是传自己的fp作为参数,所以需要将fp的_flag那个位置改为'/bin/sh\x00',要注意的是,有些对 _flag有要求的函数就不能这么改。

(若没法使用fsop触发利用链,可以参考house of kiwi,走__malloc_assert

经典例题就是house of orange

参考例题:2021安洵杯-ezheap

FSOP的高版本技巧

限于篇幅原因,这里只粗略介绍

libc-2.31

house of pig

适用于存在calloc的题目,用largebin attack配合tcache stashing unlink attack将某个hook放入tcache bin的头部,再largebin attack_IO_list_all在堆上伪造好一个_IO_file_plus,vtable换为_IO_str_jumps,依赖_IO_str_overflow中的mallocmemcpy劫持hook并getshell。如果开了沙箱,就劫持malloc hook到setcontext+61,走orw。

house of kiwi

有些题目没有直接调用exit之类的函数,导致fsop不容易触发。house of kiwi利用的是__malloc_assert中的fflush(stderr),他能够稳定调用stderr的虚表(_IO_file_jumps)的sync。并且在执行sync之前,rdx的值稳定指向_IO_helper_jumps。所以劫持sync到setcontext+61,然后改_IO_helper_jumps+0xA0为orw的rop的地址(劫持rsp),_IO_helper_jumps+0xA8为一个ret的地址(劫持rcx),即可完成栈迁移orw。

至于__malloc_assert的触发,通常可以改top chunk为一个较小值(注意prev_inuse bit置0),然后malloc一个超过top chunk大小的值即可。

libc-2.34

house of emma

由于高版本libc的诸多限制,新的堆利用思路将倾向于向某个可控地址写入某些值而getshell,而非利用任意地址申请的方式getshell。house of emma的思路是用一次largebin attack打libc上的stderr指针(当存在puts等函数时,也可以劫持stdout的vtable,那样调的就是_IO_file_xputn的偏移),伪造好一个_IO_cookie_file结构体,vtable布置为_IO_cookie_jumps的某个偏移量(sync的),然后利用__malloc_assertfflush(stderr)来触发_IO_cookie_read(也可以是_IO_cookie_write_IO_cookie_seek_IO_cookie_close),由于_IO_cookie_read用到了一个函数指针,这个指针在_IO_cookie_file中可以伪造,所以可以很方便的去控制流程orw或者直接getshell。不过有个 PTR_DEMANGLE (read_cb)的加密,用到了TLS结构体的__pointer_chk_guard,所以还需要一次largebin attack来把这个值改为已知堆地址。不过也正是这个原因,无法走exit的流程来fsop,因为exit在调用到我们布置好的fsop之前,还有一个地方要用到PTR_DEMANGLE这个宏来加密,这个时候就会出问题。

libc-2.35

house of apple

在只有一次任意写堆地址的情况下,house of apple是非常灵活的攻击手法。但是由于依赖_IO_flush_all_lockp对所有文件流调用_IO_overflow,所以前提是程序显式调用了exit函数,或能从main函数返回__libc_start_main。流程如下:

第一次largebin attack打_IO_list_all,伪造的_IO_FILE的vtable选_IO_wstrn_jumps,劫持_wide_data字段到需要任意写已知值的地方。调用_IO_wstrn_overflow的时候,首先会将该IO_FILE转化为_wstrnfile,然后将他的overflow_buf(或附近距离它一定偏移的值)赋值给之前劫持的_wide_data_IO_read_base, _IO_read_ptr, _IO_read_end, _IO_write_base(其实就是目标地址前0x20的值)。overflow_buf相对于_IO_FILE结构体的偏移为0xf0。(需要注意的是,在调用_IO_wstrn_overflow时,会调用_IO_wsetb,它可能会free掉原IO_FILE的_wide_data->_IO_buf_base,如果这个值不为0,就会出现异常。但是这个free是可以通过将IO_FILE的_flags2字段设置为8来绕过的)。

如此一来,就相当于在只有一次largebin attack的前提下,劫持了_IO_list_all并额外达到了一次任意地址写已知值,还能利用第一个IO_FILE的_chains字段去利用其它布置好的IO_FILE来getshell或走orw。

参考例题:2022强网杯-house of cat