Exploiting Spidermonkey Part 1 (CVE-2019-9791)

Environment Setup

The vulnerability in question is CVE-2019-9791 found one quiet weekend in js-vuln-db. As this was fixed in Firefox 66, I grabbed a copy of Firefox 65.0.1 to do some playing around with from the Mozilla FTP servers.

Then I followed Mozilla’s guide to building a developer (debug) build here. On Linux (I’m running x64 Ubuntu 18.04), this means extracting the sources somewhere and executing:

1
2
3
4
5
cd js/src
mkdir build_DBG.OBJ
cd build_DBG.OBJ
/bin/sh ../configure.in --enable-debug --disable-optimize --disable-tests
make

Noting that the final binaries will be located within js/src/build_DBG.OBJ/dist/bin (I added this to ~/.bashrc to make my life easier). Then, to verify things work:

1
2
~$ js --version
JavaScript-C65.0.1

Before we continue, some recommended reading materials:

The next step would be to set up GDB (I’d recommend GDB with GEF). To debug, simply run: gdb js

Understanding SpiderMonkey

We’re going to focus on two main structures: the Value and the NativeObject. We won’t be going in-depth into these, I would recommend you check out 0vercl0k’s IonMonkey exploitation on a different CVE here. To avoid confusion, js::Value and Value are the same thing. Sometimes the namespace is referenced to avoid confusing Value with the English word “value” (same with js::Shape).

Babystep #1: js::Value

Note: SpiderMonkey is bundled with several shell-only functions which should make our lives easier. One of them is objectAddress (a full list can be found here). This is unfortunately unavailable in all builds, including the one I’m using.

Let’s take a look at how objects are oriented in memory:

1
2
3
4
5
6
~$ gdb js
gef➤ break js::math_atan2
gef➤ r
js> a = {}
({})
js> Math.atan2(a)

After executing the last JS command, the debugger should break. A close look at the stack trace should tell us everything:

1
2
Thread 1 "js" hit Breakpoint 1, js::math_atan2 (cx=0x7ffff5518000, argc=0x1, vp=0x7ffff4727090) at /Firefox_SpiderMonkey/firefox-65.0.1-src/js/src/jsmath.cpp:173
173 bool js::math_atan2(JSContext* cx, unsigned argc, Value* vp) {
1
2
3
4
5
6
7
8
9
10
11
gef➤  list
168 double z = ecmaAtan2(dy, dx);
169 res.setDouble(z);
170 return true;
171 }
172
173 bool js::math_atan2(JSContext* cx, unsigned argc, Value* vp) {
174 CallArgs args = CallArgsFromVp(argc, vp);
175
176 return math_atan2_handle(cx, args.get(0), args.get(1), args.rval());
177 }

On line 173 as indicated by the breakpoint info, we see math_atan2 taking in 3 arguments. Interestingly enough, the array vp is of size argc+2 with the 0th and 1st slots taken. This leaves vp[2] containing the useful information we need:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gef➤  p vp[2]
$17 = {
asBits_ = 0xfffe7ffff4c8d120,
asDouble_ = -nan(0xe7ffff4c8d120),
debugView_ = {
payload47_ = 0x7ffff4c8d120,
tag_ = JSVAL_TAG_OBJECT
},
s_ = {
payload_ = {
i32_ = 0xf4c8d120,
u32_ = 0xf4c8d120,
why_ = 4106801440
}
}
}

Note that the value stored in asBits_ must be unmasked (see JSVAL_PAYLOAD_MASK_GCTHING in Value.h). Also notice the tag_ under debugView_ which tells us what type of value is stored in asBits_. Since this is a pointer to a JSObject, we simply do this:

1
2
gef➤  p vp[2].asBits_&0x7FFFFFFFFFFF
$18 = 0x7ffff4c8d120
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gef➤  p *((JSObject*) (vp[2].asBits_&0x7FFFFFFFFFFF))
$19 = {
<js::gc::Cell> = {
static ReservedBits = 0x2,
static RESERVED_MASK = 0x3,
static FORWARD_BIT = 0x1,
static JSSTRING_BIT = 0x2
},
members of JSObject:
group_ = {
<js::WriteBarrieredBase<js::ObjectGroup*>> = {
<js::BarrieredBase<js::ObjectGroup*>> = {
value = 0x7ffff4c87310
},
<js::WrappedPtrOperations<js::ObjectGroup*, js::WriteBarrieredBase<js::ObjectGroup*> >> = {<No data fields>}, <No data fields>}, <No data fields>},
shapeOrExpando_ = 0x7ffff4c89bc8,
static TraceKind = JS::TraceKind::Object,
static MaxTagBits = 0x3,
static ITER_CLASS_NFIXED_SLOTS = 0x1,
static MAX_BYTE_SIZE = 0xa0
}

