File-Structure Attacks: Hijacking a FILE Object
When hooks are gone, the FILE object is the target. A stdio call dispatches through a vtable pointer in writable memory — corrupt it and the next fwrite runs your code.
This is the eleventh walkthrough in the Exploitation Techniques area, and the capstone of its current arc. The GOT overwrite and heap guides ended a write primitive in code execution by aiming at the GOT or an allocator hook. On modern glibc the hooks are gone — so attackers turned to an object that is always present and always exercised: the FILE.
Heavily version-dependent, like all late-stage glibc work. Your own binary, disposable lab. See the lab rules.
The FILE object dispatches through a vtable
Every stdio stream — stdin, stdout, stderr, and anything from fopen — is a struct _IO_FILE followed by a pointer to a _IO_jump_t vtable of function pointers. Library calls do not call fixed code; they dispatch through that vtable:
FILE object (stdout)_flags, buffers, fds …vtable pointerwritable — attacker overwrites this_IO_*_jumps tablea vtable of function pointers__xsputn / __finishentry runs with controlled argsfwrite ultimately calls vtable->__xsputn, fclose calls vtable->__finish, and so on. Control what the vtable pointer points at — or what the object contains when a legitimate entry runs — and the next stdio call transfers control.
The classic technique (teaching model)
On old glibc (before the 2.24 validation), FSOP was direct: use an arbitrary write to replace a FILE's vtable pointer with a pointer to a fake vtable you built in writable memory, whose __xsputn/__overflow slot holds the address of system (or a one-gadget). The next output through that stream calls it.
/* vuln.c — a write-what-where, then output through stdout. */
#include <stdio.h>
int main(void){
setvbuf(stdout, NULL, _IONBF, 0);
unsigned long addr, val;
scanf("%lx %lx", &addr, &val);
*(unsigned long*)addr = val; /* arbitrary write primitive */
printf("done\n"); /* stdio call dispatches through the vtable */
return 0;
}
# fsop_classic.py — conceptual, for a glibc WITHOUT vtable validation
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("./libc.so.6") # the exact target libc
# Suppose a leak gave us libc.address (see the info-leak guide).
# Build a fake vtable whose __xsputn entry is system, and point
# stdout's vtable pointer at it; craft the object so system("...") runs.
# The precise field offsets depend on the glibc version.
The mechanics matter more than the exact bytes here: a stdio call is an indirect call through a pointer in writable memory, and any arbitrary write can bend it.
Why modern glibc makes you work
glibc 2.24 added _IO_vtable_check: before dispatching, it verifies the vtable pointer lies inside the read-only __libc_IO_vtables array, and aborts with Fatal error: glibc detected an invalid stdio handle otherwise. A fake vtable in your own buffer fails this check immediately.
Modern FSOP works within the rules:
- Reuse a legitimate vtable. Point the object at a real, allowed vtable such as
_IO_wfile_jumps, then craft the FILE's fields so that a valid entry (e.g._IO_wfile_overflow) performs the call you want. This is the House of Apple 2 technique and its relatives. - Target the wide-char paths, which have more exploitable indirect calls that pass validation.
- Chain from a heap primitive: tcache poisoning that returns a chunk overlapping a FILE object, then corrupt its fields.
These are among the more intricate techniques in current CTF pwn, and every step is tied to a specific glibc version's struct layout. The honest summary: FSOP is alive and well, but it is craftsmanship, not a one-liner.
Turn the mitigations back on
| Defence | Effect on FSOP |
|---|---|
| Vtable validation (glibc 2.24+) | Rejects fake vtables outside __libc_IO_vtables |
| Removed hooks (glibc 2.34+) | Forces attackers here in the first place — but the FILE path is checked |
| Pointer mangling | Some internal FILE pointers are mangled, needing a secret to forge |
| ASLR + leak requirement | The libc addresses in the fake object need a leak |
| AddressSanitizer (CI) | Catches the upstream overflow/UAF that enables the write |
The pattern of this whole series holds one more time: the technique exploits legitimate dispatch, so the durable defences are (1) never getting the arbitrary write — fix the overflow, the UAF, the format string — and (2) the runtime integrity checks glibc keeps adding.
What this teaches a defender
- Indirect dispatch is attack surface. Any object holding function pointers in writable memory — a FILE, a C++ vtable, a callback table — turns an arbitrary write into control flow. C++ codebases should note the parallel: a corrupted object vptr is the same idea.
- Keeping glibc current is a real mitigation. Vtable validation, hook removal and pointer mangling each closed a technique. "We're on an old glibc for compatibility" is a security decision, not just an ops one.
- It all traces back to one write. FSOP, GOT overwrite and hooks are interchangeable finales for the same upstream bug. Kill the bug in CI with sanitizers and fuzzing and none of the finales get a turn.
Key takeaways
- A
FILEobject dispatches stdio calls through a vtable pointer in writable memory; corrupting it (or the object) redirects the next call. - The classic attack swapped in a fake vtable; glibc 2.24's vtable validation ended that direct form.
- Modern FSOP (House of Apple and kin) reuses legitimate vtables and crafts the object to pass validation — powerful but tightly version-specific.
- Every finale in this series spends one arbitrary write; the durable fix is preventing that write, plus staying on a current, checked glibc.
This closes the current Exploitation Techniques arc. Read the area overview for the full path — ret2win through the heap and FILE — and the CTF learning path for where to practise it legally.