So on our regular CTF meetup last week, a friend told me about a pwn challenge he had tried at a recent CTF and failed, so naturally I was intrigued. So I asked him to send it over to me and I decided to give it a go.
And if nothing else I’ve learned a bunch about the heap and how ptmalloc handles things.
As the last part of the foreword, I’d just like to point out that solving this challenge took me about 3 days of work. Even though it may look straightforward in this writeup, I’ve been stuck on several parts of this for hours or even days.
A rough start
After unpacking the archive I found 3 files, the binary, a libc and a linker. So first things first, I tried to run it and was greeted by this mess
Now the file is there, so this confused me for a second. But my friend helpfully pointed out that the binary requests a specific linker, namely the one from the archive.
So let’s try again.
This isn’t actually so bad, since this time the error message is a bit more descriptive. And luckily for us on a linux system we can use LD_LIBRARY_PATH, so one more try.
Looking around
So before opening up IDA I figured I should check what things I might have to deal with. Luckily there’s the checksec utility to speed that up.
Okay, so this binary has basically all available defenses. So much for luck. Time to dive in. So playing around with the binary for a bit, there are really 3 operations that are interesting to us for now.
- Adding a component with an optional note at least 160 characters long
- Displaying a component and its note
- Deleting a note
So let’s take a look at the constraints in the binary.
Read
When trying to read a note, it mainly just checks if the +0x18 offset of the entry is set, so it makes sense to suspect that it’s an in-use flag. But as long as that’s set it will happily print whatever data the note is pointing to.
Delete
When deleting an entry, it first checks if the pointer is set and then frees it. And afterwards it sets the +0x18 offset to 0 marking the entry as not in use. BUT it does not check if the flag is set before freeing. Playing around for a bit we find our entry point.
Time to Exploit
So as a first step I created a small python script and wrote helper functions to add, read or remove to streamline the rest of the process.
Really heap exploitation can be a pain to deal with. So let’s set up a debugger so we can check out what’s going on. Luckily pwnlib makes this pretty easy. We can disable ASLR so that our binary will be at the same address every time. We’ll also set up a .gdbinit so that we can easily track allocations and frees.
Finding a leak
So let’s give this a shot. We’ll allocate a note, free it, allocate a note of the same size and free it using the first notes handle. This should allows us to still read the note through the second entry.
The idea is that after freeing the block of memory, ptmalloc sets some pointers in the chunk and reading them could give us our first information leak. Here’s the code.
And here’s our result.
After some experimentation it turns out that we don’t get anything here at the moment, it turns out that we just didn’t ask for enough memory and our chunk was at the top end of memory and was coalesced rather than put into a list. So let’s up our allocation size some and allocate another chunk after our current ones.
Converting our output to hex, we get the address 0x155555327ca0 back. Let’s ask gdb what it is.
Now that we have a leak of a libc address, we can work out the offset to the base of libc. So let’s check what that offset is by looking at the mappings in gdb.
The base of libc is at 0x155554f77000 so our offset is 0x155555327ca0– 0x155554f77000 = 0x3b0ca0. And with this we have a reliable way to calculate the base of our libc.
Forming a plan
Now that we know where the libc is we need to find a way to take over the control flow of the program. And our prime candidates are the two malloc_hook and free_hook symbols. So let’s find their addresses so we can use those later.
But in order for them to be useful we need an arbitrary write and for that we want malloc to return an arbitrary pointer to us. This part took by FAR the most time for me. There are a lot of techniques to achieve this and I highly recommend checking out the how2heap github repo from shellphish.
After a lot of trial and error I came up with this plan.
- Create two medium sized chunks back to back so that they are continuous in memory. This allows us to free at the address of the second one.
- Free both chunks
- Allocate a chunk as big as the two combined so that we can write something arbitrary at the address of the former second medium chunk.
- Write a fake chunk at that address and change its size so that it will be added to the tcache list, then free it.
- Allocate the big chunk again and overwrite the forward pointer of the fake chunk on the tcache list so that malloc will return that address after the fake chunk.
- ???
- Profit
Let’s put it into code.
And let’s give that a go in gdb.
As we can see, the last address returned seems to be in the libc and from the last 3 bytes it looks like it’s the free_hook address. Awesome.
Spawning a shell
Now this is usually the most straightforward part. But I ran into some issues here as well. one_gadget which is a great CTF tool, gave me three suggestions that I could go for, each with their own constraints
Sadly, the first constraint can’t be fulfilled the way we are redirecting control, because both malloc and free load the address of the hook into rax before jumping there. So we’re left with the stack. Long story short, the stack won’t work for us either here, but only barely, if we can push one more thing onto the stack, we’ll be lined up for the rsp+0x50 gadget. We can’t really control the stack here and we can’t really leak a stack address either to maybe take over the stack, so what are our options. After looking around for quite some time, I found this neat ROP gadget in the libc.
Now rdi is set to the address you want to free and it survives through the call to free_hook as well, so we can make one targeted jump to the value we write to an address we control and that will push one more value on the stack to make it line up. So let’s make some small adjustments to our code.
Now our stack should line up, even if we reenable ASLR. One last try.
And that’s it. I’m sure I’ve left out some details in places. Feel free to leave questions in the comments.