Take note that the object is located at 0x7ffff4c8d120. We can try the dumpObject function to compare the information with GDB:

1
2
3
4
5
6
7
8
9
10
11
gef➤  c
Continuing.
NaN
js> dumpObject(a)
object 7ffff4c8d120
global 7ffff4c8a060 [global]
class 555557f17480 Object
lazy group
flags:
proto <Object at 7ffff4c8d040>
properties:

Unsurprisingly, it shows that the object is also located at 0x7ffff4c8d120.

Babystep #2: js::NativeObject

When we passed an object into Math.atan2, the reference to the object was stored as a js::Value whereas the object information itself was stored as a JSObject which NativeObject is a subclass of (and is also a subclass of ShapedObject).

I recommend checking out this Phrack Magazine article as they do an excellent job of covering the ins and outs of objects. However, I’ll still go over Shape and slots as that is important to this specific vulnerability. Let’s see how Shape, NativeObject and slots all relate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gef➤  break js::math_atan2
gef➤ r
js> a={foo:1}
({foo:1})
js> dumpObject(a)
object 7ffff4c8b160
global 7ffff4c8a060 [global]
class 555557f17480 Object
lazy group
flags:
proto <Object at 7ffff4c8d040>
properties:
"foo": 1 (shape 7ffff4cb0d08 enumerate slot 0)
js> Math.atan2()

So currently, we have an object named a with a single property named foo storing a js::Value of 1. But where is the 1 being stored? Where is the property name foo being stored? The object is represented with the NativeObject structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
gef➤  p *((NativeObject*)0x7ffff4c8b160)
$5 = {
<js::ShapedObject> = {
<JSObject> = {
<js::gc::Cell> = {
static ReservedBits = 0x2,
static RESERVED_MASK = 0x3,
static FORWARD_BIT = 0x1,
static JSSTRING_BIT = 0x2
},
members of JSObject:
group_ = {
<js::WriteBarrieredBase<js::ObjectGroup*>> = {
<js::BarrieredBase<js::ObjectGroup*>> = {
value = 0x7ffff4c87310
},
<js::WrappedPtrOperations<js::ObjectGroup*, js::WriteBarrieredBase<js::ObjectGroup*> >> = {<No data fields>}, <No data fields>}, <No data fields>},
shapeOrExpando_ = 0x7ffff4cb0d08,
static TraceKind = JS::TraceKind::Object,
static MaxTagBits = 0x3,
static ITER_CLASS_NFIXED_SLOTS = 0x1,
static MAX_BYTE_SIZE = 0xa0
}, <No data fields>},
members of js::NativeObject:
slots_ = 0x0,
elements_ = 0x5555569fc5f0 <emptyElementsHeader+16>,
static SLOT_CAPACITY_MIN = 0x8,
static MAX_SLOTS_COUNT = 0xfffffff,
static MAX_FIXED_SLOTS = 0x10,
static MAX_DENSE_ELEMENTS_ALLOCATION = 0xfffffff,
static MAX_DENSE_ELEMENTS_COUNT = 0xffffffd,
static MIN_SPARSE_INDEX = 0x3e8,
static SPARSE_DENSITY_RATIO = 0x8
}

The two properties to look out for are:

  1. shapeOrExpando_ references the shape of the object. A shape can be thought of as representing a single property. An object with multiple properties will have shapes chained up in a linked list.
  2. slots_ is not to be confused with an object’s slots! They are different things. We’ll ignore slots_, though I’ve linked to the source later on.

An object’s property values are stored in an array, these are slots. Each array slot will correspond to the value of one property in the object. This array resides directly after the NativeObject structure.

If we quickly dump the memory contents at that address:

1
2
gef➤  p sizeof(NativeObject)
$6 = 0x20
1
2
3
4
5
6
7
gef➤  hexdump qword 0x7ffff4c8b160
0x00007ffff4c8b160│+0x0000 0x00007ffff4c87310
0x00007ffff4c8b168│+0x0008 0x00007ffff4cb0d08
0x00007ffff4c8b170│+0x0010 0x0000000000000000
0x00007ffff4c8b178│+0x0018 0x00005555569fc5f0
0x00007ffff4c8b180│+0x0020 0xfff8800000000001
0x00007ffff4c8b188│+0x0028 0xfffe4d4d4d4d4d4d

We see the property value of foo stored after the NativeObject structure (which has a size of 0x20). In this case, foo inhabits slot number 0.

