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
Set
f->flags
to_IO_MAGIC & ~_IO_CURRENTLY_PUTTING & ~_IO_UNBUFFERED
which is0xFBAD0000 & ~0x0800 & ~0x0002
Set
f->wide_data
to a controllable fakewide_data
Set
f->wide_data->_IO_buf_base
andf->wide_data->_IO_write_base
to 0x0Set
f->vtable
to_IO_wfile_jumps
Set
f->wide_data->wide_vtable
to a controllabe fakewide_vtable
Set
f->wide_data->wide_vtable->__doallocate
tosystem
orwin
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