IO_FILE Heap Exploitation: Understanding fwrite Implementation

To learn IO_FILE-based heap exploitation, we must understand its underlying mechanism. This article examines several key IO functions using source code analysis and dynamic debugging.

1. fwrite Function Overview

1.1 Overall Flow

The main implementation of fwrite resides in _IO_new_file_xsputn, containing four distinct phases:

  1. Check remaining space in the output buffer; if available, copy target data to the output buffer
  2. If no remaining buffer space (or buffer not allocated), call _IO_OVERFLOW to establish or flush the output buffer
  3. After buffer flush, determine if remaining data exceeds block size; if so, write directly in blocks using sys_write
  4. If data remains after block output, call _IO_default_xsputn to copy remaining data to the output buffer

1.2 Key IO_FILE Structure Pointers

Pointer Description
_IO_buf_base Base address of input/output buffer
_IO_buf_end End address of input/output buffer
_IO_write_base Base address of output buffer
_IO_write_ptr Current address in input buffer
_IO_write_end End address of output buffer

The _IO_doallocbuf function establishes the input/output buffer, storing the base address in _IO_buf_base and the end address in _IO_buf_end. When the buffer functions as an output buffer, the base address is assigned to _IO_write_base and the end adress to _IO_write_end. Additionally, _IO_write_ptr indicates the currently used address. The space between _IO_write_base and _IO_write_ptr represents used buffer space, while _IO_write_ptr to _IO_write_end contains remaining output buffer space.

1.3 Function Prototype

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  • ptr: Pointer to the array of elements to be written
  • size: Size of each element in bytes
  • nmemb: Number of elements, each of size size bytes
  • stream: Pointer to FILE object specifying an output stream

1.4 Sample Program

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp = fopen("output.dat", "wb");
    char *buffer = malloc(0x20);
    fwrite(buffer, 1, 0x20, fp);
    return 0;
}

2. Debugging fwrite

2.1 Compiling the Program

gcc -g fwrite.c -o fwrite

2.2 Setting Breakpoints

After compilation, use gdb for debugging. Set a breakpoint at the fwrite function. The program first stops at _IO_fwrite.

pwndbg> n
7           fwrite(buffer, 1, 0x20, fp);
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x602240 ◂— 0x0
 RBX  0x0
 RCX  0x7ffff7dd1b20 (main_arena) ◂— 0x100000000
 RDX  0x602240 ◂— 0x0
 RDI  0x0
 RSI  0x602260 ◂— 0x0
 R8   0x4
 R9   0x1
 R10  0x4a1
 R11  0x7ffff7a91130 (malloc) ◂— push   rbp
 R12  0x4004c0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe6a0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffe5c0 —▸ 0x400610 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffe5b0 —▸ 0x602010 ◂— 0xfbad2488
 RIP  0x4005df (main+41) ◂— mov    rdx, qword ptr [rbp - 0x10]
────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────────
   0x4005c8 <main+18>    call   fopen@plt <0x400490>
   0x4005cd <main+23>    mov    qword ptr [rbp - 0x10], rax
   0x4005d1 <main+27>    mov    edi, 0x20
   0x4005d6 <main+32>    call   malloc@plt <0x400480>
   0x4005db <main+37>    mov    qword ptr [rbp - 8], rax
 ► 0x4005df <main+41>    mov    rdx, qword ptr [rbp - 0x10]
   0x4005e3 <main+45>    mov    rax, qword ptr [rbp - 8]
   0x4005e7 <main+49>    mov    rcx, rdx
   0x4005ea <main+52>    mov    edx, 0x20
   0x4005ef <main+57>    mov    esi, 1
   0x4005f4 <main+62>    mov    rdi, rax
────────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────────────────────────
In file: /ctf/work/wolf/iofile/fwrite.c
   2 #include<stdlib.h>
   3 int main() {
   4     FILE *fp = fopen("output.dat", "wb");
   5     char *buffer = malloc(0x20);
 ► 6     fwrite(buffer, 1, 0x20, fp);
   7     return 0;
   8 }
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffe5b0 —▸ 0x602010 ◂— 0xfbad2488
01:0008│      0x7fffffffe5b8 —▸ 0x602240 ◂— 0x0
02:0010│ rbp  0x7fffffffe5c0 —▸ 0x400610 (__libc_csu_init) ◂— push   r15
03:0018│      0x7fffffffe5c8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
04:0020│      0x7fffffffe5d0 —▸ 0x7fffffffe6a8 —▸ 0x7fffffffe8db ◂— '/ctf/work/wolf/iofile/fwrite'
05:0028│      0x7fffffffe5d8 —▸ 0x4005fc (main+70) ◂— mov    eax, 0
06:0030│      0x7fffffffe5e0 ◂— 0x1f7b99608
07:0038│      0x7fffffffe5e8 —▸ 0x4005b6 (main) ◂— push   rbp
────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0           4005df main+41
   f 1     7ffff7a2d830 __libc_start_main+240
