
Interactive GDB walkthrough of the House of Apple 2 FSOP technique on glibc 2.43, with a reproducible sandbox covering vtable bypass, stack pivoting, and ROP.
This repository is a self-contained playground inspired by pwn.college's File Struct Exploitation module. It does not introduce a new variation of House of Apple 2, it just answers my curiosity about how the technique holds up on recent versions of glibc and whether it remains a viable exploitation path. The document provides an interactive GDB walkthrough that readers can follow alongside the sandbox to develop a more intuitive understanding of the primitive. All experiments use glibc 2.43, as packaged by Ubuntu 26.04 and Fedora 44 at the time of writing.
File Stream Oriented Programming (FSOP). This is about manipulating glibc file stream structures to hijack control flow. One way to do this is by corrupting the vtable dispatch mechanism of _IO_FILE_plus. Modern glibc validates this vtable, so the obvious approach of replacing it with an arbitrary address doesn't work.
House of Apple 2, originally introduced by Roderick, works around this restriction by using a valid _IO_FILE_plus vtable to reach the wide-character stream machinery, where a secondary vtable is directly dispatched without range validation. This provides an arbitrary call primitive that we can escalate into a stack pivot and a ROP chain.
This exploration assumes that we can overwrite a FILE structure and that we have both a heap leak and a libc leak. The target binary already provides this.
The sandbox runs Ubuntu 26.04 LTS, giving us a modern environment to explore the technique.
The image includes GDB, pwndbg, pwntools, ropper and tmux. It also contains a target binary with an interactive menu for invoking file stream operations such as fopen, fread, fwrite, and fclose. This gives us a convenient way to manipulate streams while debugging and testing ideas.
Build and run the sandbox with:
./build.sh
./run.sh
Let's start by inspecting the _IO_FILE and _IO_FILE_plus structures:
pwndbg> ptype struct _IO_FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2 : 24;
char _short_backupbuf[1];
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
struct _IO_FILE **_prevchain;
int _mode;
int _unused3;
__uint64_t _total_written;
char _unused2[8];
}
pwndbg> ptype struct _IO_FILE_plus
type = struct _IO_FILE_plus {
FILE file;
const struct _IO_jump_t *vtable;
}
In practical terms, _IO_FILE_plus is an _IO_FILE with a vtable pointer. That immediately looks interesting: if we can control this pointer, we may be able to redirect an indirect call and hijack control flow.
To inspect the vtable, let's examine a FILE pointer returned by fopen.
pwndbg> p *(struct _IO_FILE_plus *)0x37ecf010
$4 = {
file = {
_flags = 0xfbad2480,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f58a7f4b4a0 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_short_backupbuf = "",
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x37ecf0f0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x37ecf100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7f58a7f4b480 <_IO_list_all>,
_mode = 0x0,
_unused3 = 0x0,
_total_written = 0x0,
_unused2 = "\000\000\000\000\000\000\000"
},
vtable = 0x7f58a7f49030 <_IO_file_jumps>
}
The pointer targets the _IO_file_jumps table.

This is a set of 21 function pointers. File stream operations dispatch through different entries depending on the execution path.
fwrite pathFor this exploration I'll focus on the fwrite path. After placing breakpoints on each function and calling fwrite, the first breakpoint we hit is _IO_file_xsputn.

The call happens at fwrite+216. This matches the glibc source: _IO_sputn is a macro that dispatches through the vtable, resolving to _IO_file_xsputn for this stream.
0x00007fd5181d362a <+202>: mov rdx,rcx
0x00007fd5181d362d <+205>: mov rdi,rbx
0x00007fd5181d3630 <+208>: mov QWORD PTR [rbp-0x30],r8
0x00007fd5181d3634 <+212>: mov QWORD PTR [rbp-0x28],rcx
0x00007fd5181d3638 <+216>: call QWORD PTR [rax+0x38]
For a first attempt, let's overwrite the vtable pointer with desired_func - 0x38 and set a breakpoint at fwrite+216.
pwndbg> p &win
$3 = (<text variable, no debug info> *) 0x4019e1 <win>
pwndbg> p/x &win - 0x38
$4 = 0x4019a9
pwndbg> set ((struct _IO_FILE_plus *)0x5334010)->vtable = (void *)0x4019a9
pwndbg> b *fwrite+216
Breakpoint 4 at 0x7fd5181d3638: file ./libio/libioP.h, line 1042.

Execution aborts before reaching the breakpoint. The error suggests that glibc validates the vtable pointer before performing the indirect call. Let's inspect the backtrace and see where this happens.
fwrite reaches _IO_vtable_check which is rejecting the forged vtable pointer.

The implementation contains a mechanism for accepting foreign vtables, but it is not under our control. The relevant code is available in vtables.c.
By the time _IO_vtable_check is called it's already too late, the vtable validation has failed. The earlier IO_validate_vtable frame in the backtrace is the interesting part, so let's inspect that instead.
pwndbg> disass IO_validate_vtable
❌️ No symbol "IO_validate_vtable" in current context.
GDB cannot resolve IO_validate_vtable as a symbol. Looking at the source, we can see it is inlined into fwrite.