Mast1c0re

0 views
Skip to first unread message

Emigdio Binet

unread,
Aug 3, 2024, 10:10:31 AM8/3/24
to taamupnaty

In this article I will discuss how I successfully escaped the PS2 emulator developed for the PlayStation 4. See also Part 2, covering the next part of the exploit chain, and PlayStation's response to the research.

It's been a long time since I last worked on any modern PlayStation hacking, but with the release of the PS5 and the introduction of PlayStation's bug bounty program, I was motivated to attempt some kind of exploit chain that would work on the PS5.

This is particularly valuable because access to running just the subset of officially available PS2 games on these platforms is being charged at the highest tier of PlayStation's new subscription service.

Sony aggressively removed JIT privileged attack surface from the PS5, disabling JIT in both the web browser and the BluRay player. Since the PS2 emulator is really a PS4 title that runs due to backwards compatibility, they were unable to make changes to the software, and so its JIT privilege had to be spared.

Having JIT privilege means that fully compromising the emulator, including the compiler co-process, would grant the ability to run fully arbitrary native code (not just ROP) on the PS4/PS5 without the need for a kernel exploit. This would be especially convenient on the PS5 because the newly introduced hypervisor enforces that code pages (both userland and kernel) are not readable, and I don't have the patience to try to write a blind kernel exploit again as I did when I ported BadIRET to the PS4 without a kernel dump.

The console was designed to enforce required updates for the Operating System to play the latest games, but the Operating System was not designed with any mechanism to enforce the latest patches for games; ie: old versions of games can always be played on the latest version of the Operating System:

It was designed this way since PlayStation can't be held responsible for the security of third party games (particularly those that statically link to old versions of WebKit). Their security model instead focuses on securing higher privileged layers of the platform (kernel, and hypervisor on PS5), operating under the assumption that games are compromised.

It's my interpretation that the existence of games with special privileges, like the PS2 emulator's JIT, fundamentally violates their own security model because it leaves privileged code with no readily available mechanisms to patch potential future vulnerabilities.

Furthermore, in addition to the gap in their security model that prevents patching existing copies of the games, PlayStation has also decided to not even remove the identified known-exploitable PS2 games for purchase from the store. Because of these two reasons, I'm comfortable referring to this exploit chain as "unpatchable", even if it may not technically be fully accurate.

The kernel assigns each of these process different privileges, implemented by checking the result of the sceSblACMgrIsJitApplicationProcess and sceSblACMgrIsJitCompilerProcess functions (names taken from back when PS4 kernels still had symbols). The compiler can write code, and the application can execute code.

The check used to be implemented incorrectly, and the browser application process on PS4 firmware 1.76 could create both writeable mappings, and executable mappings, but nowadays we would need to control both processes in order to be able to produce fully arbitrary code, and so that will be the goal of this chain.

PS2 save game vulnerabilities are not hard to find; for example, see the GTA decompilations showing a copy from the memory card into a fixed-size buffer with size supplied by the save; exploiting these issues is relatively simple since the PS2 didn't have any exploit mitigations. With one of these exploits, a PS4 save file containing the crafted PS2 memory card can be encrypted and signed for any PSN-ID by anyone with a hacked PS4 on any firmware (or just a PC if they have the decapped SAMU keys), and then imported to the target PS4/PS5 using the USB save import feature in Settings.

A controller-input-triggered exploit would be less practical, except for having the ability to be used without requiring the USB save import feature, which depends on having signed into PSN (since saves are encrypted per-account), and times out on the PS5 after being offline for too long.

I did briefly search for PS2 games available on PS4 which could be exploitable this way, and discovered that Dark Cloud would be (there's a decades-old known bug whereby moving the cursor and pressing X on the same frame in the items menu allows you to pick up an item from out-of-bounds memory, which results in exploitable behaviour), but sadly it only received a digital PS4 release, not a physical PS4 disc release (so it doesn't help remove the PSN requirement).

Given PS2 code execution from any of the 3 identified exploitable PS2 games, I started reverse engineering the emulator itself. The very first thing I looked at was the memory read/write callbacks; you can see on ps2tek that some addresses control various PS2 hardware functionality, and so accessing them requires special code for the emulator to handle those requests.

