# IO_FILE

# 结构体及部分函数源码分析

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

_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,在这个结构体中还包含着一个_IO_jump_t 类型的指针 vtable

//_IO_jump_t 结构体源码:
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
};
......
//_IO_FILE_plus 及 vtable 指针定义:
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};
......

在_IO_jump_t 结构体中包含了一系列指针,在调用 fopen、fclose、fread 等一系列函数时就会调用这些函数指针

void * funcs[] = {
   1 NULL, // "extra word"
   2 NULL, // DUMMY
   3 exit, // finish
   4 NULL, // overflow
   5 NULL, // underflow
   6 NULL, // uflow
   7 NULL, // pbackfail
   8 NULL, // xsputn  #printf
   9 NULL, // xsgetn
   10 NULL, // seekoff
   11 NULL, // seekpos
   12 NULL, // setbuf
   13 NULL, // sync
   14 NULL, // doallocate
   15 NULL, // read
   16 NULL, // write
   17 NULL, // seek
   18 pwn,  // close
   19 NULL, // stat
   20 NULL, // showmanyc
   21 NULL, // imbue
};
可以通过gdb打印来查看vtable中的指针:
pwndbg> print _IO_file_jumps
$1 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7ffff7e540d0 <_IO_new_file_finish>,
  __overflow = 0x7ffff7e54f00 <_IO_new_file_overflow>,
  __underflow = 0x7ffff7e54ba0 <_IO_new_file_underflow>,
  __uflow = 0x7ffff7e560d0 <__GI__IO_default_uflow>,
  __pbackfail = 0x7ffff7e57800 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7ffff7e53750 <_IO_new_file_xsputn>,
  __xsgetn = 0x7ffff7e533c0 <__GI__IO_file_xsgetn>,
  __seekoff = 0x7ffff7e529e0 <_IO_new_file_seekoff>,
  __seekpos = 0x7ffff7e56780 <_IO_default_seekpos>,
  __setbuf = 0x7ffff7e526b0 <_IO_new_file_setbuf>,
  __sync = 0x7ffff7e52540 <_IO_new_file_sync>,
  __doallocate = 0x7ffff7e45df0 <__GI__IO_file_doallocate>,
  __read = 0x7ffff7e53720 <__GI__IO_file_read>,
  __write = 0x7ffff7e52fe0 <_IO_new_file_write>,
  __seek = 0x7ffff7e52780 <__GI__IO_file_seek>,
  __close = 0x7ffff7e526a0 <__GI__IO_file_close>,
  __stat = 0x7ffff7e52fc0 <__GI__IO_file_stat>,
  __showmanyc = 0x7ffff7e57990 <_IO_default_showmanyc>,
  __imbue = 0x7ffff7e579a0 <_IO_default_imbue>
}

当程序执行 fopen 等函数时就会创建 FILE 结构体,当我们打开多个文件,就会创建多个 FILE 结构体,这时候这些 FILE 结构体就会通过_chain 域相连,链表头部用全局变量_IO_list_all 表示,

当程序启动时会打开三个文件流:stdin、stdout、stderr。在初始状态下,_IO_list_all 指向的是一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。

# fopen

fopen 用于打开文件,原型如下:

FILE *fopen(char *filename, *type);
// 返回一个文件指针

fopen 函数实际调用的是_IO_new_fopen 函数,_IO_new_fopen 函数又会调用_fopen_internal 函数,__fopen_internal 函数主要分为四步:

1.malloc分配内存空间。
2.调用_IO_no_init函数对file结构体进行null初始化。
3.调用_IO_file_init函数将结构体链接进_IO_list_all链表。
4.调用_IO_file_fopen函数最终执行系统调用打开文件。

# fread

fread 从文件流中读取数据,原型如下:

_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)

fread 函数实际调用的是_IO_fread 函数,_IO_fread 函数又调用_IO_sgetn 函数,这个函数又继续调用_IO_XSGETN 函数,其实就是 vtable 中的__xsgetn 函数。

__xsgetn 函数主要分为三步:

1.当fp->_IO_buf_base==NULL时,说明FILE结构体中的指针还没有被初始化,缓冲区未建立,则调用_IO_doallocbuf初始化指针,建立缓冲区。
2.当输入缓冲区里有数据,即fp->_IO_read_ptr小于fp->_IO_read_end时,把缓冲区里的数据直接拷贝至目标buf。
3.当缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow调用系统调用读入数据。

fread 在执行系统调用前执行了多次 vtable 中的函数。

__GI__IO_file_xsgetn
_IO_file_doallocate  // 初始化输入缓冲区
__GI__IO_file_stat  // 获取文件信息
_IO_new_file_underflow  // 读取文件数据
__GI__IO_file_read  // 最终执行系统调用

# fwrite

fwrite 向文件流中写入数据,原型如下:

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

