Vtable Hijack

concept

in newer version of libc, we can no longer overwrite the file->vtable address with our fake vtable, this because libc has introduced a mitigation IO_validate_vtable that will check whether the vtable that is stored in the FILE is in the correct region or not.

_wide_data is a struct which is similar to _IO_FILE which is created to handle WCHAR or unicodes. Both struct have vtables, _wide_data uses _IO_wfile_jumps vtable while _IO_FILE uses _IO_file_jumps.

There's two macro that is being used to call these methods within the vtables:

  • _IO_JUMPS_FUNC

  • _IO_WIDE_JUMPS_FUNC

the IO_validate_vtable mitigation only exist in the _IO_JUMPS_FUNC macro.

one thing to note is that _IO_wfile_jumps.__overflow method will eventually calls file->_wide_data->_wide_vtable->__doallocate.

This if way we can:

  • file->vtable = file->_wide_data->_wide_vtable

  • file->_wide_data->_wide_vtable = &fake_wide_vtable

and because the calls to _wide_data->_wide_vtable don't have any checks, we can take control of execution.


fwrite

summary

  1. Set f->flags to _IO_MAGIC & ~_IO_CURRENTLY_PUTTING & ~_IO_UNBUFFERED which is 0xFBAD0000 & ~0x0800 & ~0x0002

  2. Set f->wide_data to a controllable fake wide_data

  3. Set f->wide_data->_IO_buf_base and f->wide_data->_IO_write_base to 0x0

  4. Set f->vtable to _IO_wfile_jumps

  5. Set f->wide_data->wide_vtable to a controllabe fake wide_vtable

  6. Set f->wide_data->wide_vtable->__doallocate to system or win

note:

  • If space is limited, fake wide_data and fake wide_vtable can be overlapped

  • make sure f->_lock contains a valid address that points to NULL

call trace

we can start from it's definition and implementation:

size_t
_IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
{
  // ...
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request); // <-- OUR INTEREST
  _IO_release_lock (fp);
  // ...
}

which then calls _IO_sputn() which is a jump to __xsputn section of the f->vtable

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)

and assuming, we have overwritten f->vtable with _IO_WIDE_JUMPS_FUNC it will call _IO_wfile_xsputn()

size_t
_IO_wfile_xsputn (FILE *f, const void *data, size_t n)
{
  // ...

  if (n <= 0)
    return 0;

  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      // ...
      if (count >= n)
	{
	  const wchar_t *p;
	  for (p = s + n; p > s; )
	    {
	      if (*--p == L'\n')
		{
                  // ...
		}
	    }
	}
    }
  
  if (count > 0)
    {
      if (count > to_do)
	// ...
      if (count > 20)
	{
	  // ...
	}
      else
	{
		// ...
	}
      to_do -= count;
    }
  if (to_do > 0)
    to_do -= _IO_wdefault_xsputn (f, s, to_do); // <-- OUR INTEREST
  
  // ...
}

which then calls _IO_wdefault_xsputn()

size_t
_IO_wdefault_xsputn (FILE *f, const void *data, size_t n)
{
  // ..
  if (more <= 0)
    return 0;
  for (;;)
    {
      // ..
      if (count > 0)
	{
	  // ..
        }
      if (more == 0 || __woverflow (f, *s++) == WEOF) // <-- OUR INTEREST
      
      // ..
}

which then calls __woverflow()

wint_t
__woverflow (FILE *f, wint_t wch)
{
  if (f->_mode == 0)
    _IO_fwide (f, 1);
  return _IO_OVERFLOW (f, wch); // <-- OUR INTEREST
}

when calls _IO_OVERFLOW ();

typedef int (*_IO_overflow_t) (FILE *, int);
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

again, remember we have overwritten f->vtable with _IO_WIDE_JUMPS_FUNC and because of it the __overflow section of f->vtable will refer to _IO_wfile_overflow() and so the execution continues from there on

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) // <-- NEEDS TO BE SATISFIED
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0) // <-- NEEDS TO BE SATISFIED
	{
	  _IO_wdoallocbuf (f);		      // <-- OUR INTEREST
	  // stuff
	  if (f->_IO_write_base == NULL)
	    {
	      // stuff
	    }
	}
     // ... not rlly relevant
}
libc_hidden_def (_IO_wfile_overflow)

assuming we set the correct f->flags and other requirement it will eventually call _IO_wdoallocbuf()

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base) // <-- NEEDS TO BE SATISFIED
    return;
  if (!(fp->_flags & _IO_UNBUFFERED)) // <-- NEEDS TO BE SATISFIED
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) // <-- OUR INTEREST
      return;
  // stuff
}
libc_hidden_def (_IO_wdoallocbuf)

again, setting the correct requirements that satisfied the if statements will call _IO_WDOALLOCATE()

typedef int (*_IO_doallocate_t) (FILE *);
#define _IO_DOALLOCATE(FP) JUMP0 (__doallocate, FP)
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

which will then call f->wide_data->wide_vtable->__doallocate. and since its within our control, we control the execution.


others

todo

Last updated