After the recent release of ps5-kstuff with support for PS4 fpkg files, there is a lot of questions about porting this to other firmwares (4.50 and 4.51 are important in particular, because users of those firmwares can't update to 4.03, but they are still vulnerable to all of the used exploits). The main problem with these ports it the bespoke XOM, which prevents finding the offsets by simply examining the dumps. So in this document I'm going to go over what offsets are important for ps5-kstuff, and how I found them for 4.03.
These are the main categories of offsets:
- Kernel data offsets, those can be found from data dumps that are not XOM-protected
- Kernel text offsets pointed to by kernel data
- doreti_iret offset (that's one offset, but it deserves its own word)
- Offsets found from single-stepping of kernel functions
- Offsets found from ps5-kstuff logs (called "parasites" in the source)
These include:
- IDT offset (search for something that looks like a pointer and is at offset 2 mod 8)
- TSS offset (per-cpu, array)
- PCPU offset (per-cpu, array. This is the kernel GS base)
- sysentvecs (native & PS4, search for xrefs to strings "Native SELF" and "PS4 SELF")
- crypt_singleton_array (this one wasn't actually found from the dump)
This category mainly contains interrupt handlers, which can be looked up in the dumped IDT. For ps5-kstuff, two of them are important: Xinvtlb (official FreeBSD name, also goes by the names of int244 or push_pop_all_iret), and Xjustreturn (also can be found from an IDT gate).
This offset is crucial to establish the kernel singlestep primitive used for finding other offsets, and since it is a kernel text offset not pointed to by anything, there needs to be some way to solve this chicken-and-egg problem. There is, and here is how:
- Set up a dedicated interrupt stack for the #GP exception (int13). Use whatever kernel malloc primitive to allocate memory for it, or use an in-kernel dmem mapping of a userspace page (this also gives you more freedom with the overwriter). Let X (x === 0 mod 16) be the stack address written into the TSS.
- From another thread, use the 20-byte write primitive to write {(uint64_t)0x43, (uint64_t)0x202, (uint32_t)0} to address X-32.
- In the main thread, pinned to the CPU for which you reconfigured the TSS, use setcontext or sigreturn to load an mcontext_t which has a non-canonical address (top 16 bits not 0000 or ffff) in its mc_rip field. This causes the kernel to crash with #GP on the iret instruction (the address of which we want to know).
- Normally the kernel would handle this as a kernel crash and panic (ps5-kstuff patches this, FreeBSD and PS4 are not affected). However the background thread doing writes overwrites m_cs and m_eflags (and the low 4 bytes of m_rsp) in the saved trap frame with valid userspace values, and makes the kernel think it's a crash in userspace.
- Before doing all the above, set up a signal stack for your thread with sigaltstack(2) (the "current" rsp will be from kernel and corrupted, thus unusable) and register a signal handler for SIGBUS. When the overwritten crash happens and the handler is called, grab the address of m_rip from the passed mcontext.
The first two important offsets are rdmsr and wrmsr_ret (which can also be used in place of wrmsr). To find rdmsr, single-step any interrupt handler entry and look for an instruction that simultaneously loads eax & edx with values. To find wrmsr, singlestep justreturn, change the direction of the jump (to make it think you have an X2APIC), and take note of the address which makes the console panic.
You'll also need the address of the "rep movsb; pop rbp; ret" gadget, which is found in the memcpy(2) function and is used by the trace programs to read/write kernel memory. To find that run the trace (without dumping the iret frame which you can't do yet) until you see the specific side effect of "rep movsb" or "rep movsq" (rdi += 1 or 8, rsi += 1 or 8, rcx -= 1), then run the trace again and use that gadget to copy its own address into userspace memory. ...Well, that's how I did it, it may be easier to map the page holding the return frame into userspace and read it out directly.
Once you have those in place, syscall tracing with the r0gdb_trace() family of functions should work, and you'll be able to get kernel traces (at least 1 other person got here on 4.50). This lets you find a bunch of other offsets:
- malloc(9) -- trace the IPV6_RTHDR allocation, look for a call with your specified size in rdi. Beware of rounding. M_something is the second argument (rsi) of the malloc call. This is necessary to have an allocation primitive of unlimited size, as the RTHDR-based malloc is limited to 2 KiB.
- printf -- trace dynlib_load_prx of an fSELF, look for the error message in rdi. Not needed at runtime, but is useful for finding other offsets.
- mprotect breakpoint offset, mmap breakpoint offests for SELF mapping (for thses, just look at where error codes originate from), permission bypass offsets from other syscalls.
However, if you try to trace any blocking syscall, the system will hang. This happens because the trap flag (TF) is not cleared during in-kernel task switches and gets leaked into another process, where the userspace part of the tracing mechanism is not mapped. To prevent this, you need to find cpu_switch (which, from the PoV of the calling thread, is the most atomic blocking primitive, and must be untraced). The idea is the following: run a known blocking syscall (nanosleep is the best as it wokes up on its own) under count_instrs, do a binary search on the number of instructions until you are close (100-200 instructions) to the death point. Dump the trace, open the first function call. At the end of it, you'll see a bunch of rets without instructions in between; this happens because the new thread's stack pointer is loaded, and it happens (happened for me?) to be higher in memory than the old one. The last instruction before this happens is within cpu_switch, scroll up to the beginning of the function and take note of the address.
cpu_switch could also be found by searching for a call instruction happening on 8-byte aligned stack (normal alignment is 16 bytes). But that did not work out for me.
With that checked, you can now trace (almost) arbitrary syscalls. Most fSELF-related offsets are now trivially found:
- sceSblServiceMailbox -- trace load_prx, search for "sx_xlock" being passed as rdi, take note of the function address. To verify, make it return an error and look at the kernel log.
- sceSblServiceMailbox_lr_* (also known as *_call_mailbox) -- trace load_prx, search for calls of sceSblServiceMailbox, take note of the return address (pushed to stack). To distinguish return addresses, make it return -1 and look at the kernel log.
- sceSblAuthMgrIsLoadable2 -- this is the function that performs the second mailbox call during load_prx. I don't remember if it's name is findable via the previous hint.
- sceSblServiceMailbox_lr_decryptMultipleSelfBlocks is special, as it's not normally called on retails SELF files and will crash the kernel in case of failure. To find that, mmap a large enough fSELF with a trace program that hangs execution on every *unknown* mailbox call (you can use trace_mailbox from prosper0gdb). After the hang happens, in another thread dump the trace log, extract the last recorded RSP value, and read the return address. Use r0gdb singlestep to confirm the error message.
Another bunch of important offsets can be found from single-stepping the cpu_switch function (and pmap_activate_sw which it calls). These are:
- dr2gpr (reading debug registers): singlestep the wrong path of a condjump (somewhere near beginning of the function).
- gpr2dr (writing debug registers): singlestep the wrong path of a condjump (somewhere near end of the function). Note that the code there is slightly different from FreeBSD 11: it first writes dr0-dr3, then does a bunch of unrelated MSR accesses (which are missing on FreeBSD), and only then it finally writes dr6-dr7.
- mov_rdi_cr3, mov_cr3_rax -- singlestep pmap_activate_sw with current thread, take the wrong path on a condjump to make it think that it needs to switch pagetables.
This should be enough to get fSELF decryption working during the syscall, for lazy paging offsets see the next section.
The fPKG mailbox offsets, sceSblServiceMailbox_lr_verifySuperBlock, and sceSblServiceMailbox_lr_sceSblPfsClearKey_{1,2}, are more tricky. The mount(2) syscall used for PFS mounting is too big to be fully single-stepped, so you have to use debug registers to break on sceSblServiceMailbox and log the accesses. The verifySuperBlock call has {uint64_t}rdx == 0x11, the two sceSblPfsClearKey calls (these appear only if you spoof the first one) have {uint64_t}rdx == 3.
These can be grouped into 2 groups by their importance:
- Important parasites -- syscall_before, fSELF watchpoints, sceSblServiceCryptAsync_deref_singleton. These are used to implement functionality, without them parts of ps5-kstuff will not work.
- Non-important parasites -- these do not cause any harm if not handled, but it's better to handle them to avoid cluttering the logs.
ps5-kstuff uses the technique I dubbed "pointer poisoning" to insert hooks into the kernel without relying on text patching. With pointer poisoning, top 16 bits of the pointer are replaced with the constant 0xdeb7 (stands for "DEad PoinTer"), which makes it a non-canonical address. When this pointer is later dereferenced, #GP happens, which is hooked by ps5-kstuff, and the latter tries to fix any pointers marked with 0xdeb7 back into normal kernel pointers, and such fixes are logged into the ps5-kstuff log (there should normally be none). Once you have the address, you need to determine whether it's important or not:
- Syscall parasites -- these appear when you poison the sysents pointer within the sysvec. The third of them is syscall_before, others are non-important.
- fSELF parasites -- these appear when you poison the self_header pointer within self_context. Two of them are inside functions called at the beginning of decryptSelfBlock (_sceSblAuthMgrLoadSelfBlock) & decryptMultipleSelfBlocks (_sceSblAuthMgrLoadMultipleSelfBlocks), and are used as watchpoints (*_watchpoint_*), others are non-important.
- fPKG parasites -- one parasite appears when you corrupt the cipher objects inside crypt_singleton_array, that is sceSblServiceCryptAsync_deref_singleton.
- Other parasites may appear, they are generally non-important.
Important parasites are handled in try_handle_*_trap like any other trap. Non-important parasites are handled in handle_*_parasite in uelf/parasites.h.
To trace mountpfs, I patched the shellcore process, so that upon encountering a mount(2) syscall, it'd dump its arguments to a known address and hang. Then I read the arguments from my main process using mdbg_call and call the same syscall in the main process. See "TEMPORARY CODE" in ps5-kstuff/main.c at commit 960a62520cd561adb66c2c010a040bc50a659633 for more information (you will need to adjust shellcore patch offsets, but that's trivial because text dumps are available).
Someone suggested that a "test payload" could be developed to find out the offsets. That wouldn't be trivial, because there was involved a lot of manual guessing and staring at the traces, trying to make sense of what's happening. Also some of these checks involve panicking the system, so it can't be fully automated as a single payload. In my opinion, a better approach is to develop a script that'd run on PC and probe the PS5 over network -- this script won't be terminated on a panic, and it could even detect the panic and ask the user for action. I think this might be the best way to reduce the manual labor involved with the search, so that everyone with a PS5 could contribute the offsets by just running the script.