Pwnable.kr - UAF Writeup

Disclaimer: I am but a Padawan of the infosec arts. At the time of writing i have about two months of experience. If you or a fellow graybeard notices an error, please let me know. I don’t know what i don’t know, and i sure don’t know a lot.

Challenge Description:

Mommy, what is Use After Free bug? ssh uaf@pwnable.kr -p2222 (pw:guest)

First Impressions:

The Challenge Directory

Here we find the usual readable source code, executable binary, and unreadable flag.

Looking at the source code we see that it defines a simple virtual class (Human) and its two subclasses (Man/Woman).

The uaf/cpp main() function

The switch block is what’s interesting to us at the moment. We are prompted with three options:

[1] Call the introduce() method of both the Man and Woman objects.

Notice that both the Man* and Woman* from the new keyword are cast to Human*. Clearly, these objects are in the same class heirarchy, and Human is a common superclass. Further still, notice that we call introduce(), A function with the same prototype, using two upcasted objects. Either we are calling the introduce() method of the Human class, or there’s some as yet unseen polymorphism mechanism that deobfuscates our function call. Running the code, you can see that Man→introduce() and Woman→introduce() do not produce the same output for identical calls, therefore, they must be virtual functions.

(This is, of course, extremely obvious because the classes are both defined in this same readable file, but you can still figure out a lot without the class definitions.)

This is a good time to take a look at the Human class and its subclasses.

The Human class and it's subclasses.

We can immediately see the goal of this challenge: find a way to call give_shell() in the Human class. Notice that the class contains two virtual functions, only one of which is overwritten by the subclasses.

Now let’s look at the last option in the switch block.

[3] Delete (and deconstruct) both the Man and Woman objects.

Huh, so we can delete the objects, then use their newly invalidated pointers to call a function.

Segfault by accessing a dangling pointer.

So, it’s clearly possible to use our pointers after they are free’d, but what weirdness can we cause with this? Well, this is where you would start googling "use after free", and eventually you might find this extremely good article on glibc’s malloc() internals, and this very handy reference for glibc malloc().

Analysis

After reading the articles linked above, you might start to see how a Use-After-Free vulnerability can be exploited. Because it is extremely expensive for a process to allocate memory through syscalls like sbrk() and mmap(), glibc malloc() avoids this as much as possible by maintaining sorted "bins" of unallocated memory.

When malloc() is called, It searches through its binlists for the first free chunk of adequate size. If one is found, It resizes the chunk (if necessary) and returns a pointer to the start of the chunk’s data section. That means if we free() and object, then allocate something of the same size, there is a 99% chance that we will get a pointer to exact same chunk that we just free’d.

But how can this be exploited? Well, We know we can call a function from the Man and Woman classes after they are free’d and we know that we can potentially write arbitrary data to the chunk their dangling pointers refer to. The key insight to exploiting this, is abusing C++'s Vtables

Vtable memory block

C++ polymorphism requires some compiler trickery to work properly. For example, if i have an instance of Man and i cast it to Human, what happens when i call introduce()? We would expect Man→introduce() to be invoked, and indeed this is what happens. But how did we know (given a Human pointer, which was the "real" function to call?

This is where Vtables come in, and while the specifics vary between compilers (the standard specifies the language, not the implementation) we’ll cover the general idea. Every class instance in a C++ program has a Vtable, which is included as the first member of the object. This table is an ordered list of function pointers that point to the methods of this particular class in the class heirarchy. If a class inherits a method, then its Vtable entry for that method will be the same pointer as its parent (to prevent repeating an identical method).

Because an object’s type may not be known until runtime, the vtables for related classes must be in the same order.

Disassembly of the switch block

Here is a radare2 control flow graph of the uaf binary. notice the block at 0x13ab. We know from analyzing the cmp instructions to get here that this must be the block that calls Man→introduce() and Women→introduce(). And, indeed, we can see two call instructions, with nearly identical arguments.

Let’s go over this segment byte by byte:

mov rax, qword [local_18h]

This instruction loads a value at address local_18h to rax.

mov rax, qword [rax]

This is telling. We’re dereferencing the value we just stored. clearly, local_18h must be a pointer to something.

add rax,8

now we add an offset of 8 bytes.

mov rax, qword [rax] call rax

And we dereference the register again and call it!

This tells us some important information: We take a pointer to a pointer, add an offset, and call it.

Thinking back to the description of Vtables above, We can start to piece together what is happening here.

  1. local_18h is the start of the Vtable for this object

  2. The second entry of the vtable (local_18h+0x8) is the method we are calling.

  3. looking to the second call instruction, we can see that it uses the very next address as its vtable pointer, and the exact same 0x8 offset.

Now, thinking back to the offsets used for the Vtable pointer: We know that the Vtables of related classes must have the same ordering, so local_18h+0x8 and local_20h+0x8 must be different implementations of the same function (in this case, introduce()). But if introduce() is the second method in both these classes, What is the first? What is local_18h?

Exploitation

One last time, let’s go back to the source code, this time looking at the Human class.

Human class

We can see that there is indeed a method defined before introduce(), which is give_shell(). Now all the pieces snap into place. Since Man and Woman are derived classes of Human, they inherit the give_shell() function! Since this is very first method defined in the class heirarchy, this must be the first method in the vtable, local_18h and local_20h (remember, inherited methods will point to the same function.)

All we need to do to call give_shell() instead of introduce(), is subtract 0x8 from the vtable entry for introduce(), and allocate it to the free’d chunk. Then, when we call Man→introduce(), it’ll call the method before it in the vtable.

Great! we’ve solved the logic of the challenge without even needing to run the program. To get our final address, we just need to debug the program, and break on the call to introduce(). Taking the address of this method, subtracting 0x8, and writing it to the front of a 24-Byte chunk with option 2, should effectively confuse the program into calling give_shell() instead of introduce().

The rest is just tying everything together.

Execution

Let’s log back into pwnable.kr and gather the last bit of information we need, the address of the Man() vtable.

We load up the uaf binary in radare and debug it. disassemble main() and find the start of the block that calls introduce()

s sym.main

pdf

(this can also be done visually with VV instead of pdf, but i thought i’d show some other viewing modes.)

image::/images/blog/uaf/breakpoint.png

here we can see that the start of the block is at 0x400FCD. We’ll set this as our next breakpoint and continue.

db 0x400fcd

dc

Now let’s step through the code and look at the registers. We know from our static analysis that the address to introduce() will be in RAX at the call instruction.

Finally, the address of the introduce method

Notice that i have stopped execution at 0x400fd8, (marked in blue with RIP), this is immediately after the offset is added, so RAX must now contain the Vtable entry for introduce()

We can see from the debug registers that RAX is 0x401578. This is the address of the Vtable entry for introduce() (the currently marked instruction, mov rdx, [rax] has not been processed yet.) We had to add an offset of 0x8 to get here, so to get the first method in the Vtable we should subtract 0x10.

Now, all we have to do is write this value in little endian format to a file, pad it to 24 bytes, and allocate it twice in uaf after freeing the objects.

Victory!

And finally, we can read the flag.