fwrite 函数实际调用的是_IO_fwrite 函数,_IO_fwrite 函数又调用了_IO_sputn 函数,即 vtable 中的__xsputn 函数。

这个函数也分为三部分:

1.判断输出缓冲区还有多少空间,如果输出缓冲区有剩余空间的话,把目标输出数据拷贝到输出缓冲区,然后计算在输出缓冲区填满后,是否仍然剩余的目标输出数据。
2.如果还有剩余的目标输出数据,说明输出缓冲区满了或者没有建立,这时候调用_IO_OVERFLOW刷新或建立输出缓冲区。
3.经过刷新或建立输出缓冲区后,检查目标输出数据的大小,如果超出输出缓冲区的大小,就跳过输出缓冲区直接输出。然后把最后剩余的数据拷贝到输出缓冲区。

fwrite 在执行系统调用前同样执行了多次 vtable 中的函数

_IO_new_file_xsputn
_IO_new_file_overflow  // 刷新或建立输出缓冲区
_IO_file_doallocate  // 初始化输入缓冲区
__GI__IO_file_stat  // 获取文件信息
_IO_new_file_write  // 最终执行系统调用

# fclose

fclose 用于关闭已打开的文件,原型如下:

int fclose(FILE *stream)

执行 fclose 后会关闭一个文件流,把缓冲区内剩余的数据输出到文件中,同时释放文件指针和有关的缓冲区。

fclose 函数实际调用的是_IO_new_fclose 函数,_IO_new_fclose 函数大致分为四部分:

1.调用_IO_un_link函数将IO FILE结构体从_IO_list_all链表中取下。
2.调用_IO_file_close_it函数关闭文件,释放缓冲区,并清空缓冲区指针。
3.调用_IO_FINISH函数,即vtable中的__finish函数,确认文件开关状态和缓冲区是否被释放。
4.此时已经将结构体从链表中删除,刷新了缓冲区,释放了缓冲区内存,只剩下结构体内存尚未释放,因此释放结构体内存。

fclose 函数执行过程中调用的 vtable 函数

__close  // 执行系统调用关闭文件描述符
__finish  // 确认文件开关状态和缓冲区状态

# 部分函数在 libc 中的函数名:

p *(struct _IO_FILE_plus *) stdout

p *_IO_list_all

p _IO_file_jumps

_IO_new_file_init

_IO_new_file_fopen

_IO_file_open

_IO_file_xsgetn

_IO_new_file_xsputn

_IO_new_file_close_it

# 劫持 vtable 并修改指针

vtable 是_IO_FILE_plus 结构体里的一个字段,是一个函数表指针,里面存储着许多和 IO 相关的函数。

fread、fwrite、fclose 等 IO 函数基本都调用了 vtable 中的函数来实现功能。

# 2.23

# vtable 劫持原理:

控制FILE结构体,实现对vtable指针的修改,使得vtable指向可控的内存,在该内存中构造好vtable,再通过调用相应IO函数,触发vtable函数的调用,即可劫持程序执行流。

# 思路:

1.修改内存中已有FILE结构体的vtable字段。
2.伪造整个FILE结构体。
本质都是修改了vtable字段。

# 例子

#define system_ptr 0x7ffff7a52390;
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
 FILE *fp;
 long long *vtable_addr,*fake_vtable;
 fp=fopen("123.txt","rw");
 fake_vtable=malloc(0x40);// 开辟一个堆当作伪造的 vtable。
 vtable_addr=(long long *)((long long)fp+0xd8);     //0xd8 为 64 位下 vtable 在_IO_FILE_plus 结构体中的偏移。
 vtable_addr[0]=(long long)fake_vtable;// 让 vtable 指针指向我们伪造的 vtable。
 memcpy(fp,"sh",3);// 因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,也就是_flags 成员变量,所以这里设置_flag="sh"。
 fake_vtable[7]=system_ptr; // 找到__xsputn 函数在 vtable 中的偏移,修改这个位置的指针,使其指向 system 函数。
 fwrite("hi",2,1,fp);// 执行 fwrite 函数会调用__xsputn 函数,实际上是执行 system ("sh")
 fclose(fp);
}

# 2.24 以后

# vtable check 机制

在执行_IO_OVERFLOW 函数前执行了 IO_validate_vtable 函数,这个函数会检测 vtable 是不是 glibc 中的 vtable,如果不是,进入_IO_vtable_check 函数。

_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)   // 检查是不是外部构造中的 vtable
    return;
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))   // 检查是不是动态链接库的 vtable
      return;
  }
#else /* !SHARED */
  if (__dlopen != NULL)
    return;
#endif
  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
  // 如果都不是,就报错,并且结束程序
}

总结:

1.判断vtable的地址是否位于glibc中的vtable数据段,若是,通过检查,返回。
2.判断vtable是否为外部合法的vtable(通过FILE*结构体创建的vtable等),若是,通过检查,返回。
3.若前两项检查都不通过,报错,退出程序。

