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:
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:
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:
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,
Value are the same thing. Sometimes the namespace is referenced to avoid confusing
Value with the English word “value” (same with
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:
After executing the last JS command, the debugger should break. A close look at the stack trace should tell us everything:
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 containing the useful information we need:
Note that the value stored in
asBits_ must be unmasked (see
JSVAL_PAYLOAD_MASK_GCTHING in Value.h). Also notice the
debugView_ which tells us what type of value is stored in
asBits_. Since this is a pointer to a
JSObject, we simply do this:
Take note that the object is located at
0x7ffff4c8d120. We can try the
dumpObject function to compare the information with
Unsurprisingly, it shows that the object is also located at
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
NativeObject is a subclass of (and is also a subclass of
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
NativeObject and slots all relate:
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
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 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
If we quickly dump the memory contents at that address:
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
The three properties to look out for are:
propid_stores a pointer to a
JSStringwhich represents the name of the property (in this case, “foo”)
- The 3 bits of
immutableFlagsis the slot number this property resides in.
parentforms 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:
In case that wasn’t clear, here’s a diagram to illustrate:
Pretty simple huh? Let’s move on to more spicy stuff.
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:
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.
The first breakpoint should be hit. Let’s see what’s in memory:
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:
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:
convertToNative first converts the
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
name points to a
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:
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
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:
gef➤ p ((ObjectGroup*)0x7ffff4c87a90)->addendumKind()
$15 = js::ObjectGroup::Addendum_UnboxedLayout
propertySetin 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.