Revisiting JavaScriptCore Internals: boxed vs. unboxed

TLDR
We go over the boxed vs. unboxed values, how to convert addresses to doubles and why our bug is a memory corruption.

Series

watch on YouTube

boxed vs. unboxed values

With last video we have seen some pretty cool stuff. We were able to craft a JavaScript object with the help of the addrof() and fakeobj() primitive. And these functions deal with Double values, but sometimes we had and somtimes we did not, add this large value 0x1000000000000 (2^48) to it. So maybe you have wondered about this sentence in the JSCJSValue.h, and why or when we have to think about that added value.

* The top 16-bits denote the type of the encoded JSValue:
*
*     Pointer {  0000:PPPP:PPPP:PPPP
*              / 0001:****:****:****
*     Double  {         ...
*              \ FFFE:****:****:****
*     Integer {  FFFF:0000:IIII:IIII
*
* The scheme we have implemented encodes double precision values by performing a
* 64-bit integer addition of the value 2^48 to the number. After this manipulation
* no encoded double-precision value will begin with the pattern 0x0000 or 0xFFFF.
* Values must be decoded by reversing this operation before subsequent floating point
* operations may be peformed.
The scheme we have implemented encodes double precision values by performing a 64-bit integer addition of the value 2^48 to the number.

Let's have a look at that in memory. Here we are creating an array that only contains Doubles and we look at the values in memory. Then we push an object into that array, which converts it to an ArrayWithContigous (this is now a generic array).

If you compare the old and new values, you will notice that ArrayWithContigous have the  0x1000000000000 (2^48) added to it: 0x402abd70a3d70a3d ->  0x402bbd70a3d70a3d. So sometimes JavaScriptCore stores the Double just as a regular raw Double, and sometimes it adds this value to it. Why?


This difference is often called boxed and unboxed. The description about the added 0x1000000000000 (2^48) was coming from the JSValue header-file. So this only happens if our value is a JSValue. Storing our Double as a JSValue is necessary, when our array contains various different types. Because JSValues allow us to encode either a pointer, an integer, or a double.

Pointer {  0000:PPPP:PPPP:PPPP
         / 0001:****:****:****
Double  {         ...
         \ FFFE:****:****:****
Integer {  FFFF:0000:IIII:IIII

And so to help the engine to differentiate between the different types, we force this addition of  0x1000000000000. That’s why doubles start from this range if you look at the ascii graphics above.

So if you want to access a double in your array, and JavaScriptCore sees that you use a generic ArrayWithContigous, it has to first subtract that constant value to turn it into a raw normal Double (unboxing). And of course the same in reverse, when you want to write a Double into that array you have to add it (boxing).

But when JavaScriptCore knows that you only operate on Doubles, because you only ever placed Doubles into your array, then the array can stay a type-specific ArrayWithDoubles, and can let the doubles be unchanged.

In the DataFormat.h file we also find a comment describing a bit more about the different types:

Values may be unboxed primitives (int32, double, or cell), or boxed as a JSValue. For boxed values, we may know the type of boxing that has taken place. (May also need bool, array, object, string types!)

Converting Doubles to Integers

Another important trick we need to learn about, is how to convert between Doubles and Integers. In previous videos we have used python to help us with the struct module. There we packed an Integer into raw bytes, and unpacked those bytes as a Double. Can we also do this in JavaScript?

Saelo has an excellent JavaScript library for this, which Linus also uses in his exploit - namely int64.js and utils.js. These functions resemble Python struct functions to convert between the types and add a lot more convenience functions. But we want to better understand how this is done, and so in the utils.js we can find the underlying trick.

var buffer      = new ArrayBuffer(8);
var byteView    = new Uint8Array(buffer);
var uint32View  = new Uint32Array(buffer);
var float64View = new Float64Array(buffer);

Here you can see the allocation of an 8 byte large ArrayBuffer, and then we create Uint8, Uint32 and Float64 interfaces into that buffer. Conversion between types then becomes as easy as simply writing values from one type interface into the buffer, and reading it from another.  I wanted to show it, because it’s a not so well-known JavaScript feature, and I believe it only exists to make exploitation easier.


So here we put in a 64bit Float/Double and extract the two 32bit Integer components

>>> buf = new ArrayBuffer(8)
[object ArrayBuffer]
>>> u32 = new Uint32Array(buf)
0,0
>>> f64 = new Float64Array(buf)
0
>>> f64[0] = 5.3678009605058e-310 // put Float/Double in
5.3678009605058e-310
>>> u32[0].toString(16) // take Integers out
7de890
>>> u32[1].toString(16)
62d0
>>> (u32[1]*0x100000000+u32[0]).toString(16)
62d0007de890
>>>

And here we put in the two 32bit Integer components, and extract the 64bit Float/Double

>>> a = 0x414141303030
18367621723336750
>>> (a/0x100000000).toString(16)
414141.303030
>>> (a%0x100000000).toString(16)
303030
>>> u32[0] = a%0x100000000 // put Integers in
808464432
>>> u32[1] = a/0x100000000
4276545
>>> f64[0] // take Float/Double out
1.9196714510291825e-307
>>>

This is useful for the exploit, because we can use it to perform the shift of the address that we pass to  fakeobj().

The JIT bug is a Memory Corruption?

And lastly I wanted to talk about that the bug we are dealing with in Linus's exploit is in fact a memory corruption. Which might feel weird at first, because it's not a bug that overwrites a buffer, smashes stack data or overflows heap metadata - and we haven't seen a crash. But this is just because we have been careful and the primitives fakeobj() and addrof() are just very powerful and stable. But if we are a bit more naughty, it is easy to make the browser crash.

The following test.js was modified to create a fake Array Object and make it's butterfly point into bad memory. Trying to access the length would then attempt to follow that bad butterfly, leading to a crash. In this case JSC is also compiled with AddressSanitizer, so the crash provides some additional information about the crash.


Resources