Intro

The Great Escape pt3 was a pwnable challenge that I helped my teammate uafio solve during Insomni’hack Teaser CTF 2017. I thought this was a very interesting challenge because the binary is linked with libjemalloc, which uses jemalloc, the same memory allocator that Firefox uses for its heap management.

Most CTF heap exploitation binaries require you to pwn the ptmalloc2 memory allocator by taking advantage of the way ptmalloc2 frees and allocates heap chunks.
This was the first time I’d seen a challenge use jemalloc, so exploiting it was a great learning experience for me.
After the contest was over, I decided to redo the challenge alone from scratch and do a writeup to solidify my understanding of the concepts I learned.

This post is a result of that effort.

Heap Leak

The program first allocates 0x8 bytes to store a pointer to an encryption function.

Therefore, if we set our goal to be a string of size 0x8, we can guarantee that its corresponding heap region will be allocated immediately after the heap region for the encryption_method pointer, which is also malloc()‘d with a size of 0x8!

Furthermore, if we overwrite the pointer to the location heap region with a pointer to the encryption_method heap region, we will force the latter to get freed()‘d.

We can further abuse this later if we set our last_words to be 0x8 bytes, as the program will reallocate the old encryption_method heap region that was just free()‘d, and fill it with data that we control, introducing a subtle use-after-free condition when the program later calls whatever pointer address is stored in the old encryption_method heap region!

Essentially, this is what our user heap region looks like before we overwrite the pointer to the location heap region.

Notice the original pointer is still preserved and that our location heap region borders the encryption_method heap region since they are both malloc()‘d with size 0x8.

Now, after we overwrite the pointer to the location region with a pointer to the encryption_method region, free the encryption_method region, and reallocate the old encryption_method heap region with data that we can control, this is what all our relevant regions will look like.

Because we have now corrupted the encryption0 function pointer, we are able to control RIP the next time the encryption0 function pointer is called!

Stage 1 ROP: Libc Leak

Notice how the RDI register points to the beginning of our user region, where our user’s name resides.
If we set a ROP chain as our name, we can return to a xchg rsp, rdi ; ret gadget to perform a stack pivot and begin moving down our ROP chain to execute arbitrary code.

We can find such a stack pivot gadget in the ELF executable and use it reliably since PIE is not enabled.

Now, for the ROP chain, we need to address a few issues.

First, because our binary is not statically compiled, we don’t have many gadgets to work with from within the ELF executable, which may prove problematic later, depending on what we want our ROP chain to do.

But more importantly, we will need to dynamically calculate the addresses of libc functions to be able to return into them in our ROP chain and do anything meaningful.

Therefore, we need to leak libc somehow.

We can do this if we set our name to be a short ROP chain to leak libc.

In my actual exploit, I couldn’t find the right gadgets within the ELF executable to also control the values in RDX and RCX, which are the 3rd and 4th arguments passed into send(), respectively, but it didn’t matter, as the existing values in those registers were good enough to still be able to call send() successfully.

Once we leaked the address of atoi@libc, we can check to see which libc the server is using.

Now that we’ve found the correct libc, we can generate gadgets from it to craft a more useful ROP chain.

Stage 2 ROP: Dup2() Trick

Unfortunately we can’t just ROP to system("/bin/sh\0"); for this particular challenge because the shell will execute on the host and interact with the stdin and stdout file descriptors, when we can only interact with the socketfd file descriptor.

Therefore, we came up with 2 approaches to bypass this issue. We can either do it the hard way, which involves calling fopen() to read the flag file to a filestream, calling read() to read the contents of the file to a buffer, and finally calling send() to send the contents of the buffer to our socketfd file descriptor, OR we can do it the easy way which simply involves doing a dup2() trick.

The dup2() trick basically involves overwriting file descriptors 0 and 1, or stdin and stdout, with file descriptor 4, which is the socket we are interacting with.
Doing this should redirect all stdin and stdout to our socketfd, giving us an interactive shell.

The following is an example of what the FD’s may look like before the dup2() trick.

The harder way is very straightfoward, but one caveat is, we need a place to write our "flag" and "r" strings to so that we can pass pointers to them into fopen(). In my exploit I just wrote the strings to the .data section, since it is has enough slack space and is writeable. In the solution I’ve included at the end of this post, I’ve commented this ROP chain out, as I believe the dup2() trick is a much easier and more elegant solution.

Putting everything together, we are able to get a shell using the following exploit.