有了这个机制后,我们伪造的 vtable 本身就位于堆栈上,肯定会进入_IO_vtable_check 函数检查,并且修改 vtable 指针的行为通过不了这个检查,最终报错。

# 绕过 vtable check 机制

# vtable 覆盖为外部地址

因为 check 机制的存在,我们如果还想伪造 vtable,就需要满足下面两个条件的其中一个,分别对应_IO_vtable_check 函数的两次检查条件。

1、flag == &_IO_vtable_check
2、_dl_open_hook!= NULL

第一个条件,flag 的生成与 canary 类似,并没有固定的值,想要让 flag == &_IO_vtable_check 是比较困难的。

第二个条件,如果能往_dl_open_hook 中写值,那完全可以往其他 hook 中写值,有更简单的方法。

# 利用另一个 vtable 函数(_IO_str_jumps)

p _IO_str_jumps

这个 vtable 中有两个函数,_IO_str_overflow 和_IO_str_finish。其中_IO_str_finish 函数在满足 if 语句的情况下直接使用了 fp->_s._free_buffer 指针,如果修改这个指针为 one_gadget,就能 getshell。

# 如何利用
  1. 绕过_IO_flush_all_lokcp 中对输出缓冲区的检查,让程序进入_IO_OVERFLOW 函数。

  2. 要绕过 vtable check 机制,可以把 vtable 的地址覆盖成_IO_str_jumps-8 的地址(_IO_str_jumps 的偏移为 0x10,_IO_OVERFLOW 的偏移为 0x18),_IO_str_finish 函数成为了伪造的 vtable 地址的_IO_OVERFLOW 函数,同时因为_IO_str_finish 函数位于 vtable 的地址段中,所以可以绕过 vtable check。_

  3. _满足_IO_str_finish 函数的 if 语句,让程序使用 fp->_s._free_buffer 指针。因此要满足 fp->_IO_buf_base 不为 NULL 并且 fp->_flags 中不包含_IO_USER_BUF。(#define _IO_USER_BUF 1;)

  4. 构造 fp->_s._free_buffer 为 system/gadget,fp->_IO_buf_base 为 "/bin/sh",调用 (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base) 时就相当于执行了 system ("/bin/sh")。

# 劫持 vtable 及 FSOP

FSOP 全称是 File Stream Oriented Programming ,利用的是前面 fopen 函数中描述过的 _IO_list_all 指针。

我们知道程序中所有打开的文件流都是由一个单链表进行管理的,并且,链表是由结构体中的_chain 字段进行连接。

在正常的程序中,会存在stderrstdoutstdin三个IO_FILE,通过gdb打印
pwndbg> print _IO_list_all
$1 = (struct \_IO\_FILE\_plus *) 0x7ffff7dd2540 <\_IO\_2\_1\_stderr\_>
pwndbg> print _IO_list_all->file._chain
$2 = (struct \_IO\_FILE *) 0x7ffff7dd2620 <\_IO\_2\_1_stdout\_>
pwndbg> print (struct _IO_FILE *) (_IO_list_all->file._chain)._chain
$3 = (struct \_IO\_FILE *) 0x7ffff7dd18e0 <\_IO\_2\_1\_stdin\_>
pwndbg> print (struct _IO_FILE *) ((_IO_list_all->file._chain)._chain)._chain
$4 = (struct \_IO\_FILE *) 0x0

形成了下图所示的链表:

Alt text

如果能够控制 _IO_list_all 指针,就可以让该指针直接指向我们想要的地址,这就是 FSOP。

libc 中存在一个函数 _IO_flush_all_lockp ,用来刷新所 有 FILE 结构体的输出缓冲区。

int _IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;
#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif
  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);
      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)     // 如果缓冲区还有数据,就刷新数据缓冲区
	result = EOF;
      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;
      if (last_stamp != _IO_list_all_stamp)
	{
	  /* Something was added to the list.  Start all over again.  */
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;        // 遍历列表,检查所有结构体的输出缓冲区。
    }
#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif
  return result;
}

前面在分析 fwrite 的时候提到过,输出缓冲区的数据保存在 fp->IO_write_base 上,且长度为 fp->_IO_write_ptr-fp->_IO_write_base ,因此,这个函数的整体思路是检查该结构体的输出缓冲区是否还有数据,如果有就调用 _IO_OVERFLOW 去刷新输出缓冲区。如果可以控制 _IO_list_all 中的某一个节点,并构造 vtable 的话,就可以控制程序流。

攻击的整体流程就是:

  1. 伪造一个 _IO_FILE 结构体,并使 _IO_list_all 链表指向该结构体,或者让 _IO_list_all 链表中某一节点的 _chain 字段指向伪造的数据。
  2. 利用 _IO_flush_all_lockp 函数,绕过检查,最终调用 _IO_OVERFLOW 来劫持程序流。

# 东华杯 2016-pwn450-note(house of orange)

待施工。。。