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.
Setup
Download the binary for local analysis, then connect to the remote service to get the flag.
wget https://mercury.picoctf.net/static/.../vulnchmod +x vulnnc mercury.picoctf.net <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Check protections and decompile the binaryObservationI 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.bashchecksec --file=./vulnbashobjdump -d vuln | grep -E '<[a-z_]+>:'bash# Alternatively open in Ghidra for full decompilationExpected 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:
mainandsigsegv_handler. There is nowin()function here.Decompiling
mainreveals the program reads the flag fromflag.txtinto a global buffer, then callssignal(SIGSEGV, sigsegv_handler)to register a crash handler. After that it accepts two rounds of input viafgets.sigsegv_handleris 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.
Step 2
Spot the vulnerable strcpyObservationI 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 charbash# fgets(local_10, 1000, stdin); // second prompt - vulnerablebash# 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
fgetscalls into the same 1000-byte scratch bufferlocal_10. The first call reads a single-character response; the second reads the string that is then passed tostrcpy.strcpycopies bytes until it hits a null terminator and writes them intolocal_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.
Step 3
Send the overflow and collect the flagObservationI 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):pythonpython3 -c "print('a'); print('a'*140)" | nc mercury.picoctf.net <PORT_FROM_INSTANCE>bashbash# Or interactively:bashnc mercury.picoctf.net <PORT_FROM_INSTANCE>bash# (first prompt) abash# (second prompt) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaThe 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>&1or usepwntoolsto 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.txtat 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.