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 |
|
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 |
|
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 |
|
After executing the last JS command, the debugger should break. A close look at the stack trace should tell us everything:
1 |
|
1 |
|
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 |
|
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 |
|
1 |
|
Take note that the object is located at 0x7ffff4c8d120
. We can try the dumpObject
function to compare the information with GDB
:
1 |
|
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 |
|
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 |
|
The two properties to look out for are:
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.slots_
is not to be confused with an object’s slots! They are different things. We’ll ignoreslots_
, 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 |
|
1 |
|
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 |
|
The three properties to look out for are:
propid_
stores a pointer to aJSString
which represents the name of the property (in this case, “foo”)- The 3 bits of
immutableFlags
is the slot number this property resides in. 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 |
|
In case that wasn’t clear, here’s a diagram to illustrate:
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 |
|
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 |
|
The first breakpoint should be hit. Let’s see what’s in memory:
1 |
|
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 |
|
1 |
|
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 |
|
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 |
|
1 |
|
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 |
|
Not to bad right? I’ll let you try with two properties by yourself. Before we move on, here are some must-reads:
- JSObject layout and fields
- NativeObject layout and fields (especially the documentation of
slots_
andelements_
). 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
2gef➤ p ((ObjectGroup*)0x7ffff4c87a90)->addendumKind()
$15 = js::ObjectGroup::Addendum_UnboxedLayoutpropertySet
in ObjectGroup.h tracks property types for type inferences.
Here’s a diagram to clear things up!
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.