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 <url>/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 1Check protections and decompile the binary
ObservationThe 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.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, 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:
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 2Spot the vulnerable strcpy
ObservationThe 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 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
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
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 3Send the overflow and collect the flag
ObservationThe 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):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, 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.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.