利用 _IO_2_1_stdout_ 泄露信息

在ctf比赛中,或者真实的环境下,有时程序并不能泄露出 libc 地址,对于 32 位的程序还有爆破的可能性,但是 64 位的几率则太小了,这时就需要额外的一种泄露信息的机制————_IO_2_1_stdout_

FILE 简介

首先来看看 FILE 的结构体信息。

pwndbg> p stdout 
$1 = (struct _IO_FILE *) 0x7ffff7dd0760 <_IO_2_1_stdout_>
pwndbg> ptype stdout
type = 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;
    int _flags2;
    __off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
    __off64_t _offset;
    struct _IO_codecvt *_codecvt;
    struct _IO_wide_data *_wide_data;
    struct _IO_FILE *_freeres_list;
    void *_freeres_buf;
    size_t __pad5;
    int _mode;
    char _unused2[20];
} *

对于一个FILE结构体来说,最重要的元素就是_flags_fileno_fileno存储的是我们的文件描述符,对于某些情况或许我们要劫持_fileno才能达到我们的目的,而_flags则标志了该FILE的一些行为,这对于我们的泄露至关重要。

我简单的说明一下_flags的规则,_flags的高两位字节,这是由 libc 固定的,不同的 libc 或许不同,但是大体相同,这就像一个文件的头标示符一样,标志这是一个什么文件,正如注释所说High-order word is _IO_MAGIC; rest is flags.,而低两位字节的位数规则可以参考下面代码,不同位的功能,在注中已经标明。

代码来自:/usr/include/x86_64-linux-gnu/bits/libio.h

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

_IO_2_1_stdout_一般是这样的:

_IO_MAGIC|_IO_IS_FILEBUF|_IO_CURRENTLY_PUTTING|_IO_LINKED|_IO_NO_READS | _IO_UNBUFFERED |_IO_USER_BUF

让我们先来看看在调用puts的过程中究竟发生了什么呢?

代码来自:glibc-2.27/libio/ioputs.c:31

int
_IO_puts (const char *str)
{
  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
       || _IO_fwide (_IO_stdout, -1) == -1)
      && _IO_sputn (_IO_stdout, str, len) == len
      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
    result = MIN (INT_MAX, len + 1);

  _IO_release_lock (_IO_stdout);
  return result;
}

可以看到这里实际上是调用了_IO_sputn 这个函数,当然这个_IO_sputn实际上就是一个宏,调用了_IO_2_1_stdout_vtable中的__xsputn,也就是_IO_new_file_xsputn函数。

代码来自:glibc-2.27/libio/fileops.c:1218

_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;
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      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;

      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);

      if (do_write)
    {
      count = new_do_write (f, s, do_write);
      to_do -= count;
      if (count < do_write)
        return n - to_do;
    }

      /* Now write out the remainder.  Normally, this will fit in the
     buffer, but it's somewhat messier for line-buffered files,
     so we let _IO_default_xsputn handle the general case. */
      if (to_do)
    to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

总之我们的最终目的是到下面的代码中去。

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

前面先判断了一堆缓冲区还够不够输出之类的,之后调用到了_IO_OVERFLOW,也就是下面的代码。

代码来自:glibc-2.27/libio/fileops.c:744

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 (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;
  *f->_IO_write_ptr++ = ch;
  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (_IO_do_write (f, f->_IO_write_base,
              f->_IO_write_ptr - f->_IO_write_base) == EOF)
      return EOF;
  return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

_IO_new_file_overflow中,首先要#define _IO_NO_WRITES 8 /* Writing not allowd */标志位不为1,否则就会返回错误。

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)

最好的办法就是将#define _IO_CURRENTLY_PUTTING 0x800置为1,当stdout从来没有输出过时,该标志位为0,因为程序绝大多数是要输出的,所以通常情况下该标志位就是1,但是如果不绕过该判断,则会对我们篡改的指针进行修复,这样就没办法泄露了。

而且不修改的话会伴随一个致命错误,导致后面的程序都没有输出,从后面的代码可得程序要输入只有当_IO_IS_APPENDING标志位为1时(默认为0),或者stdout->_IO_read_end == stdout->_IO_write_base时才行,由于执行了上面的代码导致了这两个指针不同,所以后面的输出都直接返回了。

其中最重要的代码如下:

if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
         f->_IO_write_ptr - f->_IO_write_base);

接下来就跟进_IO_do_write中看看。

In file: /glibc/glibc-2.27/libio/fileops.c
   428 
   429 int
   430 _IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
   431 {
   432   return (to_do == 0433    || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
   434 }
   435 libc_hidden_ver (_IO_new_do_write, _IO_do_write)

这里就跳到了new_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);
  if (fp->_cur_column && count)
    fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
  fp->_IO_write_end = (fp->_mode <= 0
               && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
               ? fp->_IO_buf_base : fp->_IO_buf_end);
  return count;
}

这里我们可以设置_IO_IS_APPENDING为1, 或者设置stdout->_IO_read_end == stdout->_IO_write_base,否则就不会有输出。

举例

由于printfputs原理差不多,这里我就用printf来举例:

第一种情况是同时修改_IO_IS_APPENDING_IO_CURRENTLY_PUTTING标志位。

#include <stdio.h>

int main()
{
    int flags;
    setbuf(stdout, NULL);
    flags = stdout->_flags;
    stdout->_flags = 0xfbad2087 | 0x1000 | 0x800;
    stdout->_IO_write_base -= 8;
    printf("flags: 0x%x\n", flags);
}

