Time Jump Planner
No ROP, no SROP, full RELRO, ASLR, DEP, no execve pwn inside of QEMU
Last updated
No ROP, no SROP, full RELRO, ASLR, DEP, no execve pwn inside of QEMU
Last updated
Time Jump Planner was a pwn problem hosted by Battelle written by playoff-rondo. This challenge is a pwn problem hosted within a docker container and running through qemu-x86_64 usermode emulation. It is also running a "libjail.so" plugin within qemu to further restrict exploitation options.
The file contents of the problem are shown below:
We're provided libc, ld-linux , and the binary, so this is our first clue that we'll likely need to leak library addresses when working through this problem.
To solve this problem I approached it as an arbitrary write/arbitrary read vulnerability with a modern GLIBC.
Identify vulnerabilities
Leak out addresses
Overwrite libc __exit_funcs
setjmp/longjmp to JOP chain
While looking through the libjail.so plugin for QEMU I saw the following registered callbacks:
qemu_plugin_register_vcpu_tb_trans_cb
will register a call back for every translated basic block executed by QEMU. and qemu_plugin_register_vcpu_syscall_cb
will register a callback for every single syscall run by the usermode program.
The qemu_plugin_register_vcpu_tb_trans_cb
will check every "call" and "ret" instruction, matching each call's return address with the corresponding "ret" instruction. If the values do not line up, a message "NO ROPPING!" is printed and the program exits.
Additionally within the syscall callback we see even more restrictions. "execve" is blocked, which stops one_gadget,system("/bin/sh"), and popen from working for exploitation.
However within this function it is also revealed that there is a backdoor syscall that will print the flag. When called with syscall(0x5add011,"please_give_me_flag",0x6942069420) the flag will be opened and written to STDOUT.
When run, the program is designed to print a menu and offer a number of choices for "time traveling". A current year is specified, and a number of options for "quick jumping" are provided, kind of like speed dialing for old phones.
The "add" and "remove" functions both allow you to add and remove arbitrary numbers from this variable. They will index into the "year_list" variable and take an arbitrary 64bit unsigned integer and set it. This looks like a good setup for arbitrary write!
The "quick_jump" function will set "curr_year" equal to one of the values from the "year_list". While setting the value, quick_jump will only check indexes -1 < index < 11, which enables us to index past the "year_list" array. I'll use this function later to leak out pointers.
The "manual_jump" function is the core to setting up the arbitrary write and read primitives. At first look, the double format string is confusing and it looks like it allows you to specify a number of bytes to read into a "read_buffer" only up to 30. However the check only checks to ensure that our provided input length is less than thirty which means we can use either "-1" to leak a stack value, or "0" to mean read as much data as possible.
The scanf above is a buffer overflow, overwritting the read_buffer, a 40 byte buffer, followed by the saved RBP register and then the saved RIP register.
Normally at this point we would try and use the PC overwrite from this function to start constructing a ROP chain, however we are unable to capitalize on the chain with the QEMU plugin! So we'll need to find another way to gain PC control later. For now we can safely overwrite the RBP register and stack_cookie.
The "list" function is also a built in information disclosure vulnerability. The year_list is 5 long values long, however the "list" function will print out 10 long values starting from the "year_list".
I recovered 4 leaks that I ended up using for the rest of the problem. The first leak is within "manual_jump". By setting the number of characters to "-1". printf won't be able to read any bytes into the read_buffer and will instead "%s" an earlier stack argument! I think it's an environment variable since on my QEMU it showed my execution path.
These leaks all come from using the "quick_jump" function since it will print out a value at the offset location with "%lu", which lets us read 64byte values out them. Iterating over the 10 options, it was quickly found that the following offsets corresponded to the following leaks:
Offset 5 - Stack cookie
Offset 7 - libc leak
Offset 9 - binary address leak (main address)
Using GDB I found that I needed to subtract "0x29d90 from the libc leak to get the base address. I was able to use pwntools for my other addresses.
With the stack cookie leak, we can now revisit the "manual_jump" function and overwrite the RBP register! Since "curr_year" and "year_list" from our main function were references by their offsets to RBP, that means we can set these values to anywhere and obtain arbitrary read and write.
The code below has my function to interact with manual_jump to set the RBP, and the wrapper function to do reads and writes! I wrote wrapper around the read4 and write4 to allow me to read and write 8 bytes at a time too for my final exploit.
I was stuck on this part for a little while. We're provided with a 2.35 version libc, which means __free_hook and __malloc_hook overwrites won't work anymore. However I came across this write up talking about overwritting the "__exit_funcs" variable!
Given an arb read/write, we can overwrite this special structure to execute any function we want with control over the first argument. When "exit" gets called, this function is responsible for checking all the DTORS and to cleanup all the linker stuff. However for most C binaries, this means only the "_dl_fini" address will be called.
When exit() is called it will actually call _run_exit_handlers
with the "__exit_funcs" variable as an argument.
By opening the binary in ghidra we can see where that pointer is located. For this version of libc, it's located at libc_base_address + "0x219838".
We can't immediately overwrite this structure with our own values yet. libc has started "encrypting" pointers using a special xor key, so we won't be able to supply a new pointer without knowing this value. However the function that is always called happens to be _dl_fini, a function from the ld-linux library!
The library is always aligned after the page, so we can actually use our libc leak to find the offset from our libc base address to this function. At this point, I stepped through the exit_handlers code until the _dl_fini function was decrypted and then subtracted it against my libc base to get my new offset.
Since the "encryption" is based off an XOR operation, and since XOR operations are reversible if you have two pieces, we can XOR the encrypted pointer we read out of the structure with our known address from the libc leak and recover the key.
At this point I stole the encrypt/decrypt code from the above mentioned writeup and was able to recover the libc key!
The exit_function_list struct looks like the following:
It is a linked_list of exit_functions and includes and index and a next pointer. Those exit_function structs look like the following below:
Since the structure is a union across a couple different types, it will always be of the length of that largest "CXA" type. So I opted to use the "flavor". Which means in memory, for a single function our function list looks some like below:
So for the above code where I'm getting the address of exit_function list, I'm indexing 0x18 bytes into it to get the encrypted function_ptr of _dl_fini.
At this point we have a leak for our main binary and libc, so we can use our BSS section to write a fake exit_function_list with our arbitrary write. Then set the __exit_funcs pointer in libc to point to it.
This allows us the primitive of call(arg) while controling both RDI and the RIP. This primitive isn't strong enough yet to call our backdoor syscall, so there is still room to develop.
❌ At this point if we didn't have the restrictions, we could call system("/bin/sh") and win. However "system" relies on execve and gets blocked by the syscall filter.
❌ We also can not fake-rop with chains of rop gadgets in our exit_function_list since registers aren't preserved across function calls.
❌ One of my first thoughts was to trigger an SROP, but when calling "sigreturn" QEMU errors with:
❌ I tried setcontext/getcontext, however, getcontext will use "ret" to set the RIP value after all the other registers are set and will trigger the "NO ROPPING!" message again.
❓I suspect that FSOP could have been a solution as you can turn a FILE structure you create into a call primitive with RDI,RSI, and RDX control, however it also looked like a lot of work to learn.
✅ My last resort ended up being setjmp/longjmp. Since I can control rdi, I can point setjmp's jmp_buf argument to the BSS section again, and then modify the saved register. This ended up working and I turned it into a JOP chain.
I didn't see any blog posts written about this, so I want to try and cover as much about it as I could. setjmp and longjmp are used mostly for signals and error handling in c. At the cost of losing most of your registers you can restore a given thread back some function context before an error occurred.
Calling setjmp with a pointer will dump the following register to the given pointer:
Using our arbitrary write primitive from earlier, we can then go in afterwards and change these values. The advantage of using longjmp, is that libc's longjmp will not use "call" or "ret" to return execution to the given state. The would break the purpose behind this error handling code.
These registers are not amazing to control, however with libc at our disposal we have a couple options to extend into jump oriented programming (JOP).
Fortunately the exit_function_list primitive gives us as many executions as we'd like. So using this primitive, we want to use "setjmp" to store a valid jmpbuf somewhere. (We could just write a fake one and then longjmp directly into it too.) However I want to use this as a chance to show how strong the exit_function_list primitive is.
Our exit_function_list can be expanded to call two functions, by increasing the index and adding a second exit_function structure.
So now once setjmp returns, the exit_handler code will run the second function written in the fake exit_function structure, that being our main function of the executable.
This allows us to continue using the arb read/write to overwrite the jmp_buf's values. You can do this up to 32 times, before needing to set the next_ptr to another exit_func_list structure.
At this point we have control over a handful of registers, but need to get control over our argument registers. Unfortunately I couldn't find a "one JOP" so I ended up chaining two JOP gadgets I found.
In general you want your first gadget set rax to a value you control and to end with "jmp RAX". The gadget I found to do that was inside of libc. This gadget was found in the "twalk_r" function.
I liked that RDX and RAX were being set from r12 and r13. RBP is a register we control too, so this gadget let me set RAX,RDI,RDX and jump to a new location. I didn't have to give up RSP so I can reuse it for another gadget. This gadget was important to set RDX.
My second JOP gadget continues with the "jmp rax" idea and loads my eventual RAX value from RSP, which we still control. This gadget appears to come from the "register_file" hidden function inside of libc for nss_files.
This gadget is great and gives us the rest of the control we need for controlling arguments, RDI,RSI,(RDX preserved from the last JOP), and RIP (from rax).
ROPgadget was giving me some issues when searching for gadgets, so I ended up using objdump to manually find my JOP gadgets:
Hooray! We can now call a function with RDI, RSI, and RDX registers set. The final execution flow looks like this:
With the whole approach finally in mind, I was able to make the finishing touches. After returning from the setjmp call I made sure to write the "please_give_me_flag" string to bss so I could point to it.
When using the __exit_funcs structure you need to encrypt your pointers:
I created a place to store the jmpbuf and wrote my first fake structure:
Once I returned back to main I used that old stack_leak values as scratch space for my JOP. ( I could have used the BSS section here) Then wrote what my RSP should point to in order to populate my arguments for the second JOP.
longjmp seems to use the pointer encryption for libc, so RIP/RSP/RBP registers all need to use the encrypt with the XOR key.
Finally we can setup a new exit_functions structure, point the libc variable to it, and jump!
My final solution can be found here. It includes far more arb reads and notes.
Libc G.O.T. oriented programming?
The challenge author created a technique called LGOP that relies on the partial RELRO compile for the provided libc binary for his solution. I would love a write up explaining how it works, but from briefly going over his solution, it looks like it bypasses the call/ret checking by overwritting GOT entries in libc to point to special gadgets. ( link to technique here )
These gadgets appear to perform register operations, followed by an additional call into a GOT entry. Since these GOT entries are overwritable, he has able to string together a series of these to redirect control flow across several calls.
He was able to control RDI,RSI,RDX with these gadgets and then eventually call the syscall libc symbol.
The backdoor syscall is nice, by if we're trying to obtain shell access, the execveat syscall is a nice syscall to consider.
When "pathname" is an absolute path, dirfd is ignored and it functions exactly as execve.
Since it is a different syscall number than execve it provides an alternative to the traditional execve("/bin/sh", NULL,NULL) call for ctfs.
This would have been my next step if the backdoor syscall wasn't available in the libjail.so