pwndbg> s
__GI__IO_fwrite (buf=0x602240, size=1, count=32, fp=0x602010) at iofwrite.c:31
31      {
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x602240 ◂— 0x0
 RBX  0x0
 RCX  0x602010 ◂— 0xfbad2488
 RDX  0x20
 RDI  0x602240 ◂— 0x0
 RSI  0x1
 R8   0x4
 R9   0x1
 R10  0x62c
 R11  0x7ffff7a7b6e0 (fwrite) ◂— push   r14
 R12  0x4004c0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe6a0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffe5c0 —▸ 0x400610 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffe5a8 —▸ 0x4005fc (main+70) ◂— mov    eax, 0
 RIP  0x7ffff7a7b6e0 (fwrite) ◂— push   r14
────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x7ffff7a7b6e0 <fwrite>       push   r14
   0x7ffff7a7b6e2 <fwrite+2>     push   r13
   0x7ffff7a7b6e4 <fwrite+4>     push   r12
   0x7ffff7a7b6e6 <fwrite+6>     push   rbp
   0x7ffff7a7b6e7 <fwrite+7>     push   rbx
   0x7ffff7a7b6e8 <fwrite+8>     mov    rbx, rsi
   0x7ffff7a7b6eb <fwrite+11>    imul   rbx, rdx
   0x7ffff7a7b6ef <fwrite+15>    test   rbx, rbx
   0x7ffff7a7b6f2 <fwrite+18>    je     fwrite+264 <0x7ffff7a7b7e8>
   0x7ffff7a7b6f8 <fwrite+24>    mov    eax, dword ptr [rcx]
   0x7ffff7a7b6fa <fwrite+26>    mov    r12, rdi
────────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────────────────────────
In file: /ctf/work/wolf/iofile/iofwrite.c
   26 
   27 #include "libioP.h"
   29 _IO_size_t
   30 _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
 ► 31 {
   32   _IO_size_t request = size * count;
   33   _IO_size_t written = 0;
   34   CHECK_FILE (fp, 0);
   35   if (request == 0)
   36     return 0;
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffe5a8 —▸ 0x4005fc (main+70) ◂— mov    eax, 0
01:0008│      0x7fffffffe5b0 —▸ 0x602010 ◂— 0xfbad2488
02:0010│      0x7fffffffe5b8 —▸ 0x602240 ◂— 0x0
03:0018│ rbp  0x7fffffffe5c0 —▸ 0x400610 (__libc_csu_init) ◂— push   r15
04:0020│      0x7fffffffe5c8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
05:0028│      0x7fffffffe5d0 —▸ 0x7fffffffe6a8 —▸ 0x7fffffffe8db ◂— '/ctf/work/wolf/iofile/fwrite'
06:0030│      0x7fffffffe5e0 ◂— 0x1f7b99608
07:0038│      0x7fffffffe5e8 —▸ 0x4005b6 (main) ◂— push   rbp
────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0     7ffff7a7b6e0 fwrite
   f 1           4005fc main+70
   f 2     7ffff7a2d830 __libc_start_main+240
pwndbg>

2.3 Examining the IO_FILE Structure

Before debugging, inspect the fp structure values:

pwndbg> p *_IO_list_all
$1 = {
  file = {
    _flags = -72539004,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
    _fileno = 3,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x6020f0,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x602100,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
pwndbg>

2.4 Examining the vtable

The vtable contents:

pwndbg> p *_IO_list_all->vtable
$2 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7ffff7a869c0 <_IO_new_file_finish>,
  __overflow = 0x7ffff7a87730 <_IO_new_file_overflow>,
  __underflow = 0x7ffff7a874a0 <_IO_new_file_underflow>,
  __uflow = 0x7ffff7a88600 <__GI__IO_default_uflow>,
  __pbackfail = 0x7ffff7a89980 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7ffff7a861e0 <_IO_new_file_xsputn>,
  __xsgetn = 0x7ffff7a85ec0 <__GI__IO_file_xsgetn>,
  __seekoff = 0x7ffff7a854c0 <_IO_new_file_seekoff>,
  __seekpos = 0x7ffff7a88a00 <_IO_default_seekpos>,
  __setbuf = 0x7ffff7a85430 <_IO_new_file_setbuf>,
  __sync = 0x7ffff7a85370 <_IO_new_file_sync>,
  __doallocate = 0x7ffff7a7a180 <__GI__IO_file_doallocate>,
  __read = 0x7ffff7a861a0 <__GI__IO_file_read>,
  __write = 0x7ffff7a85b70 <_IO_new_file_write>,
  __seek = 0x7ffff7a85970 <__GI__IO_file_seek>,
  __close = 0x7ffff7a85340 <_IO_file_close>,
  __stat = 0x7ffff7a85b60 <__GI__IO_file_stat>,
  __showmanyc = 0x7ffff7a89af0 <_IO_default_showmanyc>,
  __imbue = 0x7ffff7a89b00 <_IO_default_imbue>
}
pwndbg>

2.5 Initial State After fopen

After fopen initialization, no buffer has been established yet—all pointers are NULL. The _IO_fwrite function is located in /libio/iofwrite.c:

_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t request = size * count;
  _IO_size_t written = 0;
  CHECK_FILE (fp, 0);
  if (request == 0)
    return 0;
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    /* Code continues... */

At this point, the FILE structure has been allocated by fopen, but no I/O buffer has been created. The buffer pointers (_IO_buf_base, _IO_buf_end, _IO_write_base, _IO_write_ptr, _IO_write_end) all remain NULL, indicating that buffer allocation will occur during the first write operation.

Tags: IO_FILE heap exploitation fwrite glibc Pwn

Posted on Wed, 15 Jul 2026 16:12:40 +0000 by domwells27