If we move on and investigate the shape of the object (stored in shapeOrExpando_):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
gef➤  p *((Shape*)0x7ffff4cb0d08)
$7 = {
<js::gc::TenuredCell> = {
<js::gc::Cell> = {
static ReservedBits = 0x2,
static RESERVED_MASK = 0x3,
static FORWARD_BIT = 0x1,
static JSSTRING_BIT = 0x2
}, <No data fields>},
members of js::Shape:
base_ = {
<js::WriteBarrieredBase<js::BaseShape*>> = {
<js::BarrieredBase<js::BaseShape*>> = {
value = 0x7ffff4c880e0
},
<js::WrappedPtrOperations<js::BaseShape*, js::WriteBarrieredBase<js::BaseShape*> >> = {<No data fields>}, <No data fields>}, <No data fields>},
propid_ = {
<js::WriteBarrieredBase<JS::PropertyKey>> = {
<js::BarrieredBase<JS::PropertyKey>> = {
value = {
asBits = 0x7ffff4c3c860
}
},
<js::WrappedPtrOperations<JS::PropertyKey, js::WriteBarrieredBase<JS::PropertyKey> >> = {<No data fields>}, <No data fields>}, <No data fields>},
immutableFlags = 0x2000000,
attrs = 0x1,
mutableFlags = 0x2,
parent = {
<js::WriteBarrieredBase<js::Shape*>> = {
<js::BarrieredBase<js::Shape*>> = {
value = 0x7ffff4cb0ce0
},
<js::WrappedPtrOperations<js::Shape*, js::WriteBarrieredBase<js::Shape*> >> = {<No data fields>}, <No data fields>}, <No data fields>},
{
kids = {
w = 0x0
},
listp = 0x0
},
static TraceKind = JS::TraceKind::Shape
}

The three properties to look out for are:

  1. propid_ stores a pointer to a JSString which represents the name of the property (in this case, “foo”)
  2. The 3 bits of immutableFlags is the slot number this property resides in.
  3. parent forms a linked list of shapes so chain multiple properties can be chained together.

If we dump the contents of propid_, we should see the string “foo” pop up:

1
2
gef➤  p (*((JSString*) 0x7ffff4c3c860)).d.inlineStorageLatin1
$10 = "foo\\000MM\\376\\377MMMMMM\\376\\377"

In case that wasn’t clear, here’s a diagram to illustrate:

Firefox

Pretty simple huh? Let’s move on to more spicy stuff.

Babystep #3: js::UnboxedObject

An UnboxedObject is essentially an optimized NativeObject. They have been removed since Firefox 68. To see the relationship between native and unboxed objects, let’s take a look at this JS snippet:

1
2
3
4
5
6
7
8
9
10
let bkpt = Math.atan2;

function A(v) {
this.foo = v;
}

bkpt(new A(100)); // New object will be created as a NativeObject
for(var i = 0; i < 1000; i++)
new A(100);
bkpt(new A(100)); // New object will be created as an UnboxedObject

At the first breakpoint, a native object will be created as expected. We then create many instances of A and see how at the second breakpoint, an unboxed object will be created.

1
2
3
$ gdb --args js test.js
gef➤ break math_atan2
gef➤ r

The first breakpoint should be hit. Let’s see what’s in memory:

1
2
3
4
5
6
7
8
gef➤  hexdump qword vp[2]->asBits_&0x7FFFFFFFFFFF
0x00007ffff4c8a240│+0x0000 0x00007ffff4c87a90
0x00007ffff4c8a248│+0x0008 0x00007ffff4cb87e0
0x00007ffff4c8a250│+0x0010 0x0000000000000000
0x00007ffff4c8a258│+0x0018 0x00005555569fc5f0
0x00007ffff4c8a260│+0x0020 0xfff8800000000064
0x00007ffff4c8a268│+0x0028 0xfffe4d4d4d4d4d4d
0x00007ffff4c8a270│+0x0030 0xfffe4d4d4d4d4d4d

Our property value 100 is stored as 0xfff8800000000064 at an offset of 0x20 (NativeObject has a size of 0x20). Let’s continue on to the second breakpoint:

1
2
gef➤  c
Continuing.
1
2
3
4
5
6
gef➤  hexdump qword vp[2]->asBits_&0x7FFFFFFFFFFF
0x00007ffff7e08020│+0x0000 0x00007ffff4c87a90
0x00007ffff7e08028│+0x0008 0x0000000000000000
0x00007ffff7e08030│+0x0010 0xfffe2d2d00000064
0x00007ffff7e08038│+0x0018 0xfffe2d2d2d2d2d2d
0x00007ffff7e08040│+0x0020 0xfffe2f2f2f2f2f2f

