Arming the Use-After-Free()

Ajin Deepak (AD2001)
InfoSec Write-ups
Published in
14 min readJan 17, 2022

--

Hello Hackers,

Let’s continue with our heap exploitation series. If you are new to this please go through the previous article.

https://medium.com/@ad2001/heap-exploitation-for-homo-sapiens-f166cd6a59fe

If you are already familiar with some heap basics then it’s okay to skip that. So today we will look into Use-After-free(). This is a common Heap Bug that is even relevant today.

Most of you may know about this bug. it’s actually very common and easy to understand. So let’s dive into this.

Uncovering the Allocation and Deallocation

Let’s see how heap manager allocates memory when we do a malloc().

So initially, the heap manager checks if there are any other free chunks of the same size as the allocated chunk. if there is it will return that chunk to the user. so what’s a free chunk?

It’s simply a chunk(memory block) that is freed by the free() function. When we free a chunk it will be added to a special linked list called “bins”.

So basically bin is a linked list of free chunks. When a memory block is free()’d it is stored in the list. They are sorted according to the size of the chunks.

There are mainly two types of bins.

  1. Fast Bin
  2. Regular Bin

The regular bins are further divided into Unsorted bin, Small bin, Large bin. We will mostly be dealing with fast bins. so let’s ignore the Regular bins for now.

The fast bin stores free()’d chunks of small size. As I said above the list of these fast bins is stored in a singly linked list. Addition and deletion happen from the front of this list (LIFO manner). There are 10 bins and each bin has chunks of the same size. For example, the bin for holding 16 byte-sized chunks should only have 16 byte-sized chunks, it should not contain any other sized chunks. So there sizes are 16, 24, 32, 40, 48, 56, 64, 72, 80 and 88.

In this article, we will only look at the exploitation of fastbin chunks.

So where was I at?

Yeah, So if there are any free chunks of the requested size are available in the bins.it is returned by the heap manager.

If there are no free chunks. The heap manager will check if there is enough space available at the top of the heap (Top chunk). if there is, the heap manager will allocate a new chunk out of that available space and use that.

The Top Chunk is a large heap chunk that holds the unallocated space. If there are no free chunks, the heap manager will make a new chunk out of this. This will become smaller and smaller with each allocation (if there are no free chunks)

Let’s show you the top chunk.

If there is no space in the top chunk, Then the heap manager will request the kernel to add new memory to the end of the heap, and then allocate a new chunk from this allocated space.

If even this fails, then it means that the heap manager failed to allocate memory and it will return null.

Let’s now look at the structure of the freed chunk.

Taken from azerialabs.com

FWD Pointer: Forward pointer to next chunk in the list.
BCK Pointer: Back pointer to the previous chunk in the list.

Let’s inspect this C program to understand the behavior and see some free chunks in action.

#include <stdio.h>
#include <stdlib.h>

void main(){
int *a = malloc(2);
int *b = malloc(3);
int *c = malloc(10);

free(a);
free(b);
free(c);

}

Compile this and run this inside gdb.

Disassemble the main function and put a breakpoint at the end of the function ( pop{r11,pc} ).

Run the program.

When it hits our breakpoint use the heap bins command to inspect the heap. We know that we freed() three chunks so there will be 3 free()d chunks.

This is the linked list we talked about. The head points to the newly added chunk. Here we can see that The head points to the lastly free()d chunk ie,0x21028.

These free()d chunks are added in the 0x10 (16 bytes) sized bins. But as we look at the size of the fast bin where our free()d chunks are located it shows size=0x8. This is a bug of the gef and it’s fixed in the latest version. So don’t worry about it .it should be added in the 0x10(16 bytes) sized bin. No bin stores 0x8 sized chunks, as we already know that the minimum sized chunk is 16 bytes sized. If we try to malloc() again it will return the chunk at the head of the corresponding sized fast bin.

