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 aJSStringwhich 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:
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_UnboxedLayoutpropertySetin 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.