Kevin Backhouse
I'm a security researcher on the GitHub Security Lab team. I try to help make open source software more secure by searching for vulnerabilities and working with maintainers to get them fixed.
This is the fourth and final post in a series about Ubuntu’s crash reporting system. We’ll review CVE-2019-11484, a vulnerability in whoopsie which enables a local attacker to get a shell as the whoopsie user, thereby gaining the ability to read any crash report.
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.
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.
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:
/var/crash
, so I can control when it will allocate the block at the address that I have chosen.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:
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./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.
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.
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.
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 probability 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.
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.
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 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.
This diagram shows how I create the fake objects in memory:
The steps are as follows:
malloc
-ed string.parse_report
a final time so that my fake heap objects are memcpy
-ed into the malloc
-ed string./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.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.
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.
For this year’s Cybersecurity Awareness Month, the GitHub Bug Bounty team is excited to feature another spotlight on a talented security researcher who participates in the GitHub Security Bug Bounty Program—@imrerad!
For this year’s Cybersecurity Awareness Month, GitHub’s Bug Bounty team is excited to offer some additional incentives to security researchers!
In this post, I’ll exploit CVE-2024-5830, a type confusion in Chrome that allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site.