1220a577a2
900 Port in process: DO NOT USE IT.
583 lines
16 KiB
JavaScript
583 lines
16 KiB
JavaScript
/* Copyright (C) 2023-2025 anonymous
|
|
|
|
This file is part of PSFree.
|
|
|
|
PSFree is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
PSFree is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
|
|
import { Int, lohi_from_one } from './int64.mjs';
|
|
import { get_view_vector } from './memtools.mjs';
|
|
import { Addr } from './mem.mjs';
|
|
|
|
import * as config from '/config.mjs';
|
|
|
|
// put the sycall names that you want to use here
|
|
export const syscall_map = new Map(Object.entries({
|
|
'read' : 3,
|
|
'write' : 4,
|
|
'open' : 5,
|
|
'close' : 6,
|
|
'getpid' : 20,
|
|
'setuid' : 23,
|
|
'getuid' : 24,
|
|
'accept' : 30,
|
|
'pipe' : 42,
|
|
'ioctl' : 54,
|
|
'munmap' : 73,
|
|
'mprotect' : 74,
|
|
'fcntl' : 92,
|
|
'socket' : 97,
|
|
'connect' : 98,
|
|
'bind' : 104,
|
|
'setsockopt' : 105,
|
|
'listen' : 106,
|
|
'getsockopt' : 118,
|
|
'fchmod' : 124,
|
|
'socketpair' : 135,
|
|
'fstat' : 189,
|
|
'getdirentries' : 196,
|
|
'__sysctl' : 202,
|
|
'mlock' : 203,
|
|
'clock_gettime' : 232,
|
|
'nanosleep' : 240,
|
|
'sched_yield' : 331,
|
|
'kqueue' : 362,
|
|
'kevent' : 363,
|
|
'rtprio_thread' : 466,
|
|
'mmap' : 477,
|
|
'ftruncate' : 480,
|
|
'shm_open' : 482,
|
|
'cpuset_getaffinity' : 487,
|
|
'cpuset_setaffinity' : 488,
|
|
'jitshm_create' : 533,
|
|
'jitshm_alias' : 534,
|
|
'evf_create' : 538,
|
|
'evf_delete' : 539,
|
|
'evf_set' : 544,
|
|
'evf_clear' : 545,
|
|
'set_vm_container' : 559,
|
|
'dmem_container' : 586,
|
|
'dynlib_dlsym' : 591,
|
|
'dynlib_get_list' : 592,
|
|
'dynlib_get_info' : 593,
|
|
'dynlib_load_prx' : 594,
|
|
'randomized_path' : 602,
|
|
'budget_get_ptype' : 610,
|
|
'thr_suspend_ucontext' : 632,
|
|
'thr_resume_ucontext' : 633,
|
|
'blockpool_open' : 653,
|
|
'blockpool_map' : 654,
|
|
'blockpool_unmap' : 655,
|
|
'blockpool_batch' : 657,
|
|
// syscall 661 is unimplemented so free for use. a kernel exploit will
|
|
// install "kexec" here
|
|
'aio_submit' : 661,
|
|
'kexec' : 661,
|
|
'aio_multi_delete' : 662,
|
|
'aio_multi_wait' : 663,
|
|
'aio_multi_poll' : 664,
|
|
'aio_multi_cancel' : 666,
|
|
'aio_submit_cmd' : 669,
|
|
'blockpool_move' : 673,
|
|
}));
|
|
|
|
const argument_pops = [
|
|
'pop rdi; ret',
|
|
'pop rsi; ret',
|
|
'pop rdx; ret',
|
|
'pop rcx; ret',
|
|
'pop r8; ret',
|
|
'pop r9; ret',
|
|
];
|
|
|
|
// implementations are expected to have these gadgets:
|
|
// * libSceLibcInternal:
|
|
// * __errno - FreeBSD's function to get the location of errno
|
|
// * setcontext - what we call Sony's own version of _Ux86_64_setcontext
|
|
// * getcontext - what we call Sony's own version of _Ux86_64_getcontext
|
|
// * anywhere:
|
|
// * the gadgets at argument_pops
|
|
// * ret
|
|
//
|
|
// setcontext/getcontext naming came from this project:
|
|
// https://github.com/libunwind/libunwind
|
|
//
|
|
// setcontext(context *ctx):
|
|
// mov rax, qword [rdi + 0x38]
|
|
// sub rax, 0x10 ; 16
|
|
// mov qword [rdi + 0x38], rax
|
|
// mov rbx, qword [rdi + 0x20]
|
|
// mov qword [rax], rbx
|
|
// mov rbx, qword [rdi + 0x80]
|
|
// mov qword [rax + 8], rbx
|
|
// mov rax, qword [rdi]
|
|
// mov rbx, qword [rdi + 8]
|
|
// mov rcx, qword [rdi + 0x10]
|
|
// mov rdx, qword [rdi + 0x18]
|
|
// mov rsi, qword [rdi + 0x28]
|
|
// mov rbp, qword [rdi + 0x30]
|
|
// mov r8, qword [rdi + 0x40]
|
|
// mov r9, qword [rdi + 0x48]
|
|
// mov r10, qword [rdi + 0x50]
|
|
// mov r11, qword [rdi + 0x58]
|
|
// mov r12, qword [rdi + 0x60]
|
|
// mov r13, qword [rdi + 0x68]
|
|
// mov r14, qword [rdi + 0x70]
|
|
// mov r15, qword [rdi + 0x78]
|
|
// cmp qword [rdi + 0xb0], 0x20001
|
|
// jne done
|
|
// cmp qword [rdi + 0xb8], 0x10002
|
|
// jne done
|
|
// fxrstor [rdi + 0xc0]
|
|
// done:
|
|
// mov rsp, qword [rdi + 0x38]
|
|
// pop rdi
|
|
// ret
|
|
//
|
|
// getcontext(context *ctx):
|
|
// mov qword [rdi], rax
|
|
// mov qword [rdi + 8], rbx
|
|
// mov qword [rdi + 0x10], rcx
|
|
// mov qword [rdi + 0x18], rdx
|
|
// mov qword [rdi + 0x20], rdi
|
|
// mov qword [rdi + 0x28], rsi
|
|
// mov qword [rdi + 0x30], rbp
|
|
// mov qword [rdi + 0x38], rsp
|
|
// add qword [rdi + 0x38], 8
|
|
// mov qword [rdi + 0x40], r8
|
|
// mov qword [rdi + 0x48], r9
|
|
// mov qword [rdi + 0x50], r10
|
|
// mov qword [rdi + 0x58], r11
|
|
// mov qword [rdi + 0x60], r12
|
|
// mov qword [rdi + 0x68], r13
|
|
// mov qword [rdi + 0x70], r14
|
|
// mov qword [rdi + 0x78], r15
|
|
// mov rsi, qword [rsp]
|
|
// mov qword [rdi + 0x80], rsi
|
|
// fxsave [rdi + 0xc0]
|
|
// mov qword [rdi + 0xb0], 0x20001
|
|
// mov qword [rdi + 0xb8], 0x10002
|
|
// xor eax, eax
|
|
// ret
|
|
|
|
// ROP chain manager base class
|
|
//
|
|
// Args:
|
|
// stack_size: the size of the stack
|
|
// upper_pad: the amount of extra space above stack
|
|
export class ChainBase {
|
|
constructor(stack_size=0x1000, upper_pad=0x10000) {
|
|
this._is_dirty = false;
|
|
this.position = 0;
|
|
|
|
const return_value = new Uint32Array(4);
|
|
this._return_value = return_value;
|
|
this.retval_addr = get_view_vector(return_value);
|
|
|
|
const errno = new Uint32Array(1);
|
|
this._errno = errno;
|
|
this.errno_addr = get_view_vector(errno);
|
|
|
|
const full_stack_size = upper_pad + stack_size;
|
|
const stack_buffer = new ArrayBuffer(full_stack_size);
|
|
const stack = new DataView(stack_buffer, upper_pad);
|
|
this.stack = stack;
|
|
this.stack_addr = get_view_vector(stack);
|
|
this.stack_size = stack_size;
|
|
this.full_stack_size = full_stack_size;
|
|
}
|
|
|
|
// use this if you want to write a new ROP chain but don't want to allocate
|
|
// a new instance
|
|
empty() {
|
|
this.position = 0;
|
|
}
|
|
|
|
// flag indicating whether .run() was ever called with this chain
|
|
get is_dirty() {
|
|
return this._is_dirty;
|
|
}
|
|
|
|
clean() {
|
|
this._is_dirty = false;
|
|
}
|
|
|
|
dirty() {
|
|
this._is_dirty = true;
|
|
}
|
|
|
|
check_allow_run() {
|
|
if (this.position === 0) {
|
|
throw Error('chain is empty');
|
|
}
|
|
if (this.is_dirty) {
|
|
throw Error('chain already ran, clean it first');
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.empty();
|
|
this.clean();
|
|
}
|
|
|
|
get retval_int() {
|
|
return this._return_value[0] | 0;
|
|
}
|
|
|
|
get retval() {
|
|
return new Int(this._return_value[0], this._return_value[1]);
|
|
}
|
|
|
|
// return value as a pointer
|
|
get retval_ptr() {
|
|
return new Addr(this._return_value[0], this._return_value[1]);
|
|
}
|
|
|
|
set retval(value) {
|
|
const values = lohi_from_one(value);
|
|
const retval = this._return_value;
|
|
retval[0] = values[0];
|
|
retval[1] = values[1];
|
|
}
|
|
|
|
get retval_all() {
|
|
const retval = this._return_value;
|
|
return [new Int(retval[0], retval[1]), new Int(retval[2], retval[3])];
|
|
}
|
|
|
|
set retval_all(values) {
|
|
const [a, b] = [lohi_from_one(values[0]), lohi_from_one(values[1])];
|
|
const retval = this._return_value;
|
|
retval[0] = a[0];
|
|
retval[1] = a[1];
|
|
retval[2] = b[0];
|
|
retval[3] = b[1];
|
|
}
|
|
|
|
get errno() {
|
|
return this._errno[0];
|
|
}
|
|
|
|
set errno(value) {
|
|
this._errno[0] = value;
|
|
}
|
|
|
|
push_value(value) {
|
|
const position = this.position;
|
|
if (position >= this.stack_size) {
|
|
throw Error(`no more space on the stack, pushed value: ${value}`);
|
|
}
|
|
|
|
const values = lohi_from_one(value);
|
|
const stack = this.stack;
|
|
stack.setUint32(position, values[0], true);
|
|
stack.setUint32(position + 4, values[1], true);
|
|
|
|
this.position += 8;
|
|
}
|
|
|
|
get_gadget(insn_str) {
|
|
const addr = this.gadgets.get(insn_str);
|
|
if (addr === undefined) {
|
|
throw Error(`gadget not found: ${insn_str}`);
|
|
}
|
|
|
|
return addr;
|
|
}
|
|
|
|
push_gadget(insn_str) {
|
|
this.push_value(this.get_gadget(insn_str));
|
|
}
|
|
|
|
push_call(func_addr, ...args) {
|
|
if (args.length > 6) {
|
|
throw TypeError(
|
|
'push_call() does not support functions that have more than 6'
|
|
+ ' arguments');
|
|
}
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
this.push_gadget(argument_pops[i]);
|
|
this.push_value(args[i]);
|
|
}
|
|
|
|
// The address of our buffer seems to be always aligned to 8 bytes.
|
|
// SysV calling convention requires the stack is aligned to 16 bytes on
|
|
// function entry, so push an additional 8 bytes to pad the stack. We
|
|
// pushed a "ret" gadget for a noop.
|
|
if ((this.position & (0x10 - 1)) !== 0) {
|
|
this.push_gadget('ret');
|
|
}
|
|
|
|
if (typeof func_addr === 'string') {
|
|
this.push_gadget(func_addr);
|
|
} else {
|
|
this.push_value(func_addr);
|
|
}
|
|
}
|
|
|
|
push_syscall(syscall_name, ...args) {
|
|
if (typeof syscall_name !== 'string') {
|
|
throw TypeError(`syscall_name not a string: ${syscall_name}`);
|
|
}
|
|
|
|
const sysno = syscall_map.get(syscall_name);
|
|
if (sysno === undefined) {
|
|
throw Error(`syscall_name not found: ${syscall_name}`);
|
|
}
|
|
|
|
const syscall_addr = this.syscall_array[sysno];
|
|
if (syscall_addr === undefined) {
|
|
throw Error(`syscall number not in syscall_array: ${sysno}`);
|
|
}
|
|
|
|
this.push_call(syscall_addr, ...args);
|
|
}
|
|
|
|
// Sets needed class properties
|
|
//
|
|
// Args:
|
|
// gadgets:
|
|
// A Map-like object mapping instruction strings (e.g. "pop rax; ret")
|
|
// to their addresses in memory.
|
|
// syscall_array:
|
|
// An array whose indices correspond to syscall numbers. Maps syscall
|
|
// numbers to their addresses in memory. Defaults to an empty Array.
|
|
static init_class(gadgets, syscall_array=[]) {
|
|
this.prototype.gadgets = gadgets;
|
|
this.prototype.syscall_array = syscall_array;
|
|
}
|
|
|
|
// START: implementation-dependent parts
|
|
//
|
|
// the user doesn't need to implement all of these. just the ones they need
|
|
|
|
// Firmware specific method to launch a ROP chain
|
|
//
|
|
// Proper implementations will check if .position is nonzero before
|
|
// running. Implementations can optionally check .is_dirty to enforce
|
|
// single-run gadget sequences
|
|
run() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
// anything you need to do before the ROP chain jumps back to JavaScript
|
|
push_end() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
push_get_errno() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
push_clear_errno() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
// get the rax register
|
|
push_get_retval() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
// get the rax and rdx registers
|
|
push_get_retval_all() {
|
|
throw Error('not implemented');
|
|
}
|
|
|
|
// END: implementation-dependent parts
|
|
|
|
// note that later firmwares (starting around > 5.00?), the browser doesn't
|
|
// have a JIT compiler. we programmed in a way that tries to make the
|
|
// resulting bytecode be optimal
|
|
//
|
|
// we intentionally have an incomplete set (there's no function to get a
|
|
// full 128-bit result). we only implemented what we think are the common
|
|
// cases. the user will have to implement those other functions if they
|
|
// need it
|
|
|
|
do_call(...args) {
|
|
if (this.position) {
|
|
throw Error('chain not empty');
|
|
}
|
|
try {
|
|
this.push_call(...args);
|
|
this.push_get_retval();
|
|
this.push_get_errno();
|
|
this.push_end();
|
|
this.run();
|
|
} finally {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
call_void(...args) {
|
|
this.do_call(...args);
|
|
}
|
|
|
|
call_int(...args) {
|
|
this.do_call(...args);
|
|
// x | 0 will always be a signed integer
|
|
return this._return_value[0] | 0;
|
|
}
|
|
|
|
call(...args) {
|
|
this.do_call(...args);
|
|
const retval = this._return_value;
|
|
return new Int(retval[0], retval[1]);
|
|
}
|
|
|
|
do_syscall(...args) {
|
|
if (this.position) {
|
|
throw Error('chain not empty');
|
|
}
|
|
try {
|
|
this.push_syscall(...args);
|
|
this.push_get_retval();
|
|
this.push_get_errno();
|
|
this.push_end();
|
|
this.run();
|
|
} finally {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
syscall_void(...args) {
|
|
this.do_syscall(...args);
|
|
}
|
|
|
|
syscall_int(...args) {
|
|
this.do_syscall(...args);
|
|
// x | 0 will always be a signed integer
|
|
return this._return_value[0] | 0;
|
|
}
|
|
|
|
syscall(...args) {
|
|
this.do_syscall(...args);
|
|
const retval = this._return_value;
|
|
return new Int(retval[0], retval[1]);
|
|
}
|
|
|
|
syscall_ptr(...args) {
|
|
this.do_syscall(...args);
|
|
const retval = this._return_value;
|
|
return new Addr(retval[0], retval[1]);
|
|
}
|
|
|
|
// syscall variants that throw an error on errno
|
|
|
|
do_syscall_clear_errno(...args) {
|
|
if (this.position) {
|
|
throw Error('chain not empty');
|
|
}
|
|
try {
|
|
this.push_clear_errno();
|
|
this.push_syscall(...args);
|
|
this.push_get_retval();
|
|
this.push_get_errno();
|
|
this.push_end();
|
|
this.run();
|
|
} finally {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
sysi(...args) {
|
|
const errno = this._errno;
|
|
this.do_syscall_clear_errno(...args);
|
|
|
|
const err = errno[0];
|
|
if (err !== 0) {
|
|
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
|
}
|
|
|
|
// x | 0 will always be a signed integer
|
|
return this._return_value[0] | 0;
|
|
}
|
|
|
|
sys(...args) {
|
|
const errno = this._errno;
|
|
this.do_syscall_clear_errno(...args);
|
|
|
|
const err = errno[0];
|
|
if (err !== 0) {
|
|
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
|
}
|
|
|
|
const retval = this._return_value;
|
|
return new Int(retval[0], retval[1]);
|
|
}
|
|
|
|
sysp(...args) {
|
|
const errno = this._errno;
|
|
this.do_syscall_clear_errno(...args);
|
|
|
|
const err = errno[0];
|
|
if (err !== 0) {
|
|
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
|
}
|
|
|
|
const retval = this._return_value;
|
|
return new Addr(retval[0], retval[1]);
|
|
}
|
|
|
|
}
|
|
|
|
export function get_gadget(map, insn_str) {
|
|
const addr = map.get(insn_str);
|
|
if (addr === undefined) {
|
|
throw Error(`gadget not found: ${insn_str}`);
|
|
}
|
|
|
|
return addr;
|
|
}
|
|
|
|
function load_fw_specific(version) {
|
|
if (version & 0x10000) {
|
|
throw RangeError('ps5 not supported yet');
|
|
}
|
|
|
|
const value = version & 0xffff;
|
|
// we don't want to bother with very old firmwares that don't support
|
|
// ECMAScript 2015. 6.xx WebKit poisons the pointer fields of some types
|
|
// which can be annoying to deal with
|
|
if (value < 0x700) {
|
|
throw RangeError("PS4 firmwares < 7.00 isn't supported");
|
|
}
|
|
|
|
if (0x800 <= value && value <= 0x900) {
|
|
return import('/rop/900.mjs');
|
|
}
|
|
|
|
throw RangeError('firmware not supported');
|
|
}
|
|
|
|
export let gadgets = null;
|
|
export let libwebkit_base = null;
|
|
export let libkernel_base = null;
|
|
export let libc_base = null;
|
|
export let init_gadget_map = null;
|
|
export let Chain = null;
|
|
|
|
export async function init() {
|
|
const module = await load_fw_specific(config.target);
|
|
Chain = module.Chain;
|
|
module.init(Chain);
|
|
({
|
|
gadgets,
|
|
libwebkit_base,
|
|
libkernel_base,
|
|
libc_base,
|
|
init_gadget_map,
|
|
} = module);
|
|
}
|