For example, we try to malloc for a chunk with size 16 (Minimum sized chunk: 16 bytes), the heap manager will look into the fast bin which stores the 16 byte-sized chunks, and if there are available chunks it will be returned by the heap manager to the user.

Let’s edit the source code and add a malloc() call and verify if our theory is right.

#include <stdio.h>
#include <stdlib.h>

void main(){
int *a = malloc(2);
int *b = malloc(3);
int *c = malloc(10);

free(a);
free(b);
free(c);

int *d = malloc(2);

}

Let’s apply our theory here. The edited code below contains a new call to malloc which will allocate a new chunk of size 16 bytes and will be returned in the pointer ‘d’. So according to our theory the new chunk that will be returned to the pointer ‘d’ will be the last chunk of the same size which we free()d. This chunk will be added to the head of the corresponding sized bin. We only allocated and free()d 16 byte-sized chunks, so there will be only one fast bin (16 byte-sized). So the newly added malloc() call will return the last 16 byte-sized chunk that we free()d which was pointed by ‘c’ into the pointer ‘d’.

Let’s check our thesis using our debugger.

This is before our last malloc() call. we can see all our free()d chunks in the fast bin. If you look at the head of the fast bin it contains the chunk having the address “0x21028”. This is the chunk that will be returned to the next malloc call().

Let’s step over the malloc call() and see.

If we look now the chunk pointing to the address “0x21028” is missing from the fast bins.

Let’s also inspect the r0. (r0 will contain the address to the allocated chunk we malloc )

As expected this contains the same address as that of the last free()d chunk that was in our fast bin. So this confirms our theory.

This is how the bins are utilized for storing free()’d chunks. Now let’s look at the use-after-free vulnerability.

Use-After-Free

I already wrote this briefly in the previous article. So to free() a chunk we use the free() function,right? .But even though we free() this we still need to assign the pointer to “NULL” Because even if free the pointer it will still point to that particular location. So using the memory still after being free()d is called use-after-free. This is the origin of our bug starts.

If you look at cve mitre we can see that it’s still even relevant and common today.

If you to read more about this check out these articles below.

www.blackhat.com%2Fdocs%2Feu-16%2Fmaterials%2Feu-16-Wen-Use-After-Use-After-Free-Exploit-UAF-By-Generating-Your-Own-wp.pdf&usg=AOvVaw01DzaZqA91ZqSVKeBkR13V

Let’s understand this using an example program below to gain some more clarity.

#include <malloc.h>
#include <stdio.h>

typedef struct function{
void (*funct_ptr)();
}ptr;

void one(){
printf(“This is function one \n”);
}

void two(){
printf(“This is function two \n”);
}

void main(){
ptr *malloc1 = malloc(sizeof(ptr)); //First malloc
malloc1->funct_ptr = one;
printf(“ Calling malloc1->funct_ptr \n”);
malloc1->funct_ptr();
printf(“ Freeing malloc1 \n”);
free(malloc1);
ptr *malloc2 = malloc(sizeof(ptr)); //Second malloc
malloc2->funct_ptr = two;
printf(“Calling malloc2->funct_ptr \n”);
malloc2->funct_ptr();
printf(“Trying to call malloc1 \n “);
malloc1->funct_ptr();

}

There are two functions here and a structure called “function” which contains a function pointer. we will be using these function pointers to store the two functions in our program (function ‘one ‘ and function ‘two’).

As you can see in the source code we are using malloc() and reserving the space to store the function pointer in the structure. This will allocate a chunk for storing the address of function “one”. Later malloc1 is assigned with function one and it’s called.

malloc1->funct_ptr = one; //Assigning function one

printf(“ Calling malloc1->funct_ptr \n”);

malloc1->funct_ptr(); // Calling the function

After calling the function, it’s freeing this chunk using the free().

free(malloc1);

But the issue is that even after freeing this chunk, the pointer ‘malloc1’ still points to the chunk that is returned from the first malloc. Even worse it is still being used this is where it becomes an actual vulnerability. Using after freed, right?

