# 简介

IO_FILE 是描述 IO 的文件结构体,相关源码来自 libio/libioP.h 文件中。

IO_FILE 结构:在执行 fopen 等函数时创建。不同 IO_FILE 以链表形式串接起来。

# _IO_list_all 变量

_IO_list_all 变量:指向链表头部。默认链如下。

_IO_list_all - stderr -> stdout -> stdin

# 三文件流

存在以下三种符号,指向他们对应的 file 结构。

其存放在 libc.so 中。

_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

# io file plus

包裹着 file 结构体。

vtable 的偏移

架构 偏移
32 0x94
64 0xd8
struct _IO_FILE_plus
{
	_IO_FILE    file;
	_IO_jump_t   *vtable;
}

# file 结构

不同 file 结构体用 chain 域串起形成链表。

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

# _IO_jump_t

是 vtable 指针指向的结构体,存放着各种函数跳转地址。

其中偏移为 7 的 xsputn 很有用。

libc.so 自带的 _IO_jump_t 一般不可修改。但我们可以伪造 _IO_jump_t 并修改 vtable 的值指向伪造的结构体。

#define JUMP_FIELD(TYPE, NAME) TYPE NAME
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);	// printf函数会用 
    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
};

_IO_2_1_stdout_ 中 vtable 的 setbuf 在程序退出时会执行,可修改为 one_gadget 进行利用。

# 输出输入函数调用栈

# fread

函数位于 /libio/iofread.c

size_t fread (void *buffer, size_t size, size_t count, FILE *stream);
  • buffer 存放读取数据的缓冲区。
  • size:指定每个记录的长度。
  • count: 指定记录的个数。
  • stream:目标文件流。
  • 返回值:返回读取到数据缓冲区中的记录个数

其调用函数栈为

_IO_fread(buf, size, count, fp)	// fread本体
_IO_sgetn(fp, (char*)buf, bytes_requested)	//(fp,data,n)
_IO_XSGETN (fp, data, n)	// 为vtable中的函数指针
_IO_file_xsgetn(fp, data, n)	// 默认情况下_IO_XSGETN所指向

# fwrite

函数位于 /libio/iofwrite.c

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • buffer: 是一个指针,对 fwrite 来说,是要写入数据的地址;
  • size: 要写入内容的单字节数;
  • count: 要进行写入 size 字节的数据项的个数;
  • stream: 目标文件指针;
  • 返回值:实际写入的数据项个数 count。
_IO_fwrite
_IO_XSPUTN	// vtable 函数指针
||
_IO_new_file_xsputn	
_IO_OVERFLOW	// vtable 函数指针
||
_IO_new_file_overflow
write

# printf

vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

# file 结构创建

# fopen

fopen 在标准 IO 库中用于打开文件,函数原型如下

FILE *fopen(char *filename, *type);
  • filename: 目标文件的路径
  • type: 打开方式的类型
  • 返回值:返回一个文件指针

在 fopen 内部会创建 FILE 结构并进行一些初始化操作,下面来看一下这个过程

首先在 fopen 对应的函数 __fopen_internal 内部会调用 malloc 函数,分配 FILE 结构的空间。因此我们可以获知 FILE 结构是存储在堆上

*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

之后会为创建的 FILE 初始化 vtable,并调用_IO_file_init 进一步初始化操作

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);

_IO_file_init 函数的初始化操作中,会调用 _IO_link_in 把新分配的 FILE 链入 _IO_list_all 为起始的 FILE 链表中

void
_IO_link_in (fp)
     struct _IO_FILE_plus *fp;
{
    if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;
      ++_IO_list_all_stamp;
    }
}

之后__fopen_internal 函数会调用_IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数,这里不再深入。

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

总结一下 fopen 的操作是

  • 使用 malloc 分配 FILE 结构
  • 设置 FILE 结构的 vtable
  • 初始化分配的 FILE 结构
  • 将初始化的 FILE 结构链入 FILE 结构链表中
  • 调用系统调用打开文件