Now, the value 100 is at an offset of 0x10, exactly the size of an UnboxedObject! But how are UnboxedObjects represented? Well, to understand, we can trace how an UnboxedObject turns into a NativeObject.

The magic happens, unsurprisingly, in UnboxedObject.cpp on line 741. If you’re following along, the lines in question are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* static */
NativeObject* UnboxedPlainObject::convertToNative (JSContext* cx,
JSObject* obj) {
...
// Line 748
const UnboxedLayout& layout = obj->as<UnboxedPlainObject>().layout();
UnboxedExpandoObject* expando = obj->as<UnboxedPlainObject>().maybeExpando();
...
// Line 755
if (!layout.nativeGroup()) {
if (!UnboxedLayout::makeNativeGroup(cx, obj->group())) {
return nullptr;
}
...
}
...
// Line 767
for (size_t i = 0; i < layout.properties().length(); i++) {
...
if (!values.append(obj->as<UnboxedPlainObject>().getValue(
layout.properties()[i], true))) {
return nullptr;
}
}
...
// Line 792
for (size_t i = 0; i < values.length(); i++) {
obj->as<PlainObject>().initSlotUnchecked(i, values[i]);
}
...
}

So, convertToNative first converts the layout though makeNativeGroup, which reintroduces shaped properties. Then, properties values are extracted looping through the layout.properties() array. Finally, the values are reintroduced into the new NativeObject slots. These are the steps which “boxes” the properties of an unboxed object.

In GDB, we can extract the property names from our object, noting that our object is located at 0x7ffff7e08020:

1
2
gef➤  p ((UnboxedPlainObject*) 0x7ffff7e08020)->layout().properties().length()
$1 = 0x1
1
2
3
4
5
6
gef➤  p ((UnboxedPlainObject*) 0x7ffff7e08020)->layout().properties()[0]
$2 = (const js::UnboxedLayout::Property &) @0x7ffff49f8080: {
name = 0x7ffff4c3c860,
offset = 0x0,
type = JSVAL_TYPE_INT32
}

Where name points to a JSString, and offset marks the slot offset to get the value of the property. This is different from slot index (as was the case with Shape). For instance, if I had two properties, the second one would be:

1
2
3
4
5
6
gef➤  p ((UnboxedPlainObject*) (vp[2]->asBits_ & 0x7FFFFFFFFFFF))->layout().properties()[1]
$3 = (const js::UnboxedLayout::Property &) @0x7ffff49eb0f0: {
name = 0x7ffff4caec40,
offset = 0x4,
type = JSVAL_TYPE_INT32
}

Not to bad right? I’ll let you try with two properties by yourself. Before we move on, here are some must-reads:

  1. JSObject layout and fields
  2. NativeObject layout and fields (especially the documentation of slots_ and elements_).
  3. addendum_ in ObjectGroup.h, the addendum types (i.e., UnboxedLayouts are stored within the group’s addendum field). You can get the addendum type like so:
    1
    2
    gef➤  p ((ObjectGroup*)0x7ffff4c87a90)->addendumKind()
    $15 = js::ObjectGroup::Addendum_UnboxedLayout
  4. propertySet in ObjectGroup.h tracks property types for type inferences.

Here’s a diagram to clear things up!

Firefox

The Vulnerability (finally)

This bug report was submitted by Samuel Groß, a security expert from Google’s Project Zero. You can see the commit patch over here. Now you are armed with the knowledge to understand all the jargon:

CVE-2019-9791: SpiderMonkey: IonMonkey’s type inference is incorrect for constructors entered via OSR

The issue comes from the conversion from an UnboxedObject to a NativeObject due to optimization. This results in an exploitable type confusion vulnerability. A detailed writeup of this issue can be found here or here.

In short, the issue comes during the deoptimization of unboxed objects. As a result, a disconnect can be formed between the object’s template properties (Group/Shape) and its actual properties. I highly encourage reading Samuel Groß’s detailed report on this subject including a line-by-line walkthrough of the bug.

The patch presented by Brian Hackett:

When unboxed objects are in use, the objects which have their properties rolled back have the native ObjectGroup which results when an unboxed object is converted to a native object. The group which has its definite property information cleared is the original unboxed ObjectGroup, however. As a result, definite properties are not cleared from the native ObjectGroup as they should be. […] The attached patch deoptimizes UnboxedLayout::makeNativeGroup so that the definite properties are not added to the native group in the first place.

Next up, we’ll discuss in more detail the bug and how we may exploit it for a 0day attack.