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 overflow a stack buffer to trigger a pre-registered crash handler that prints the flag.

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>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Check protections and decompile the binary
    Observation
    I noticed the challenge description mentioned a 'pre-registered crash handler that prints the flag,' which suggested I needed to understand the binary's protection profile and symbol table before crafting any payload.
    Run checksec to confirm the protection profile, then decompile with Ghidra or objdump to find the key functions.
    bash
    checksec --file=./vuln
    bash
    objdump -d vuln | grep -E '<[a-z_]+>:'
    bash
    # Alternatively open in Ghidra for full decompilation

    Expected output

    picoCTF{9595dc79...}
    What didn't work first

    Tried: Trying to find a win() or ret2libc target after seeing NX disabled in checksec output

    checksec shows NX disabled, which makes shellcode viable, but the binary has no win() function and no leaked libc address to chain. The actual mechanic is simpler: a pre-registered SIGSEGV handler already prints the flag on any crash. Looking for a jump target wastes time because you do not need to control where execution lands.

    Tried: Running strings on the binary hoping to find the flag embedded in the file

    strings vuln shows the format string from sigsegv_handler and function names, but not the flag itself - the flag is read from flag.txt at runtime into a global buffer. strings only inspects bytes already in the binary; dynamic file I/O happens at execution time and cannot be captured this way.

    Learn more

    checksec output for this binary shows: Partial RELRO, no stack canary, NX disabled, no PIE. Two functions stand out from the symbol table: main and sigsegv_handler. There is no win() function here.

    Decompiling main reveals the program reads the flag from flag.txt into a global buffer, then calls signal(SIGSEGV, sigsegv_handler) to register a crash handler. After that it accepts two rounds of input via fgets.

    sigsegv_handler is defined as:

    void sigsegv_handler(int sig) {
        fprintf(stderr, "%s\n", flag);
        fflush(stderr);
        exit(1);
    }

    This is the entire exploit surface: make the process segfault and the OS will call this handler, which prints the flag and exits. No shellcode, no ROP, no win address needed.

  2. Step 2
    Spot the vulnerable strcpy
    Observation
    I noticed from the decompiled main that a 1000-byte input buffer was being copied into a 108-byte stack buffer via strcpy with no length check, which immediately identified an unchecked size mismatch as the overflow vector.
    The decompiled main shows a 1000-byte input buffer fed into a 108-byte stack buffer with no length check. That mismatch is the entire vulnerability.
    bash
    # Ghidra / pseudocode of main (simplified):
    bash
    # char local_10[1000];
    bash
    # char local_88[108];
    bash
    # fgets(local_10, 1000, stdin);  // first prompt - one char
    bash
    # fgets(local_10, 1000, stdin);  // second prompt - vulnerable
    bash
    # strcpy(local_88, local_10);    // no bounds check!
    What didn't work first

    Tried: Using gdb to look for a format string vulnerability after seeing fgets feeding into fprintf in sigsegv_handler

    fprintf in sigsegv_handler uses a fixed format string "%s\n" with the flag buffer as argument, so the format string is not attacker-controlled. The real vulnerability is strcpy copying attacker-supplied data from a 1000-byte buffer into a 108-byte stack buffer with no length check - a classic stack overflow, not a format string bug.

    Tried: Targeting the first fgets call with a long input to trigger the overflow

    The first fgets call reads into local_10 but its result is not passed to strcpy - the program discards it and reads again. Only the second fgets call populates the source for strcpy(local_88, local_10). Sending a long string on the first prompt gets truncated and causes no overflow; you must send the payload on the second prompt.

    Learn more

    The program makes two fgets calls into the same 1000-byte scratch buffer local_10. The first call reads a single-character response; the second reads the string that is then passed to strcpy.

    strcpy copies bytes until it hits a null terminator and writes them into local_88, which is only 108 bytes wide. If the source string is longer than 108 bytes, the copy walks past the end of the destination buffer and overwrites adjacent stack memory, including the saved frame pointer and return address. When the function returns (or the CPU tries to fetch the next instruction from a corrupted address), the kernel delivers a SIGSEGV.

    Because the SIGSEGV handler is already registered, the kernel will call it instead of terminating silently, and it will print the flag before exiting.

  3. Step 3
    Send the overflow and collect the flag
    Observation
    I noticed the 108-byte destination buffer plus 16 bytes of saved frame pointer and return address meant I needed at least 124 bytes to guarantee a crash, so sending 140 bytes via nc would reliably trigger sigsegv_handler and print the flag.
    Pipe 140+ bytes into the remote service. Answer the first prompt with a single character, then send the long string. The crash triggers sigsegv_handler which writes the flag to stderr.
    bash
    # One-liner via Python pipe (answers both prompts):
    python
    python3 -c "print('a'); print('a'*140)" | nc mercury.picoctf.net <PORT_FROM_INSTANCE>
    bash
    bash
    # Or interactively:
    bash
    nc mercury.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # (first prompt) a
    bash
    # (second prompt) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    The flag appears on stderr from the crash handler. Some terminal setups mix stdout and stderr, so if you pipe the output you may need to redirect: 2>&1 or use pwntools to capture both streams.

    What didn't work first

    Tried: Sending exactly 108 bytes expecting that to be the exact offset needed to overwrite the return address

    108 bytes fills local_88 but strcpy also writes a null terminator at byte 109, which only barely overflows. The saved frame pointer and return address start 108 bytes in and are 16 bytes total on 64-bit, so you need at least 124+ bytes to reliably corrupt the return address. 108 bytes may crash inconsistently depending on alignment; using 140 bytes guarantees the segfault every time.

    Tried: Looking for the flag in stdout by piping nc output without redirecting stderr

    sigsegv_handler calls fprintf(stderr, ...) so the flag is written to file descriptor 2, not stdout. A plain pipe (nc ... | grep flag) only captures stdout (fd 1) and will show nothing. You need to merge streams with 2>&1 or use pwntools which captures both descriptors, or redirect in the shell: nc ... 2>&1 | grep picoCTF.

    Learn more

    Why 140 bytes? The destination buffer is 108 bytes. After filling it you also need to overwrite the saved base pointer (8 bytes on 64-bit) and the saved return address (8 more bytes), for a total of 124+ bytes. 140 gives a comfortable margin and reliably corrupts the return address to a non-mapped value, guaranteeing the segfault.

    The crash handler runs with the same privileges as the process, reads the flag that was already loaded from flag.txt at startup, and sends it to stderr. This is a purely passive exploit: you do not control where execution jumps, you just need the program to crash.

    This pattern (register a signal handler that leaks a secret, then let the attacker crash the process) appears across the Binary Gauntlet series. Later entries add mitigations such as a stack canary (which catches the overflow before the return) or NX (which prevents injecting shellcode), requiring different exploit strategies. Here, with all protections off and a helpful crash handler, sending junk is sufficient.

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{9595dc79...}

The flag is printed by the pre-registered SIGSEGV handler when the strcpy overflow corrupts the stack and crashes the process. No ret2win, no shellcode - just send enough bytes to crash.

Key takeaway

Stack buffer overflows occur when a fixed-size buffer receives more data than it can hold, corrupting adjacent memory including the saved return address. Unix signal handlers registered with signal() or sigaction() run in the same process context and with the same privileges as the crashed code, so a crash handler that prints a secret is just as exploitable as a win() function. The same signal-handler abuse pattern appears in real-world crash reporters and error logging code that inadvertently exposes sensitive state during a fault.

Related reading

Want more picoCTF 2021 writeups?

Tools used in this challenge

What to try next