运行结果如下:

ex@Ex:~/test$ gcc main.c
ex@Ex:~/test$ ./a.out 
�����flags: 0xfbad2087

第二种情况是同时修改_IO_CURRENTLY_PUTTING标志位和stdout->_IO_write_base和stdout->_IO_read_end

#include <stdio.h>

int main()
{
    int flags;
    char *_IO_read_end, *_IO_write_base;
    setbuf(stdout, NULL);
    flags = stdout->_flags;
    _IO_read_end = stdout->_IO_read_end;
    _IO_write_base = stdout->_IO_write_base;
    stdout->_flags = 0xfbad2087 | 0x800;
    stdout->_IO_read_end -= 8;
    stdout->_IO_write_base -= 8;
    printf("flags: 0x%x\n_IO_read_end: %p\n_IO_write_base: %p\n", flags, _IO_read_end, _IO_write_base);
}

运行结果如下:

ex@Ex:~/test$ gcc main.c
ex@Ex:~/test$ ./a.out 
�����flags: 0xfbad2087
_IO_read_end: 0x7fd30475b7e3
_IO_write_base: 0x7fd30475b7e3

第三种情况是stdout已经输出过,所以_IO_CURRENTLY_PUTTING标志位就是1,我们只要修改stdout->_IO_write_base和stdout->_IO_read_end指针就好,具体代码如下:

#include <stdio.h>

int main()
{
    int flags;
    char *_IO_read_end, *_IO_write_base;
    setbuf(stdout, NULL);
    puts("hello world");

    flags = stdout->_flags;
    _IO_read_end = stdout->_IO_read_end;
    _IO_write_base = stdout->_IO_write_base;
    stdout->_IO_read_end -= 8;
    stdout->_IO_write_base -= 8;
    printf("flags: 0x%x\n_IO_read_end: %p\n_IO_write_base: %p\n", flags, _IO_read_end, _IO_write_base);
}

运行结果如下:

ex@Ex:~/test$ ./a.out 
hello world
�����flags: 0xfbad2887
_IO_read_end: 0x7fc09ad0f7e3
_IO_write_base: 0x7fc09ad0f7e3

利用

大体的利用方法就是利用unsorted bin的在tcachefastbin的fd上踩出main_arena的地址,然后部分覆盖修改main_arena的地址实现对stdout的地址进行爆破,从而劫持stdout以达到泄露的目的 。

对于没有tcache的 glibc 版本,我们可以使用 fastbin attack就好,因为_IO_2_1_stdout_上面就是_IO_2_1_stderr_stderr->__pad2一般是指向_IO_wide_data_2的指针,而_IO_wide_data_2是在libc中的,所以我们可以用其来伪造size。

pwndbg> p/x *stderr
$5 = {
  _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 = 0x7ffff7dd3740, 
  _fileno = 0x2, 
  _flags2 = 0x0, 
  _old_offset = 0xffffffffffffffff, 
  _cur_column = 0x0, 
  _vtable_offset = 0x0, 
  _shortbuf = {0x0}, 
  _lock = 0x7ffff7dd4890, 
  _offset = 0xffffffffffffffff, 
  __pad1 = 0x0, 
  __pad2 = 0x7ffff7dd2760, 
  __pad3 = 0x0, 
  __pad4 = 0x0, 
  __pad5 = 0x0, 
  _mode = 0x0, 
  _unused2 = {0x0 <repeats 20 times>}
}

具体伪造size的情况如下的调试代码所示:

pwndbg> x/14gx 0x7ffff7dd36f0
0x7ffff7dd36f0 <_IO_2_1_stderr_+144>: 0xffffffffffffffff  0x0000000000000000
0x7ffff7dd3700 <_IO_2_1_stderr_+160>: 0x00007ffff7dd2760  0x0000000000000000
0x7ffff7dd3710 <_IO_2_1_stderr_+176>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd3720 <_IO_2_1_stderr_+192>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd3730 <_IO_2_1_stderr_+208>: 0x0000000000000000  0x00007ffff7dcf2a0
0x7ffff7dd3740 <_IO_2_1_stdout_>: 0x00000000fbad2887  0x00007ffff7dd37c3
0x7ffff7dd3750 <_IO_2_1_stdout_+16>:  0x00007ffff7dd37c3  0x00007ffff7dd37c3
pwndbg> x/14gx 0x7ffff7dd36f0+5-8
0x7ffff7dd36ed <_IO_2_1_stderr_+141>: 0xffffffffff00007f  0x0000000000ffffff
0x7ffff7dd36fd <_IO_2_1_stderr_+157>: 0xfff7dd2760000000  0x000000000000007f
0x7ffff7dd370d <_IO_2_1_stderr_+173>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd371d <_IO_2_1_stderr_+189>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd372d <_IO_2_1_stderr_+205>: 0x0000000000000000  0xfff7dcf2a0000000
0x7ffff7dd373d <_IO_2_1_stderr_+221>: 0x00fbad288700007f  0xfff7dd37c3000000
0x7ffff7dd374d <_IO_2_1_stdout_+13>:  0xfff7dd37c300007f  0xfff7dd37c300007f

从中就可以看到我们已经有一个0x7f的size了。

对于有tcache的 glibc 版本,直接爆破劫持stdout就可以了。

实例

SCTF2019 one_heap:2019-06-23.SCTF2019 writeups#pwn_one_heap