Lion Cipher Lab
Hijack a file stream on modern glibc: File Stream-Oriented Programming with House of Apple 2 (borrow _IO_wfile_jumps to survive _IO_vtable_check)
- Penetration Testing / Red Team
- Advanced
- ~90 min
- Free · no account required
Why it matters
For years the fastest heap-to-shell path was a hook: overwrite __malloc_hook or __free_hook with system (or a one-gadget) and let the next allocation call it. glibc 2.34 REMOVED both hooks. That single change promoted FILE-stream hijacking from a party trick to a PRIMARY modern control-flow primitive: the C stdio structures are still full of function pointers glibc calls on your behalf, and every process has open streams that get flushed at exit. The classic form of this -- the forged-vtable FSOP you met in the House of Orange lab -- is dead on any glibc >= 2.24, because _IO_vtable_check /…
What you'll learn
- Why the classic House-of-Orange FSOP (forge a vtable, overwrite _IO_list_all, trigger via the malloc/abort path) now ABORTS on modern glibc: _IO_vtable_check / IO_validate_vtable (2.24+) rejects any FILE vtable that is not inside libc's __libc_IO_vtables section
- The House of Apple 2 primitive end to end: point the FILE's MAIN vtable at a LEGITIMATE glibc jumps table (_IO_wfile_jumps) so the vtable check passes, then abuse the UNVALIDATED wide vtable (_wide_data->_wide_vtable) that _IO_WDOALLOCATE dereferences without checking
- Reading the exact glibc call chain: _IO_flush_all_lockp -> _IO_OVERFLOW -> _IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE(fp) == _wide_data->_wide_vtable->__doallocate(fp), which calls your target with rdi = the FILE pointer
- Crafting a fake _IO_FILE + fake _IO_wide_data + fake wide jump table that satisfies every branch condition (the flush trigger _mode<=0 && _IO_write_ptr>_IO_write_base, the _flags bits _IO_NO_WRITES/_IO_CURRENTLY_PUTTING/_IO_UNBUFFERED, and the NULL wide _IO_write_base/_IO_buf_base that force the allocate path)
- The ' sh;' _flags trick: making the FILE's first 8 bytes a runnable command string so system(fp) pops a shell, while keeping the flag bits the flush path checks all clear
- Triggering FSOP through exit(): exit -> _IO_cleanup -> _IO_flush_all_lockp walks the _IO_list_all chain, so appending your fake FILE to a real stream's _chain is enough to reach it
- Binary-exploitation JUDGMENT: WHY FSOP is a primary control-flow primitive now that __malloc_hook/__free_hook were removed in glibc 2.34, WHAT still stops these techniques (2.36+ hardening, pointer mangling, getting the offsets wrong), and the honest constraint that you almost always need a libc (and often heap) leak first
Steps in this lab
- Read this first: your own container, your own program, authorized-only
- The wall this climbs: why the House-of-Orange FSOP vtable forge now aborts
- The idea in one breath: borrow _IO_wfile_jumps, abuse the unvalidated wide vtable
- Pin glibc 2.35 with Docker -- and confirm the mitigation you are bypassing is ON
- Write the toy: a FILE you fully control, leaked addresses, and an exit() trigger
- Compile with mitigations off for teaching -- and note what stays ON
- Build the fake _IO_FILE: every field, and the branch it satisfies
- Fire it, and get the shell -- a call glibc made from its own exit path
- When it fails, and do it better: why FSOP matters now, and what still stops it