Skip to content

Latest commit

 

History

History
223 lines (186 loc) · 7.41 KB

File metadata and controls

223 lines (186 loc) · 7.41 KB

Unsubscriptions are Free

Introduction

Unsubscriptions are Free is a 100 point binary-exploitation challenge in picoCTF. The description states:

Check out my new video-game and spaghetti-eating streaming channel on Twixer! program and get a flag. source nc mercury.picoctf.net 48259

So we get the program, the source code, and the address:port to connect to the remote challenge.

Information Gathering

Hint #1

The hint just links to a PDF here.

The hint links to a PDF titled "Heap Overflows and Double-Free Attacks" by Yan Huang at Indiana University. The presentation covers variable arguments in format strings, abuse of dynamic memory on the heap, and use after free attacks. Given the title of the challenge, I will assume the author of this challenge wants us to key in on the use after free section. It's a good presentation!

Vulnerability Mitigations

$ checksec ./vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Source Code

I always check out main first; I think it gives a good starting point to understanding the program.

int main(){
    setbuf(stdout, NULL);
    user = (cmd *)malloc(sizeof(user));
    while(1){
        printMenu();
        processInput();
        //if(user){
            doProcess(user);
        //}
    }
    return 0;
}

The first thing we notice is that there are commented lines that look like it used to check whether or not the user was NULL. user is dynamically created on the heap in this function, so that might be a clue. Let's walk down the functions that main calls. printMenu() doesn't look interesting. processInput does some parsing of the user data, so let's look at that.

void processInput(){
    scanf(" %c", &choice);
    choice = toupper(choice);
    switch(choice){
        case 'S':
            if(user){
                user->whatToDo = (void*)s;
            }else{
                puts("Not logged in!");
            }
            break;
        case 'P':
            user->whatToDo = (void*)p;
            break;
        case 'I':
            user->whatToDo = (void*)i;
            break;
        case 'M':
            user->whatToDo = (void*)m;
            puts("===========================");
            puts("Registration: Welcome to Twixer!");
            puts("Enter your username: ");
            user->username = getsline();
            break;
        case 'L':
            leaveMessage();
            break;
        case 'E':
            exit(0);
        default:
            puts("Invalid option!");
            exit(1);
            break;
    }
}

The call to scanf looks fine. It also doesn't look like we can provide any other input other than ['S', 'P', 'I', 'M', 'L', 'E'] without exit'ing the program. There are an appropriate amount of break statements and a default option. Choosing S will set the user->whatToDo pointer to the function s, which prints out the address to a function, hahaexploitgobrrr, that will read the flag. The pointer to the function hahaexploitgobrrr will probably come in handy.

void s(){
  printf("OOP! Memory leak...%p\n",hahaexploitgobrrr);
  puts("Thanks for subsribing! I really recommend becoming a premium member!");
}

void hahaexploitgobrrr(){
  char buf[FLAG_BUFFER];
  FILE *f = fopen("flag.txt","r");
  fgets(buf,FLAG_BUFFER,f);
  fprintf(stdout,"%s\n",buf);
  fflush(stdout);
}

Entering P at the prompt just prints a message. Doesn't look interesting. Entering I asks the user to confirm that they are leaving via a valid scanf call and then free's the malloc'ed user. It is notable that the call to free does not have any checks to ensure it hasn't already been free'd. Based on what we've seen so far, it looks like it would be possible to call free multiple times over and over again since this takes place in a loop. This could be interesting.

void i(){
    char response;
    puts("You're leaving already(Y/N)?");
    scanf(" %c", &response);
    if(toupper(response)=='Y'){
        puts("Bye!");
        free(user);
    }else{
        puts("Ok. Get premium membership please!");
    }
}

Entering M will allow the user to enter a username and store it on the heap inside of the user structure. The processing of user input looks OK, as it realloc's the size of the buffer if we enter more than the initial buffer can hold. Entering L allows us to leave a message which suspiciously only reads in eight bytes (the size of a pointer or integer) onto the heap. Hmm.

void leaveMessage(){
    puts("I only read premium member messages but you can ");
    puts("try anyways:");
    char* msg = (char*)malloc(8);
    read(0, msg, 8);
}

Finally, E just exits the program.

Source Summary

  1. We can call doProcess even if the user has been free'd.
  2. We can leak the address of the function that will read out the flag.
  3. We can free the user at any point.
  4. We can read a message onto the heap that contains exactly eight bytes.

Here's my current thought process. We malloc a user that gets allocated on the heap (this is done for us in main). We then unsubscribe and free our user. We could then leave a message, msg, which will malloc eight bytes of data and set it to the message that we write.

When we malloc eight bytes for our message, the heap manager will first look in the per-thread cache (also known as the tcache) to see if a recently free'd chunk can satisfy the request. Because user was also eight bytes, this same chunk will be given to malloc when we ask to leave a message. We now have a situation where msg and user (even though we have free'd it) point to the same thing. Of course, the program shouldn't be using user anymore since we have released that chunk.

But it does! We noticed that we can free the user and continue to call processInput which references user a lot. Even worse, after processInput is called in main, there is a call to doProcess(user). This function will just invoke the whatToDo function that belongs to user's struct.

Here is where we get lucky. The cmd structure that implements user is defined as so:

typedef struct {
 uintptr_t (*whatToDo)();
 char *username;
} cmd;

How structs are laid out in memory is implementation specific. However, there's a really good chance that when we write our msg that shares the same pointer to user, the eight bytes that we write will overwrite the whatToDo field. This means that the pointer we end up writing to msg will just be directly called by doProcess(user).

Running the Program

Let's set breakpoints for free and malloc. If we run our program up until we malloc our msg (GDB script Part 1 in solve.py), we can see that the address 0x8b861a0 (this may change) is in the per-thread cache. This was our pointer to the user struct. If we continue our script (Part 2) and examine what malloc returns when we dynamically create our msg, we get the same pointer returned, 0x8b861a0! When we next call read from stdin, we will write our function pointer and overwrite the function pointer in the user struct. When this is called later, we will get our flag.

Flag

picoCTF{d0ubl3_j30p4rdy_cff1f12d}