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:
- Check remaining space in the output buffer; if available, copy target data to the output buffer
- If no remaining buffer space (or buffer not allocated), call
_IO_OVERFLOWto establish or flush the output buffer - After buffer flush, determine if remaining data exceeds block size; if so, write directly in blocks using
sys_write - If data remains after block output, call
_IO_default_xsputnto 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 writtensize: Size of each element in bytesnmemb: Number of elements, each of sizesizebytesstream: 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.