My exploit source code: https://gist.github.com/33d6e03d0c9a8c13f3ba
This challange is a ELF64 binary pwnable, which the libc.so library is also provided. The service provides some key-value operations, GET, PUT, DUMP and DEL, by a red-black tree (we spent lots of time on checking the implementation of the tree but nothing there). First we found an input sequence that may cause heap corruption:
The bug is in the function at 0x1040. The length of the buffer for the row key is not enough for the trailing '\0'. The last row key in the input above, which is exactly 24 bytes, causes an off-by-one null byte overwriting into the size entry of the next chunk in heap memory. This makes the next chunk shrink and clears the inuse bit so that the current chunk seems already freed. Since the row key buffer will not be freed when the row is not found in DEL operation, we use DEL to trigger this bug to avoid unwanted memory corruption and also overwrite the prevsize entry of the next chunk. After then, free the next chunk which the prevsize is overwrited and its forward chunk is also unlinked. The result is to put a large chunk which crosses over other chunks into the unsorted bin and we can get it back by malloc a data buffer with matching size later.
Since the data buffer is overlapping with other node structures, we can use GET to leak information on these structures or use PUT to overwrite them. Note that the PUT operation creates a new data buffer. So we have to call PUT twice to swap the current buffer back and fill it up. By overwriting the data pointer in the node struture, we are able to dump memory at arbitrary address by apply GET to the node (see the
leak() function in my exploit). First we can leak heap base address by reading the pointer in node strutures.
There is a trick to defeat ASLR for the libc base address. In the libc implementation,
mmap instead for large size (0x21000 is enough). In general, these pages will be placed at the address just before the .tls section. There are some useful information on .tls, such as the address of main_arena, the canary value of stackguard and a pointer which points to somewhere on stack with fixed offset.
However, the data buffer pointer can not be used as arbitrary memory writing, since the PUT operation always create a new buffer. Thus we have to make
malloc() return the address which we want to overwrite and fill it by node data. There are two approaches: free a fake chunk with the target address or corrupt the fastbin. Since
free() check both head and foot of the chunk, corrupt fastbin is easier and the only sanity checking is the chunk size in
malloc(). We can free a chunk in fastbin range and then overwrite its fd pointer with the target address. Then after
malloc() called twice, the fake chunk at the target address should be return, if the chunk size has been set correctly. (See the fastbin path in libc source for more details, which start from line 3331 to 3358)
We decided to overwrite the stack start from a return address entry with ROP chain. Thus we have to setup a valid chunk size on stack somewhere, which is before but close enough to a return address so that a tiny fastbin chunk can successfully cover it. We found that there is a buffer on stack in the PUT function at 0x1240. The buffer stores the data size and the rest space (total 16 bytes) is large enough for a 8 bytes size value. Also the address (start from [rbp-0x38]) is close to the return address entry. The valid length of ROP chain is only 16 bytes, which is not enough for a
system('/bin/sh') call, but its enough for jump to a
leave; ret gadget and migrate stack to second ROP chain which placed on heap in advance. Note that the stackgaurd canary should be overwrite with correct value.
Cat the flag by shell: