An interactive GDB walkthrough of House of Apple 2 on glibc 2.43: FSOP past vtable checks, wide-stream arbitrary call, stack pivot, and ROP—with a follow-along lab.
An interactive GDB walkthrough of House of Apple 2, from FSOP to stack pivot and ROP on glibc 2.43 (Ubuntu 26.04 / Fedora 44 packaging at time of writing).
File Stream Oriented Programming (FSOP) manipulates glibc file stream structures to hijack control flow. Modern glibc validates _IO_FILE_plus vtables, so replacing the vtable with an arbitrary address aborts via _IO_vtable_check / IO_validate_vtable (vtable must fall in [__io_vtables, __io_vtables + IO_VTABLES_LEN)).
House of Apple 2
Originally introduced by Roderick, House of Apple 2 works around this by using a valid_IO_FILE_plus vtable to reach the wide-character stream machinery, where a secondary _wide_vtable is dispatched without range validation — yielding an arbitrary-call primitive that escalates into a stack pivot and ROP.
Prerequisites
Overwrite a FILE structure; heap leak and libc leak. Sandbox provides interactive fopen/fread/fwrite/fclose with GDB/pwndbg.
Wide-character path
_wide_data → _IO_wide_data with its own _wide_vtable. Path through _IO_wfile_overflow → _IO_wdoallocbuf → _IO_WDOALLOCATE dispatches _wide_vtable + 0x68 with no validation.
Conditions to reach _IO_wdoallocbuf:
_flags must not contain _IO_NO_WRITES (0x0008) or _IO_UNBUFFERED (0x0002)
_wide_data->_IO_write_base and _IO_buf_base must be NULL
_lock must point to a zero-initialized writable 0x10-byte region
Compact overlapping payload
Fake _IO_wide_data starts at offset 0x08 inside the fake _IO_FILE_plus. Key layout:
Offset
Role
0x00
_flags (bit constraints)
0x20 / 0x38
write/buf bases NULL
0x88
_lock
0xa0
_wide_data → base+0x08
0xd8
outer vtable → _IO_wfile_overflow slot
0xe0
arbitrary function pointer (wide_vtable+0x68)
At the call site, RDI and RDX point to the controlled FILE.
Stack pivot and ROP
mov rsp, rdx; ret in __push___start_context+63 pivots into the fake structure. First qword (_flags) needs LSB bits clear of 0x2/0x8 — use a mid-instruction ret gadget. NULL holes at write/buf bases consumed via pop gadgets. _lock at 0x88 cannot be overwritten — ~17 qwords remain for the chain (demo: execve("/bin/sh", NULL, …)).
Conclusion
House of Apple 2 remains reproducible on glibc 2.43: a valid vtable reaches unvalidated wide-stream dispatch. Offsets/gadgets vary by build; the control-flow idea still applies.