Double free() attacks on ARM.
Last updated
Last updated
In this chapter, we will dive into double-free bugs. So what’s a double-free bug?
So basically it’s a vulnerability that occurs when a variable is freed twice. When we try to free() a resource more than once, it corrupts the data structures of the heap allocator.
For more details, you can check the link below.
Let’s go through an example and see this.
#include <stdio.h> #include <malloc.h> int main(){
char *a = (char*) malloc(10); char *b = (char*) malloc(10); char *c = (char*) malloc(10);
printf(“a is at memory %p \n”,a); printf(“b is at memory %p \n”,b); printf(“c is at memory %p \n”,c);
free(a); printf(“Freeing a \n”);
free(b); printf(“Freeing b \n “);
free(c); printf(“Freeing c \n”); free(c); printf(“Freeing c twice \n”);
return 0;
}
Compile this source code in GCC.
pi@raspberrypi:~/asm/tmp $ gcc double-free.c -o db
So there are 3 pointers in the program.
char *a = (char*) malloc(10); char *b = (char*) malloc(10); char *c = (char*) malloc(10);
So there are 3 pointers in the program which hold three memory locations in the heap. They are freed one by one but in the last case, the pointer c is free()d twice.
Let’s run this and see what happens.
As you can see in the third case, the program was aborted due to double-free corruption. The double-free bug in the program was identified and execution was aborted.
So the question arises if the bug is identified within the program how does this bug still exist these days or how do we exploit this?
if we look at the error message, it says:
“*** Error in `./db’: double free or corruption (fasttop): 0x00021028 ***”
So what’s fast top?
It’s a simple check to prevent double-free vulnerabilities. it checks if the top of the bin is not the record we are going to add to the bin.
For example, when we are freeing a chunk it will get added to the corresponding bin. if the chunk that is to be freed is the same as the one on the top of the corresponding bin then that chunk will not be added. This check in the case of fastbins is fasttop. So we know that we cannot free a chunk two times consecutively.
So is there any way to bypass this check and free a chunk twice?
Yes, Because the check is done only with the top record in the bin. we can easily bypass this by freeing another chunk in between this and we can free the freed chunk again. For example, to double free the pointer c (above example) we first free c and then free a or b, this will bypass the fasttop check, and finally, we can free the pointer c again. This will free the memory location in the c once more causing the double-free bug.
Let’s see this in action
Firstly let’s edit the code and recompile the code.
#include <stdio.h> #include <malloc.h> int main(){
char *a = (char*) malloc(10); char *b = (char*) malloc(10); char *c = (char*) malloc(10);
printf(“a is at memory %p \n”,a); printf(“b is at memory %p \n”,b); printf(“c is at memory %p \n”,c);
free(a); printf(“Freeing a \n”);
free(c); printf(“Freeing c \n”); free(b); printf(“Freeing b : To bypass fastbin top check \n”); free(c); printf(“Freeing c again \n”);
As we can see now we have successfully bypassed the top check and triggered the double-free vulnerability.
#include <stdio.h> #include <malloc.h>
struct s{ int num; };
int main(){ struct s *a,*b,*c,*d,*e,*f;
a = malloc(sizeof(struct s)); printf(“a is at %p \n”,a);
b = malloc(sizeof(struct s)); printf(“b is at %p \n”,b);
c = malloc(sizeof(struct s)); printf(“c is at %p \n”,c);
free(a); printf(“Freeing a : %p \n”,a);
free(c); printf(“Freeing c : %p \n”,c);
free(b); printf(“Freeing b : %p \n”,b); //Bypassing top check
free(c); printf(“Freeing c : %p again \n”,c); //Double free
printf(“The freed bins will be added to the corresponding bins \n”); printf(“Now we are again allocating memory using malloc into three pointers d,e and f \n”); printf(“As they are of the same sizes as the previously allocated chunks, pointer d,e and f will get allocated the chunks that have been just free()d and added to the fastbin earlier \n “); d = malloc(sizeof(struct s)); printf(“d is at %p \n”,d);
e = malloc(sizeof(struct s)); printf(“e is at %p \n”,e);
f = malloc(sizeof(struct s)); printf(“f is at %p \n”,f);
d->num = 1111; printf(“d->num is assigned to 1111 \n”);
f->num = 2222; printf(“f->num is assigned to 2222 \n”);
printf(“Now let’s print the values of the both e->num and d->num \n”);
printf(“d->num is %d \n “,d->num); printf(“f->num is %d \n “,f->num);
}
Compile and run the program.
Let’s inspect the output.
Firstly we are allocating three memory locations for the pointer a,b and c. Then we free them so that they will get added into the fastbin of the same size. Then we allocate memory for the next three-pointers (d,e,f).
As we can see in the result ‘d’ gets the location of ‘c’: 0x21028 (because c is the last chunk that was freed and it gets added to the head of the fastbin).
‘e’ will get the memory location of ‘b’ and finally ‘f’ will get the memory location of ‘c’ again (0x21028).
Hmmmm …so you all will be thinking how is this possible?
I already said this at the beginning of this article to get familiar with the fastbins and how they work. Please go through the previous articles above if you don’t have an idea about fastbins.
Now let’s walk through this step by step.
When we free ‘a’, it will get added to the 0x10 (16) sized fastbin. let’s check this using gdb.
As you can see now the chunk is added in the fastbin but it is added in the 0x8 fastbin but the actual size of the chunk is 0x10. Don’t get confused the minimum sized fastbin is 0x10 (16) there is no fastbin having the size of 0x8. This is just a bug in gef. This is normally added in the 0x10 fastbin as depicted in the size of the chunk.
Let’s visualize this.
The newly added chunks will be added to the head of the bin list. (LIFO manner). The fwd pointer is null here because there is no other free chunks in the bin.
Let’s free c and see what happens. The reason why we are freeing ‘c’ instead of ‘b’ is to bypass the top check. we will free ‘b’ after freeing c.
Now c(address:0x21028) is added at the head of the 0x10 fastbin.
Chunk ‘a’ was previously at the head position, Now the head points to chunk ‘c’ and the forward pointer of chunk ‘c’ are set to point to the address of chunk ‘a’.
So what’s the forward pointer used for?
A forward pointer (fwd) is a pointer that points to the next chunk in a specific fastbin. when fwd is NULL, it indicates the end of the list. it can be used to link the chunks in the bins. This can keep track of the available free chunks in the list.
fwd is very important in double-free attacks we will see this later.
Let’s inspect chunk ‘c’ to verify this theory.
The purple marked area is the forward pointer and it points to the chunk ‘a’. But the address of ‘a’ is 0x21008 and the forward pointer has the address 0x21000. This is because the forward pointer points to the ‘prev size’ header section of the chunk ‘a’ which is at the location 0x21000. This is also part of the freed chunk’s metadata. But when we inspect the chunk using gef it shows that the chunk starts from 0x21008 because it doesn’t consider this ‘prev size’ metadata (it is not set in this case).
So this verifies our theory.
Let’s free ‘b’ and inspect.
Now the head points to chunk ‘b’ at 0x21018 and the forward pointer of chunk ‘b’ is adjusted to point to chunk ‘c’.
Now for the important part. This is where the double free occurs. We will free the pointer c again.
Let’s inspect with gef after freeing ‘c’ again.
if you inspect the fastbin using the ‘heap bins’ command you can see something strange. if you look at the entries in the fastbin, the chunk ‘c’ that we freed to trigger double free is added to the head of the 0x10 fastbin again. gef has detected this and says ‘loop detected’.
Let’s visualize this to understand why this is a loop.
When the chunk ‘c’ is freed it is again added to the head of the fastbin and the forward pointer of the chunk ‘c’ is readjusted to point to chunk ‘b’.The link to the chunk ‘a’ is removed from chunk ‘c’. So there is no link to the chunk ‘a’ in the fastbin.
Let’s change the diagram to a more accurate representation.
The chunk ‘b’ and chunk ‘c’ and pointing to each other like a loop.
Now let’s allocate memory for the pointers d,e, and f.
d = malloc(sizeof(struct s)); printf(“d is at %p \n”,d);
e = malloc(sizeof(struct s)); printf(“e is at %p \n”,e);
f = malloc(sizeof(struct s)); printf(“f is at %p \n”,f);
As you know fastbin works in a LIFO manner so the last record which is added in the fastbin will get removed first or the chunk that is pointed by the head in the fastbin will be allocated next for the malloc request if it requests for the same size.So the pointer ‘d’ will get the memory space of chunk ‘c ’ (0x021028 )and the head will point to the next freed chunk in the fastbin which is chunk ‘b’.This location is identified using the fwd pointer.
If we check the fastbin now using the ‘heap bins’ command, we can see that chunk ‘c’ which was at the head of the fastbin is removed and the head points to chunk ‘b’ (0x21018) now.
But even if I say it’s removed it’s not technically removed due to the loop caused by the double free. This is because the fwd of chunk ‘b’ still points to chunk ‘c’ so this tricks the heap allocator to think that chunk ‘c’ is still free and available in the fastbin even if it is being allocated and used. This is because the heap allocator uses the fwd to keep track of the available free chunks in the bins. As a result of this, chunk ‘c’ is still considered a free chunk and is available in the fastbin.This will form a circular chain in which the head points to chunk ‘b’ and chunk ‘b’ points to chunk ‘c’ which also points to chunk ‘b’.
Let’s see the diagram now.
Let’s now allocate the space for the pointer ‘e’ now. As you have guessed by now ‘e’ will get the memory space of chunk ‘b’ (0x21018) which was in the head of the fastbin.
if you inspect using gef you can see this.
$r0 contains the address which malloc() returned for pointer ‘e’ . This contains 0x21018, which was the address of freed chunk ‘b’ in the fastbin. Also the head of the fastbin now points to chunk ‘c’ which was pointed by fwd pointer of chunk ‘b’.
Let’s see the diagram of the chunks in the fastbin and also the output of ‘heap bins’ in gef.
if you continue the execution and the pointer ‘f’ will get allocated the memory location of chunk ‘c’ (0x21028) which is at the head of the list and the head gets updated to chunk ‘b’ (0x21018).
Let’s inspect this one last time.
So we can conclude that as a result of the double-free bug, it returns chunk ‘b’ and chunk ‘c’ interchangeably. This is because it forms a circular link. if we keep on allocating new memory, we will only the 0x21028 and 0x21018 alternatively.
we can verify this by modifying this same program.
int num;
};
int main(){
struct s *a,*b,*c,*d,*e,*f,*g,*h;
a = malloc(sizeof(struct s)); printf(“a is at %p \n”,a);
b = malloc(sizeof(struct s)); printf(“b is at %p \n”,b);
c = malloc(sizeof(struct s)); printf(“c is at %p \n”,c);
free(a); printf(“Freeing a : %p \n”,a);
free(c); printf(“Freeing c : %p \n”,c);
free(b); printf(“Freeing b : %p \n”,b); //Bypassing top check
free(c); printf(“Freeing c : %p again \n”,c); //Double free
printf(“The freed bins will be added to the corresponding bins \n”); printf(“Now we are again allocating memory using malloc into three pointers d,e and f \n”); printf(“As they are of the same sizes as the previously allocated chunks, pointer d,e and f will get allocated the chunks that have been just free()d and added to the fastbin earlier \n “); d = malloc(sizeof(struct s)); printf(“d is at %p \n”,d);
e = malloc(sizeof(struct s)); printf(“e is at %p \n”,e);
f = malloc(sizeof(struct s)); printf(“f is at %p \n”,f);
g = malloc(sizeof(struct s)); printf(“g is at %p \n”,g);
h = malloc(sizeof(struct s)); printf(“h is at %p \n”,h);
d->num = 1111; printf(“d->num is assigned to 1111 \n”);
f->num = 2222; printf(“f->num is assigned to 2222 \n”);
printf(“Now let’s print the values of the both e->num and d->num \n”);
printf(“d->num is %d \n “,d->num); printf(“f->num is %d \n “,f->num);
}
In this program, there are extra pointers allocating for more memory. let’s check the output.
As you can see now it only returns 0x21018 and 0x21028 alternatively. it doesn’t matter how many times we try to malloc(), the heap allocator will only return these two memory locations.
Now let’s explore the final section of the program.
d->num = 1111; printf(“d->num is assigned to 1111 \n”);
f->num = 2222; printf(“f->num is assigned to 2222 \n”);
printf(“Now let’s print the values of the both e->num and d->num \n”); printf(“d->num is %d \n “,d->num); printf(“f->num is %d \n “,f->num);
}
if we look at the output of this program it prints the output of d->num and f->num as 2222 even if both are assigned different values. d->num was assigned 1111 and f->num as 2222. So what’s the reason for this?
Some of you may have already gotten it by now.
The reason is that both d and f point to the same memory location.
Both ‘e’ and ‘f’ points to 0x021028 due to double free().
When d->num is assigned the value ‘1111’ it is actually writing the data in the memory location ‘0x021028’. Later when f->num is assigned the value ‘2222’ it also writes to location 0x021028 which is the same chunk used by ‘d’. So it overwrites the value to 2222. That’s why the output of d->num becomes ‘2222’.
That pretty much sums up the demo of double-free vulnerability.