So what can we do to fix this?

We should simply assign “NULL” to this pointer so that it won’t point anywhere.

But the issue is that even after freeing this chunk, the pointer ‘malloc1’ still points to the chunk that is returned from the first malloc. Even worse it is still being used after being freed. (Last line of the source code)

malloc1->funct_ptr();

This is where it becomes an actual vulnerability. Using after freed, right?

After this, there is another function pointer that has the address of the second function “two”. So “malloc2” contains the address of the second function and function two is called later.

ptr *malloc2 = malloc(sizeof(ptr)); //Second malloc
malloc2->funct_ptr = two;
malloc2->funct_ptr();

Let’s compile this and run this to see the vulnerability in action.

Let’s break down the output.

First, its calling function one uses the function pointer “malloc1”. We can see the output of the printf function from function one “This is function one “.After that, the chunk which was used to hold the function pointer of function one was freed(). As we know this free()’d chunk will go to bins.
Even though it is free()’d the pointer still points to the first chunk that contains the function pointer of function one.

Next, it calls the second function using the function pointer “malloc2”.We can see the output of the printf function in this function too “This is function two”.

After this, we are calling function one using our function pointer “malloc1”.But if you look at the output it’s the output of the printf() function from function two. But we never assigned function two to “malloc1”. Then how did this happen ??

So what happened was that when we free()d the first chunk (malloc1) .it will be added to the head of the corresponding bin of its size. So when we try to allocate the next chunk it will look into the bins to see there are any free()d chunks of that exact size. If there is a chunk of that size it will be returned by the heap manager. So here our second malloc is allocating the same size so it will return the first chunk we free()’d which was used to store the function pointer of function one.

When this is returned, we use this to store the function pointer of “function two”.As we assign “function two” to the returned chunk, the contents inside the chunk ie, the pointer to “function one” will be replaced by the pointer of function two. Don’t forget that “malloc1” still pointers to this chunk even after it’s free()’d. Now the function pointer of function one is replaced by function two inside the chunk. So When we try to execute the function inside the function pointer using malloc1. It will execute the second function as it now has the pointer to “function two” because the same exact chunk was used to store the function pointer of “function two” using “malloc2”.

ptr *malloc2 = malloc(sizeof(ptr)); //Second malloc

malloc2->funct_ptr = two;

malloc2->funct_ptr();

So this is why we saw function two get executed instead of function one which was assigned first to malloc1.

Let’s do a simple challenge to understand this better.

Challenge UAF

Compile the source code below.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
char *name = 0;
char *pass = 0;
while(1)
{
if(name) printf(“name adress: %x\nname: %s\n”,name,name);
if(pass) printf(“pass address: %x\npass: %s\n”,pass,pass);
printf(“1: Username\n”);
printf(“2: Password\n”);
printf(“3: Reset\n”);
printf(“4: Login\n”);
printf(“5: Exit\n”);
printf(“Selection? “);
int num = 0;
scanf(“%d”, &num);
switch(num)
{
case 1:
name = malloc(20*sizeof(char));
printf(“Insert Username: “);
scanf(“%254s”, name);
if(strcmp(name,”root”) == 0)
{
printf(“root not allowed.\n”);
strcpy(name,””);
}
break;
case 2:
pass = malloc(20*sizeof(char));
printf(“Insert Password: “);
scanf(“%254s”, pass);
break;
case 3:
free(pass);
free(name);
break;
case 4:
if(strcmp(name,”root”) == 0)
{
printf(“You just used after free!\n”);
system(“/bin/sh”);
exit(0);

}
break;
case 5:
exit(0);
}
}

}

Link : https://pastebin.com/LKfaUK6v

I got this source from a paste. So all credits to the original owner. i just edited this to add the call to the system function

So anyway our task is to get the shell. we can only get the shell if we are the root user. but we can’t set the username as “root”.we can enter any other name other than root.