For example, you can see how the PS2's Linux kernel port performs CDVD S commands using these IO registers. To pass arguments to an S command, they are written byte-by-byte into the SCMD_SEND / SCMD_STATUS register (0x1F402005), and there is a similar register used for supplying arguments to CDVD N commands (0x1f402017).

In other words, simply writing to either of these registers consecutively more than 16 times will lead to overflowing the status buffers with arbitrary bytes; we'll call this Primitive 1, and by submitting invalid commands to reset the index, we can use it repeatedly:

Note that other registers like 0x1f402016 (CDVD S Command), and 0x1f402004 (CDVD N Command), are also vulnerable to buffer overflows, so in total there are at least 4 variant vulnerabilities like this, but since the emulator is quasi-unpatchable, and PlayStation's bounty program stopped accepting PS2 emulator escape reports after the first one, there is no reason to find or analyse other bugs.

Looking back at the handling of writing a byte to the N status register, you'll notice that once we control the N status index, this code path will allow us to write our arbitrary input byte to the N status buffer at an arbitrary 4-byte unsigned index (and then advance the index by 1):

Since the write is made relative to a statically allocated buffer in the eboot's read-write data region, ASLR doesn't affect our ability to corrupt any other reachable targets in the eboot's mapped sections, but just for demonstration purposes: if we temporarily disable ASLR, we can use it to create a small Proof-Of-Concept that writes to the native PS4 address 0x41414141 from within PS2 code execution context:

As with the first primitive, we can do this repeatedly, which results in an extremely powerful primitive: the ability to corrupt any bytes in the eboot's read-write data region that come after the status buffer (since the index is unsigned) to controlled values, without any significant corruption side effects.

If we go back to the memory read/write handlers, we'll see that the code handling virtual memory addresses backed by Random-Access-Memory regions are implemented using pointers. For instance, when the PS2 performs a 32-bit write to IOP RAM, the emulator will eventually perform a write at its native iopram pointer:

By overwriting it, we will effectively remap the emulator's internal pointer to IOP RAM (from its normal value of the fixed address 0x9000000000), so that any read/writes we make from the PS2 to the IOP RAM region will be redirected to our new address.

In practice, this primitive is not very reliable because the emulator runs multiple threads, which may start to behave unexpectedly if we redirect this pointer, so I didn't end up using it in the final exploit. Let's continue browsing for other corruption targets.

Since this program was not compiled with CFI enabled, this will allow us to then trigger a call to our corrupted function pointer by reading from 0x10000000, and the eax register will ultimately be returned as the result back to our PS2 read instruction; we'll call this Primitive 3:

Whilst I had considered that it may be possible to bypass ASLR without any software vulnerability by implementing a spectre-style side channel attack using the high precision timers the PS2 is provided access to, it turned out to be easier to just continue to leverage the primitives I've already established.

I went with the partial-pointer-overwrite technique. This exploits the fact that module base addresses are page aligned, so not fully random. Specifically, the PS4 page size is 0x4000 = 2^14, so the least significant 14-bits (1.75 bytes) of any code address will always be the same.

We know for certain that the least-significant byte of this function's address will always be 0x50. This makes corrupting just this one byte fully deterministic, ie: by changing it to 0x51 we would always point at the offset 1 byte into the function, etc, despite ASLR. Let's add an option to the pre-established callGadgetAndGetResult function to allow partial-pointer overwrite:

So where do we redirect the function pointer to? Knowing that whatever is in eax will be returned back to the PS2 code that initiated the memory read, we need some code that will leave a pointer in eax... If you recall how the function pointer was called, the rax register was used to hold the function pointer address, so we don't need to have it do anything, just immediately return!

With the leaked address, we can derive the address of anything else in any of the eboot binary's mapped sections (since they all coalesce). Another helpful note for us is that since these are the first things mapped into the process their addresses are guaranteed to fit within 32-bits; below is a sample:

We already know what the initial eax value will be at the time of calling the gadget (from the partial-pointer-overwrite leak described above), so we can just subtract it to get esp, and then rsp is predictably esp 0x700000000 (another weakness of the PS4 ASLR implementation):

c80f0f1006
Reply all
Reply to author
Forward
0 new messages