Skip to main content

Binary Gauntlet 0 picoCTF 2021 Solution

A binary exploitation challenge where crashing the program in a specific way is the path to the flag.

Published: April 2, 2026Updated: August 13, 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 <url>/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 1Check protections and decompile the binary
    Observation
    The description mentions a pre-registered crash handler that prints the flag. So look at 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, so shellcode is viable, but there is no win() function and no leaked libc address to chain to. The mechanic is simpler than that: a pre-registered SIGSEGV handler already prints the flag on any crash. Hunting for a jump target wastes time, because you never need to control where execution lands.

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

    strings shows the format string from sigsegv_handler and the function names, but not the flag: that is read from flag.txt at runtime into a global buffer. strings only sees bytes already in the binary, and file I/O happens at execution time.

    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 2Spot the vulnerable strcpy
    Observation
    The decompiled main copies a 1000-byte input buffer into a 108-byte stack buffer with strcpy and no length check. That size mismatch is 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

    The fprintf in sigsegv_handler uses a fixed format string with the flag buffer as its argument, so nothing there is attacker-controlled. The real bug is strcpy moving attacker data from a 1000-byte buffer into a 108-byte stack buffer with no length check: a plain stack overflow, not a format string bug.

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

    The first fgets reads into local_10, but its result never reaches strcpy: the program discards it and reads again. Only the second fgets fills the source for strcpy(local_88, local_10). A long string at the first prompt is truncated and overflows nothing, so send the payload at the second.

    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 3Send the overflow and collect the flag
    Observation
    The 108-byte destination plus 16 bytes of saved frame pointer and return address means at least 124 bytes are needed to guarantee a crash. Sending 140 over nc reliably triggers sigsegv_handler and prints 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, and strcpy adds a null terminator at byte 109, which barely overflows anything. The saved frame pointer and return address start at 108 and run 16 bytes on 64-bit, so corrupting the return address takes at least 124. At 108 the crash depends on alignment; 140 guarantees the segfault.

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

    sigsegv_handler calls fprintf(stderr, ...), so the flag goes to file descriptor 2, not stdout. A plain pipe only captures fd 1 and shows nothing. Merge the streams with 2>&1, or use pwntools, which captures both descriptors.

    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

A stack buffer overflow happens when a fixed-size buffer takes more data than it holds, corrupting adjacent memory including the saved return address. Signal handlers registered with signal() or sigaction() run in the same process context and with the same privileges as the code that crashed, so a crash handler printing a secret is as exploitable as a win() function. The same pattern shows up in real crash reporters and error logging that leak sensitive state during a fault.

Related reading

Tools used in this challenge

Where to go next