So let’s break down the program options

1 → Will allocate some chunk for storing the username
2 → Will allocate some chunk for storing the password (same size as username)
3 → frees the username and the password using the free function
4 → Tries to spawn a shell if the user is root.

Okay, Now let’s check where the vulnerability lies.

Try looking at the third option. It is used to reset the username and password using the free function. But the problem is that even after these pointers are freed, they are still pointing to the same location because they are not NULLified.

As they are of the same sizes they will go the same fast bins.

So how will we exploit it? Try to think of a way in which we can point to the username to the location having the text “root” by leveraging this vulnerability.

I highly recommend you to try this out by yourself for some time.

Let’s discuss the easy way to solve this.

Firstly, we can use option 1 to allocate a chunk for storing the username. Then, let’s use option 2 to allocate a chunk for storing the password.This will allocate two chunks of the same size.

Let’s trigger the vulnerability by using option 3 to reset the username and password. This will free them both. As they are of the same size they will go to the same sized fastbin.

if you look at the code for option 3, the “pass” pointer which is used to hold the password is freed() first followed by the “name” pointer which is used to hold the name. So the chunk used for the password will go to the bin first followed by the chunk used for saving the username. So the head of the fastbin will point to the chunk which is used to save the username.

Let’s see this in action.

These chunks are of the same size and we can see the address in the output. Next, we can use the reset option to free() the username and password.

As they are of the same size, they will belong to the same sized bin.

We can see that even after this is free()d it’s still pointing to the same location.
We also see some gibberish characters in that location, this is because when the chunk is free()d, there are additional metadata included. This metadata will overwrite the data in the chunk. you don’t need to mind about that.

Let’s confirm if they are in the same bin using gdb.

(You don’t need to worry about the wrong size of chunk shown in the bins. This is mainly due to the bug in gdb)

As we can see, the head of the bin contains the pointer to “user chunk” which was the last free()d chunk followed by the “password chunk”.

Now if we try to allocate any chunk for storing the password and username, the free()d chunk which is at the head (previous chunk used for username) of the bin will be returned.

I think now you got the idea of exploiting this.

So what we have to do now is to use option 2 to request a chunk for storing the password. This will return the free()d chunk at the head of the bin which is still pointing to the username.

So we try to add new content in this chunk, the previously free()d username pointer will point to this as there are the same chunks. As we already know by now, we can only get the shell if we are the root user and we cannot directly set the username as root, the program won’t accept that. So the trick here is that if we set the newly requested password chunk’s content to “root”, then the previously free()d username chunk will point to this content as there are the same chunks.

As a result, the content of the previously free()d username chunk will get replaced and will point to the newly set password.

So we if set the new password to “root”.Then the username will also point to “root”.

And then if we try to log in, we will easily get the shell as we are the root user.

Let’s just do this and get over with this challenge.

Let me summarize the steps for exploiting this for one last time.

1 →Use option 1 to allocate a chunk for storing the username.
2 →Use option 2 to allocate a chunk for storing the password.
3 →Free the chunks using option 3.
4 →Use option 2 to allocate a password chunk so that it can rewrite the contents of the previously allocated username chunk.
5 →Use option 4 to log in and get the shell.

Let’s do this in order to get the shell

Finally, we got our beautiful shell :)

So I guess it’s time to wrap now. I hope you understood something about Use-After-Free vulnerabilities in general.

Try doing other use-after-free CTF challenges and try to exploit them by yourself.

Have a good day :)

Mentioned articles and links : https://azeria-labs.com/

For any queries you can contact me

Facebook : https://www.facebook.com/i.am.ultralegend

Instagram :https://www.instagram.com/hagane_no_rekinjutsushi/

if you want to support me or learn ARM exploitation please check out my course:https://www.udemy.com/course/reverse-engineering-and-binary-exploitation-in-arm/?referralCode=8C725D513E77420A0CB

--

--