Arming the Use-After-Free()
Introduction
In this chapter, 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 the 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.
Fast Bin
Regular Bin
The regular bins are further divided into Unsorted bins, Small bins, and Large bins. 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 their 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 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.
FWD Pointer: Forward pointer to next chunk in the list. BCK Pointer: Backward 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 last 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 to 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 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.
https://pure.security/introduction-to-use-after-free-vulnerabilities/
If you to read more about this check out the articles below.
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 credit is 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 in 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 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 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 me summarize the steps for exploiting this 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.
Last updated