Skip to content

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.

Published on 4 min read

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:

A FILE dispatches stdio calls through its vtable pointer
FILE object (stdout)_flags, buffers, fds …
holds
vtable pointerwritable — attacker overwrites this
points to
_IO_*_jumps tablea vtable of function pointers
fwrite / fclose call
__xsputn / __finishentry runs with controlled args
datawritable memorycode

fwrite 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

DefenceEffect 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 manglingSome internal FILE pointers are mangled, needing a secret to forge
ASLR + leak requirementThe 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 FILE object 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.

Related guides

0x7000 · Exploitation Techniques

Format-String Bugs: Arbitrary Read and Write

One printf(user_input) is a full read/write primitive. Leak with %p, overwrite with %n via pwntools, then watch -Wformat=2 and FORTIFY_SOURCE shut it down.

0x7000 · Exploitation Techniques

Hijacking the GOT: Redirecting a libc Call

Lazy binding leaves the Global Offset Table writable. Aim an arbitrary write at a GOT entry, turn puts() into system(), then enable Full RELRO and watch the same write fault instantly.

0x7000 · Exploitation Techniques

Heap Exploitation: tcache and Use-After-Free

The heap's own metadata is the write primitive. Build a use-after-free, poison the glibc tcache to allocate a chunk over a chosen address, and see why safe-linking and ASan raise the bar.