December 23, 2019

Ubuntu whoopsie integer overflow vulnerability (CVE-2019-11484)

Kevin Backhouse

In this final post of my four-part series about several vulnerabilities, I'll focus on whoopsie CVE-2019-11484, an integer overflow leading to a heap buffer overflow. To successfully exploit the vulnerability, I need to use a separate vulnerability (described in my previous blog post), to obtain the ASLR offsets of whoopsie. By chaining both vulnerabilities, I am able to get a shell as the whoopsie user, as you can see in this exploit video.

The bugs

As I explained in the second post in this series, I need a vulnerability in whoopsie to complete my exploit chain for CVE-2019-7307. I actually found two. Both are heap buffer overflows, but I was only able to successfully exploit the second. The first bug (CVE-2019-11476), which I was not able to exploit (except for crashing whoopsie), is at whoopsie.c, line 425:

/* The length of this value string */
value_length = token_p - p;
if (value) {
    /* Space for the leading newline too. */
    value_pos = value_p - value;
    if (INT_MAX - (1 + value_length + 1) < value_pos) {
        g_set_error (error,
                     g_quark_from_static_string ("whoopsie-quark"),
                     0, "Report value too long.");
        goto error;
    }
    value = g_realloc (value, value_pos + 1 + value_length + 1);
    value_p = value + value_pos;
    *value_p = '\n';
    value_p++;
} else {

This code is in parse_report, which is invoked when a new crash report is written to /var/crash. The problem is that both value_length and value_pos have type int. My PoC works by creating a crash report with a "value string" just under 4GB in length. This bypasses the bounds check on line 426 and subsequently causes a heap buffer overflow. But I found that the heap buffer overflow always overwrites an unmapped memory region, causing an immediate SIGSEGV, so I concluded that it is only a denial of service vulnerability. This bug was assigned CVE-2019-11476.

The second bug, which is exploitable, is also in code that is invoked when a new crash report is written to /var/crash, but it's buried a bit deeper in the code. After the report has been parsed on line 656, it gets converted to the BSON format on line 669. It turns out, as you can see from the discussion on the bug report, that whoopsie is using a very old fork of libbson to do this conversion. And there is a integer overflow in bson_ensure_space:

int bson_ensure_space( bson *b, const int bytesNeeded ) {
    int pos = b->cur - b->data;
    char *orig = b->data;
    int new_size;

    if ( pos + bytesNeeded <= b->dataSize )
        return BSON_OK;

The addition pos + bytesNeeded can overflow and become negative, which leads to bson_ensure_space returning immediately without allocating more memory. What's worse is that it returns BSON_OK, so the caller will think that the memory allocation was successful. It's relatively easy to trigger this bug and get a SIGSEGV by writing more than 2GB to the BSON object, as I have done in this simple PoC.

The exploit

Getting code execution is quite a bit more tricky. In fact, I am very lucky that the bug is exploitable at all. It's a heap buffer overflow on a 2GB malloc-ed buffer. Because I have no control over the size of the buffer, I have no control over where it gets placed in memory. And because it's so large, it's allocated in a mmap-ed region, which rules out most of the usual malloc exploitation tricks.

The reason why the bug is exploitable is that the memory region that I am able to corrupt contains a memory allocator, called the GSlice allocator. The GSlice allocator uses separate "magazines" of blocks to make small allocations efficient. For example, it has one magazine for 16 byte blocks, and a separate one for 32 byte blocks. The uniform block size within each magazine enables it to store the unallocated blocks as a singly linked list with zero memory overhead. (Most memory allocators use extra storage space for metadata such as the size of the block and the prev/next pointers.) The memory region that I am able to corrupt contains the 16-byte magazine. My exploit strategy is to overwrite the next pointer of one of the 16-byte blocks, so that the allocator will allocate a 16 byte block at an address chosen by me. In terms of exploitability, there's some good news and some bad news. Let's start with the good news:

  1. Thanks to CVE-2019-15790, I know the ASLR offsets of whoopsie, so I can calculate exactly what the target address should be.
  2. I can trigger a 16-byte allocation by creating a file in /var/crash, so I can control when it will allocate the block at the address that I have chosen.
  3. The offsets of the magazine blocks within the mmap-ed region are consistent from one run to the next, so I know which offset I need to corrupt.

But now the bad news:

  1. The heap overflow string cannot contain any bytes less than 0x20 (the space character). It also has to be a valid UTF8 string. So I can only overwrite the next pointer with an address that is also a valid string.
  2. The fake block that I allocate will not contain a valid "next" pointer, so the allocator will almost certainly crash on the next allocation. So I only get one chance to make something interesting happen.
  3. Triggering the heap overflow involves creating a file in /var/crash, which triggers a 16-byte allocation and pops one block off the singly linked list. So I can only trigger the overflow a small number of times before all the blocks have been allocated and there's nothing left to corrupt.

To a certain extent, I can solve the UTF8 problem by brute force: just run the exploit multiple times, until ASLR produces an address that is a valid UTF8 string. The problem with that approach is that the PID recycling exploit, which I described in my previous blog post, is quite slow. It has to use the heap overflow in bson_ensure_space to restart whoopsie, which takes around 15 seconds each time, and it is also only able to guess the PID correct approximately 1/3 of the time due to non-determinism in the assignment of whoopsie's PID. So it typically takes at least a minute to get a new set of ASLR offsets. 28 bits of a code address are affected by ASLR. For example, in my exploit video, some of the addresses that you see the system function getting assigned are 0x7ffad145c440, 0x7f54e2d08440, and 0x7feeed303440. The probability of one of these addresses being valid UTF8 and not containing any bytes less than 0x20 is just 3.8%. It gets worse if I need to write more than one address. The ASLR offsets for code addresses are independent from the heap offsets, so the chance of both being valid on the same run is far too small for the exploit to finish in a reasonable amount of time. When I first started working on the exploit, I was expecting it to take several hours to finish due to these low probabilities. But I discovered a better solution which increased the probability of ASLR picking suitable offsets to 32.6%, which means that the exploit usually finishes in under 5 minutes.

Memory layout

The diagram below shows the memory layout during two of the phases of processing a new crash report:

During the first phase (parse_report), the crash report is mmap-ed into the process. It contains a 2GB value string, which is memcpy-ed into a malloc-ed buffer. The crash report is then munmap-ed, leaving a gap in memory. This gap gets filled during the second phase (bsonify), when the 2GB string gets copied into a bson object. And this is where I got lucky, because there is no gap between the end of the memory region that has been mapped for the bson object and the start of region that has been mapped for the 16-byte GSlice magazine. So I can use the heap overflow to corrupt the magazine.

16-byte GSlice magazine layout

I have included a number of sample memory dumps of the region occupied by the 16-byte GSlice magazine with my exploit. As I mentioned earlier, the magazine is a simply linked list of blocks, ready to be used. For example, here is an excerpt from one of the memory dumps:

0x7f6f48006f40: 0x48006f50  0x00007f6f  0x00000000  0x00000000
0x7f6f48006f50: 0x48006f60  0x00007f6f  0x00000000  0x00000000
0x7f6f48006f60: 0x48006f80  0x00007f6f  0x00000000  0x00000000
0x7f6f48006f70: 0x6f8347c0  0x000055af  0x00000000  0x00000000
0x7f6f48006f80: 0x48006fa0  0x00007f6f  0x00000000  0x00000000
0x7f6f48006f90: 0x48006f70  0x00007f6f  0x00000000  0x00000000
0x7f6f48006fa0: 0x48006f90  0x00007f6f  0x00000000  0x00000000
0x7f6f48006fb0: 0x48006fc0  0x00007f6f  0x00000000  0x00000000
0x7f6f48006fc0: 0x48007200  0x00007f6f  0x00000000  0x00000000

As you can see, the blocks are not always consecutive in memory. For example, the snippet above includes the sequence 0x7f6f48006f80 -> 0x7f6f48006fa0 -> 0x7f6f48006f90 -> 0x7f6f48006f70. One of the comments in the source code seems to suggest that this is a deliberate optimization, known as "cache colorization". But the good news is that, even though the block offsets seem random, they are consistent from one run to the next. In particular, the block with the lowest address is consistently at offset 0x6f40. So my goal is to overwrite the next pointer at offset 0x6f40 so that the next block to be allocated is fake.

ASLR entropy of mmap-ed regions

I mentioned earlier that the ASLR entropy on code addresses, such as the address of the system function, is 28 bits, which makes the probability of the address being a valid UTF8 string very low. But the entropy of the mmap-ed addresses is much more favorable. In the address 0x7f6f48006f40, which you can see in the memory snippet above, only the 6f48 is affected by ASLR. So chance of one of these addresses being a valid string is much higher. I calculated that the probabilty is 32.6%. The only caveat is that there is a zero byte in the middle of the address, so I need to run the heap overflow twice to write one address. (The first pass writes an address like 0x7f6f48212121 and the second pass writes a slightly shorter string, with its null-terminator becoming the zero byte in the middle of the address.) As a result of this, it's relatively easy for me to create fake magazine blocks within the mmap-ed region occupied by the GSlice allocator.

GSlice magazine list reversal behavior

One of the peculiar features of the GSlice allocator is that when magazine blocks are freed, they are not returned to the same list that there were allocated from. You can see at gslice.c, line 841 that thread_memory_magazine1_alloc pops new blocks from magazine1 and at line 853 that thread_memory_magazine2_free returns them to magazine2. The effect of this is that the simply linked list gets reversed. I can use this behavior to overwrite a pointer at an almost arbitrary address: by overwriting the next pointer of the magazine block, I can get the next allocation to return a (fake) block at an address of my choice. When that fake block is freed, its next pointer is overwritten with the address of the previous magazine block.

Exploit plan

Summarizing everything that I have said so far: the vulnerability gives me one chance to overwrite an arbitrary memory location with a pointer back to a memory location that I control. The program is almost certainly going crash very soon after this, so this one operation needs be enough to give me a shell. Which pointer should I overwrite? After some searching, I found a global variable named glib_worker_context. It contains a field named source_lists, which, via a few more indirections, points to a struct named GSourceFuncs:

struct _GSourceFuncs
{
  gboolean (*prepare)  (GSource    *source,
                        gint       *timeout_);
  gboolean (*check)    (GSource    *source);
  gboolean (*dispatch) (GSource    *source,
                        GSourceFunc callback,
                        gpointer    user_data);
  void     (*finalize) (GSource    *source); /* Can be NULL */

  /*< private >*/
  /* For use by g_source_set_closure */
  GSourceFunc     closure_callback;
  GSourceDummyMarshal closure_marshal; /* Really is of type GClosureMarshal */
};

It contains function pointers! And it turns out that those pointers get called when an event happens, like writing a new file in /var/crash. So all I need to do is use the vulnerability to replace glib_worker_context->source_lists with a pointer into memory that I control and fill my fake GSourceFuncs with pointers to system.

The big problem with this plan is the same problem as before: the heap overflow only allows me to write valid UTF8 strings, which is going to make it extremely difficult for me to create all the fake heap objects that I need to replace sources_list with a working alternative.

The memcpy window

The solution is simple, but it took me a long time to figure it out! I have said repeatedly that the string needs to be valid UTF8 and it cannot contain any bytes less than 0x20. The 0x20 restriction is enforced by this code in parse_report:

value = g_malloc ((token_p - p) + 1);
memcpy (value, p, (token_p - p));
value[(token_p - p)] = '\0';
for (char *c = value; c < value + (token_p - p); c++)
  if (*c >= '\0' && *c < ' ')
    *c = '?';

First, the 2GB string is memcpy-ed into memory. Then any bytes less than 0x20 get replaced with the question mark character. The UTF8 check doesn't happen until later, during the bsonify phase. When I first read this code, I immediately concluded that characters less than 0x20 are impossible and moved on. But what I eventually realized is that memcpy-ing 2GB takes a long time. So there's a window of time, probably half a second or so, during which I have complete control of the bytes in the malloc-ed string (the box on the left in the memory layout diagram). So the solution is to put all my fake heap objects in the malloc-ed string, where I have complete control of all the bytes. I only need my fake magazine block to redirect to it. And because the base address of the malloc-ed string is exactly 0x101000000 below the GSlice magazine in memory, there is a high probability that if the GSlice addresses satisfy the UTF8 requirement then I will also be able to construct a valid pointer into the malloc-ed area too.

Redirecting to fake heap objects

This diagram shows how I create the fake objects in memory:

The steps are as follows:

  1. Use the heap overflow several times to create a fake block in the GSlice magazine, with a next pointer that points into the area of memory where the 2GB string will be allocated.
  2. Trigger a heap overflow which overwrites the next pointer in my fake block in the GSlice magazine. Because the process of triggering the heap overflow also triggers a 16 byte magazine allocation, this also causes my fake block to be allocated and freed, so the next block to be allocated will be the second fake block in the malloc-ed string.
  3. Trigger parse_report a final time so that my fake heap objects are memcpy-ed into the malloc-ed string.
  4. Trigger a quick flurry of file events in /var/crash so that the GSlice allocator allocates and frees a block at the address of sources_list, which means that it now points to my fake heap objects. These events are handled by a separate thread, so I can trigger them while the memcpy is still in progress and the invalid bytes have not yet been replaced with question mark characters.
  5. The next event immediately after sources_list has been overwritten causes one of the function pointers in my fake GSourceFuncs object to be called, with my fake GSource object as its first argument. I have filled my GSourceFuncs object with pointers to the system function and I have put the string "/tmp/kev.sh" in my GSource object, so the next thing that happens is that my script gets called!

I have glossed over some of the details in this description. The source code for the exploit contains extra comments for anyone who is interested.

Thanks for reading!

That's the end of the "whoopsie-daisy" series. Thank you very much for reading and congratulations for making it all the way to the end! I learned a lot from working on these exploits, so I hope you enjoyed hearing about what worked, what didn't, and some of the tips and tricks that I learned along the way.