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:
- Thanks to CVE-2019-15790, I know the ASLR offsets of whoopsie, so I can calculate exactly what the target address should be.
- 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. - 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:
- 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. - 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.
- 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:
- 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.
- 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. - Trigger
parse_report
a final time so that my fake heap objects arememcpy
-ed into themalloc
-ed string. - Trigger a quick flurry of file events in
/var/crash
so that the GSlice allocator allocates and frees a block at the address ofsources_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 thememcpy
is still in progress and the invalid bytes have not yet been replaced with question mark characters. - The next event immediately after
sources_list
has been overwritten causes one of the function pointers in my fakeGSourceFuncs
object to be called, with my fakeGSource
object as its first argument. I have filled myGSourceFuncs
object with pointers to thesystem
function and I have put the string “/tmp/kev.sh” in myGSource
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.