05 January 2020
Last year (or a few days ago), I took a look at HXP 36C3 CTF. Due to other things I was busy with (as well as my own incompetence), I couldn’t complete the only challenge I looked at, onetimepad. Nonetheless, I solved it after the CTF and decided to write on it due to the interesting heap manipulation involved. Skip to conclusion for exploit.
For the challenge, source is provided. Looking at the source, we see that it is a typical format for a heap challenge, where you can create, read, or rewrite a onetimepad.
Source gist
The main vulnerability lies in the fact that the rewrite function does not check the boolean used by the other two functions to indicate if a onetimepad is in use. As reading a onetimepad frees it, we can rewrite to the pad after read, causing a use-after-free.
The vulnerability
One tricky part of the challenge was that the entire code made use of only string-based functions such as strdup, strcpy etc. This meant that leaking cannot be done by aligning input with an address in memory due to the null-byte appended. Additionally, rewriting is limited to once only.
My idea behind the exploit was to first make a chunk that will belong the unsorted bin (i.e. > 1024 bytes) when free’d.
Initially
Then, we can allocate 3 chunks that use the original chunk in the unsorted bin. Chunk 1 is minimal in size, but ensures that the header of the chunk 2 can be overwritten by rewriting. The chunk 2 also has a size > 1024 bytes while chunk 3 < 1024.
After allocation
Now, we use rewriting to overwrite the size of chunk 2 in its header to a size of chunk_2_size + chunk_3_size + 0x10 (inclusive of header size of chunk 3). Then we free chunk 2 with the read function.
After overwriting
Now, we have a big chunk in the unsorted bin that covers both chunk 2 and 3. We allocate a chunk 4 the exact size of chunk 2, causing the chunk in the unsorted bin to be shrunk such it aligns up with chunk 3.
After second allocation
For a chunk in the unsorted bin, it’s first 2 QWORDs after the header are backward and forward pointers as part of a doubly-linked list of chunks. In this case where there are no other chunks, they point to a LIBC location. In other words, the contents in chunk 3 has been overwritten by LIBC pointers, giving us a chance to leak.
However, there was a slight problem which stumped me even to the end of the CTF. Reading chunk 3 meant free’ing it, which results in it being placed on the tcache. This overwrites its first 2 QWORDs, which coincidentally are also unsorted bin pointers.
Uh oh
Using chunks from the unsorted bin afterwards miraculously worked on my local LIBC 2.27, but not on the remote 2.28.
A series of checks. Number 4 fails
The solution was rather simple in retrospect. By freeing the previously allocated chunk 4, chunk 4 coalesces back with the remaining chunk, creating the original big chunk, with the 2 pointers being shifted back to the beginning. A (hopefully) clearer illustration is shown here:
The fix
The original two pointers remain where they are and we can leak them by reading chunk 3, which also places it on the tcache. We can then allocate in the unsorted bin again and overwrite the tcache linked list pointer in the first QWORD of chunk 3, pointing it to any location.
In this case, I chose to point it to __free_hook. Overwriting it with the address of system, all was left to do was to “free” a chunk that began with the string “sh;”, dropping to a shell. (It segfault’d locally due to LD_PRELOAD but essentially worked).
I am certain that my readers (if any) probably couldn’t understand my writeup with the horrendously drawn graphics so here is the exploit. As mentioned earlier, the challenge in retrospect wasn’t really too hard but sometimes we can become fixated on a problem so much that we miss out the obvious. I will work harder and hopefully achieve more in the CTF next year! :D