IO_FILE 浅析

·约 8 分钟

目录
  1. 前言
  2. _IO_FILE
  3. _IO_FILE_plus
  4. _IO_jump_t

前言

glibc 2.34 之后 __malloc_hook / __free_hook 被移除,利用面随之转向 IO_FILE。 这篇把 _IO_FILE 的结构和几个关键字段整理一遍,方便回头查。

_IO_FILE

struct _IO_FILE
{
  int _flags;                  /* 高位字为_IO_MAGIC,剩余的部分是flag */
  char *_IO_read_ptr;          /* 正在使用的input缓冲区的input地址 */
  char *_IO_read_end;          /* input缓冲区的结束地址 */
  char *_IO_read_base;         /* input缓冲区的基址 */
  char *_IO_write_base;        /* output缓冲区的基址 */
  char *_IO_write_ptr;         /*  指向还没输出的那个字节 */
  char *_IO_write_end;         /* output缓冲区的结束地址 */
  char *_IO_buf_base;          /* input和output缓冲区的基址 */
  char *_IO_buf_end;           /* input和output缓冲区的结束地址 */

  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;     /* 存放着一个单链表,用于串联所有的file stream */

  int _fileno;                 /* 与文件相关的文件描述符 */
  int _flags2;
  __off_t _old_offset;         /* This used to be _offset but it's too small.  */

                               /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;  /* 存放虚表(virtual table)的偏移 */
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
  __off64_t _offset;           /* 存放当前文件的偏移 */
                               /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  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)];
};

结构体字段很多,但真正在利用里反复出现的是这几个:

  • _flags:高 32 位是 _IO_MAGIC(0xFBAD0000),低位是一组状态位(_IO_CURRENTLY_PUTTING、_IO_NO_WRITES 之类),决定接下来的读写走哪条分支。FSOP 的第一步往往就是把它改成一个「看起来正常」的值。
  • _IO_read_ptr / _IO_read_end / _IO_read_base:输入缓冲区的当前指针、末尾和基址。
  • _IO_write_base / _IO_write_ptr / _IO_write_end:输出缓冲区三件套。fwrite、puts 落到底层时写的是 [_IO_write_base, _IO_write_ptr) 这段,把 base 调小、ptr 调大就能顺带泄露内存。
  • _IO_buf_base / _IO_buf_end:缓冲区边界,分配/扩容缓冲时用。
  • _chain:把所有 FILE 结构串成一条链表,_IO_list_all 指向表头。FSOP 就是把伪造的结构挂到这条链上,等退出流程遍历链表时触发。
  • _lock、_mode、_wide_data:加锁、宽字符流相关,走 _IO_wfile_jumps 那条路时会用到。

_IO_FILE_plus

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

这里的 FILE 就是 _IO_FILE,只是用 typedef 换了个名字:

#ifndef __FILE_defined
#define __FILE_defined 1

struct _IO_FILE;

/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;

#endif

所以一个 _IO_FILE_plus 就是 _IO_FILE 本体加一个 vtable 指针。在堆上伪造这个结构时,通常只需要让 _flags 和 vtable 看起来合法,其余字段按具体利用路径填。

_IO_jump_t

_IO_FILE_plus 的后半部分是一个指向 _IO_jump_t 的指针。 所谓 vtable 是 virtual table(虚函数表)的缩写,这里存放的是一组函数指针,调用 fread、fwrite 这类函数时会先查这张表。

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

glibc 2.24 起对 vtable 加了校验:指针必须落在 __libc_IO_vtables 段内,直接改成任意地址会被 _IO_vtable_check 拦下。于是后续的利用转向 _wide_data->_wide_vtable——_IO_wfile_overflow 及其调用链会从那里取函数指针,早期版本没有同样的检查。