Finally, after 4 months of break from PWN, I learned how to leverage the Use After Free attack. It feels really good to take a huge transition from stack-based attack to the heap. Stories later, let’s get started with the “Introduction to UAF”.
INTRODUCTION — Use After Free [UAF]
Skip to the exploitation part if you know the basics of UAF. When we allocate some data, Heap will create chunks based on the size. These chunks will have the metadata field and user data field. The minimum chunk size is 32 bytes (24 bytes for the user data and 8 for the metadata). Anything more than 24 bytes, the chunk size will get incremented by 16 bytes. So the next chunk will be 48 bytes and so on.
When we use “free” to de-allocate a chunk, the heap will reuse the chunk for other data that we subsequently use. Heap manages these free data as a linked list in Fastbins or Tcache bins. The freed chunk will have a pointer to the next freed chunk in the user data space.
But there is a small issue. When a variable pointing to a chunk is deleted, the variable is left as a dangling pointer.
A pointer pointing to a memory location that has been deleted (or freed) is called dangling pointer.
This means that we can control the chunk even after being freed. Now imagine this situation. What if we have control over the freed chunk and another chunk has been allocated there. We can use this chunk or even overwrite some data into the chunk and use it. This is called use after free. I think the name UAF makes sense now.
This is just a refresher. I would like everyone to know the basics of heap exploitation before going further. I’ll add some links below for references.
Let’s go through the binary and understand how it works. We will start from the main() function, it will make things a lot easier.
Initially, 2 allocations are done. One is the “m” and another is the “w”. We will debug this on gdb-pwndbg to understand how the chunks are created. We will set a breakpoint and run the program.
After setting a breakpoint, run the program.
Now let’s see how the chunks are created. For this use the “vis” command, a short form for “vis_heap_chunks”.
We can see that 2 chunks sized 48 bytes for Jack and 32 bytes for Jill have been created respectively. But why is there 2 extra chunks ??? We will come to that later. For now, we know that allocation is done.
Next, we have the menu-driven switch case. Let us go through all the options to get a clear view of how it works.
Option 1 is “use”. Here we have m->introduce() and w->introduce(). This is a pointer pointing to introduce() in man and women classes respectively.
An Arrow operator in C/C++ allows to access elements in Structures
Let’s see what gets printed when we use the first option.
2nd option is “after”. This is used for data allocation. It takes the size parameter as 1st argument and data (as a file) in 2nd argument in command line.
Without the arguments you cannot allocate data.
The next option is “free”. This deletes both variables “m” and “w”. We can confirm this by using the vis option.
Now the question is, how do we exploit this? There should be a way to open the shell right ?? If you see the source code you can find an interesting function.
give_shell() function when called can give us the shell. But it is not used anywhere in the program. How do we manipulate the program to get the function call?
Now, let’s get back to the chunk. Remember I said there was 2 extra chunk allocated. We see an address in the chunk. Let’s analyze it further
Let us analyze the address further and see if they actually mean something.
Aha! They are the vTables.
vTables contain the addresses to all virtual functions of a class.
The virtual function will have a special entry in the vTable. Remember give_shell() and introduce() were virtual functions??
Let’s see what the 2 addresses in vTable mean.
As expected. When we point to introduce(), they are pointed to the vTable->introduce(). This is how all vTable work. Now that we know 0x401570 points to introduce(), 0x401570–8 = 0x401568 points to give_shell(). Simple.
Introduce() is stored in the chunk when the program allocates data for “m” and “w”. What if we delete both the chunk “m” and “w”, allocate a new chunk, but fill the chunk with the give_shell() address in place of introduce(). This means that whenever we call the introduce() it will call the give_shell() function. Sounds fun right ?? let’s try it out.
For this, we need to create a file with the give_shell() address. Then we allocate a chunk with the file and any subsequent call to introduce() will invoke get_shell().
But we are missing out on an important point. There were 2 deleted chunks “m” and “w”. They are stored in bins. So if we allocate data of the same size, the bin will give us the chunk which was deleted most recently (LIFO). If we allocate data,it will give the “w” chunk first and then “m” for the second allocation.
If we use the “use” option, the first command is m->introduce() and the second one is w->introduce(). If we allocate only once, then only w->introduce() will have the give_shell() address, and m->introduce() will point to NULL as it was deleted as well. We are calling the m->introduce first in the program, If it points to NULL, it will give a segfault. We allocate twice to fill the chunk pointed by m.
To allocate data, we need a file containing the data. We write address (8 bytes) + 16 Junk characters. Then we run the program with the size of 24 bytes and file name. We will first free the m and w. The next 2 allocation has to be done. Next, we use the “use” option to call m->introduce. But instead of introduce(), we have replaced it with give_shell(). Thus popping us a shell.
I have written a script which automates this as well.
from pwn import *elf = context.binary = ELF(b"./uaf")
p = elf.process([b'24' , 'heap'])#context.log_level = 'debug'p.recvuntil(b"free")
log.info("Free the Man and Women Chunk")p.sendline(b"3")
log.info("Freed 2 chunks")p.recvuntil(b"free")
p.sendline(b"2")log.info("Allocate data in women region (Fifo Structure of Fastbins/Tcache)")p.recvuntil(b"free")
p.sendline(b"2")log.info("Allocate data in man region , As first one called is man->introduce()")p.recvuntil(b"free")
p.sendline(b"1")log.info("Call the replaced shell address chunk to get Shell")p.interactive()
Running this on remote will give you the flag. Make sure you create a file for yourself in the /tmp/ for writing your file and exploit.
Hope you understood the challenge. Do check out my other blogs.
Don’t forget to give some claps if you reached here :) Follow me for more write-ups.