原文始发于mccaulay:mast1c0re: Part 3 – Escaping the emulator

Introduction
In the previous post, we developed a traditional stack buffer overflow exploit in the Okage: Shadow King game which resulted in us being able to execute arbitrary code from within a PlayStation 2 ELF that was embedded inside the exploitable game save file. In this post, we will specifically target a vulnerability within the PlayStation emulator to gain userland ROP (return-oriented programming) code execution on the PlayStation 4 and PlayStation 5.
The research and development discussed within this post was heavily influenced from the research conducted by CTurtE in the blog “mast1c0re: Hacking the PS4 / PS5 through the PS2 Emulator – Part 1 – Escape“. I would strongly recommend you read that blog post in conjunction with this post to improve your understanding of the mast1c0re vulnerability.
Executing a game save on the PlayStation 4 and PlayStation 5
The previous posts within this blog post series specifically focused on developing the initial entry point vulnerability for the PCSX2 PlayStation 2 emulator. However, the vulnerability exists within the Okage: Shadow King game itself and therefore can also be triggered on both the PlayStation 4 and PlayStation 5.
The memory card file within PCSX2 is named Mcd001.ps2
, where as the memory card file within a PlayStation 4 game save file is named VCM0.card
. Although they differ in name and file extension, they both contain the same content type of “Sony PS2 Memory Card Format 1.2.0.0”. This means that we can still use mymc or mymcplus to extract the BASCUS-97129.psu
file, and then use pypsu to extract the bkmo0.dat
file.
└─$ mymcplus -i VMC0.card export BASCUS-97129
Exporing BASCUS-97129 to BASCUS-97129.psu
└─$ psu export BASCUS-97129.psu bkmo0.dat
[+] bkmo0.dat exported to bkmo0.dat
└─$ ls -al
total 8608
drwxr-xr-x 2 user user 4096 Dec 22 15:33 .
drwxr-xr-x 7 user user 4096 Dec 22 15:33 ..
-rw-r--r-- 1 user user 148992 Dec 22 15:33 BASCUS-97129.psu
-rw-r--r-- 1 user user 3460 Dec 22 15:33 bkmo0.dat
-rw-r--r-- 1 user user 8650752 Dec 22 15:33 VMC0.card
PlayStation 4
To import the VMC0.card
file on your PlayStation 4, you require a low firmware version capable of running Apollo PS4 and an FTP server. Additionally, you require an Okage: Shadow King game save which can be created by playing the game as discussed under the heading “Obtaining a game save file” in “mast1c0re: Part 1 – Modifying PS2 game save files“.
Once you have a game save file on your PlayStation 4, open Apollo PS4, choose “HDD Saves”, select “OKAGE: Shadow King – SCUS-97129”, then select “Export decrypted save files” and finally click “VCM0.card”. This will save the existing memory card file to /data/apollo/<user-id>/CUSA02282_SCUS-97129/VCM0.card
.
Next, using an FTP client, overwrite the existing VCM0.card
with the custom built VCM0.card
. Then in Apollo PS4, select “Import decrypted save files”, and finally click “VCM0.card” to import the custom VCM0.card
file into the PlayStation 4 save.
Okage: Shadow King can now be loaded and the exploit will execute on the PlayStation 4.
If you are wanting to sign and encrypt a PlayStation 4 game save using a PlayStation Network account, you must follow the steps in Apollo PS4 (offline-account-activation) to do an offline account activation. This should be done if you wish to use the game save on another console such as a PlayStation 4 on the latest firmware. You can find your PlayStation Network account id by copying a save file from your PlayStation 4 on the account linked to PlayStation Network to a USB, and then viewing the created folder name which should be a 16 character hex value that looks like “343ab456d7ef8901
“. The Console PSID and Console IDPS are not required for this operation.
PlayStation 5
Follow the steps under the PlayStation 4 heading to sign and encrypt a game save file. You must follow the Apollo PS4 (offline-account-activation) steps in order for the save to be accepted on the PlayStation 5. Once the save has been encrypted and signed on the PlayStation 4, copy it to a USB using the system menu (Settings -> Application Saved Data Management -> Saved Data in System Storage -> Copy to USB Storage Device -> OKAGE: Shadow King -> Copy). Plug your USB into your PlayStation 5, then import the save file on to your PlayStation 5 (Settings -> Saved Data and Game/App Settings -> Saved Data (PS4) -> USB Drive -> OKAGE: Shadow King -> Copy).
Debugging on the PlayStation 4
My understanding of the PlayStation emulator is that it translates 64-bit MIPS instructions into x86-64 instructions on an ad hoc basis using a Just-in-time (JIT) compiler. This means that we cannot expect our translated x86-64 instructions to be in the same location in memory each time we execute the game.
The easiest way I found to debug the PlayStation 2 MIPS code on the PlayStation 4 was by writing a value to a specific address in PlayStation 2 memory, and then setting a hardware write breakpoint on that address in the PlayStation 4 memory space.
For this example, I am using a client-server PlayStation 4 debugger I built in 2018 called MEMAPI, which can be found at memapi-debugger. The server binary which is executed through a webkit exploit can be found at memapi-server. Alternatively, it should be possible to follow a similar procedure using ps4debug.
In this example, I am writing the value 0x30
to the address 0xe10000
first, executing some functionality, then writing the value 0x31
to the address 0xe10000
.
First we connect the debugger to the PlayStation 4 and attach to the eboot.bin
process. Next, we set a hardware write breakpoint on the address 0x8000e10000
which is the PlayStation 4 memory address of the PlayStation 2 memory address.

Once hitting the “RESTORE GAME” option in Okage: Shadow King, the hardware breakpoint should trigger once the PlayStation 2 code writes to the 0xe10000
memory address.

Arbitrary gadget execution
We begin by extracting the eboot.bin
binary from Okage: Shadow King version 1.01 on the low firmware PlayStation 4 via FTP and then loading it in Ghidra 9.0.1 using the GhidraPS4Loader plugin. Make sure the base image address is set to 003fc000
in Window -> Memory Map -> <House Icon>
so that addresses match those mentioned in this post.
N/S status buffer overflow
As mentioned in CTurtE‘s blog post, there are multiple fixed memory addresses which when read from or wrote to, trigger hardware functionality to occur. This hardware logic is handled by subroutines within the embedded PlayStation emulator inside the eboot.bin
binary.
For example, when writing to the fixed memory address 0x1f402017
(SCMD_STATUS
), the emulator appends the given byte value to an array. A global variable I named gSStatusIndex
(0x08978a0
) keeps track of the current index in the array gSStatusBuffer
(0x0897820
).
The code within eboot.bin
which handles the SCMD_STATUS
(0x1f402017
) command can be seen in the function _handleIOPCDVDDrive
(0x00479200
) as shown below:
The gSStatusBuffer
(0x0897820
) array has a size of 16 bytes and there is no bounds checking on the gSStatusIndex
(0x08978a0
) value, therefore by triggering the command more than 16 bytes we can write arbitrary bytes beyond the gSStatusBuffer
(0x0897820
) array.
The following sStatusBufferOverflow
function fills the gSStatusBuffer
(0x0897820
) array with 16 (0x10
) null bytes, then proceeds to overflow the buffer with the given overflow
data of length size
.
As you may have noticed from the previous function, the gSStatusIndex
(0x08978a0
) is reset to zero before overflowing the buffer. This is done by sending an invalid command argument to SCMD_COMMAND
(0x1f402016
) which sets the gSStatusIndex
(0x08978a0
) value to zero in the _handleIOPCDVDDrive
(0x00479200
) function. We need to wait for the command to finish processing by checking the SCMD_STATUS
(0x1f402017
) value and waiting until the CMD_STATUS_BUSY
(0x80
) flag is false. Additionally, we need to flush the SCMD_COMMAND
(0x1f402016
) result by reading the data buffer from SCMD_RECV
(0x1f402018
). A similar set of operations is done to reset the gNStatusIndex
(0x0897890
) in PS::Breakout::resetNStatusIndex
as shown below.
Relative write-what-where
We are now able to overflow the gSStatusBuffer
(0x0897820
) and corrupt global values directly beyond the buffer. After 0x60 bytes of other global variables, we can overwrite the gNStatusIndex
(0x0897890
) variable to any unsigned 32-bit integer as shown in the following memory dump:

Therefore, the following setOOBindex
function can be used to set the gNStatusIndex
(0x0897890
) value using the buffer overflow vulnerability.
By setting the gNStatusIndex
(0x0897890
) to an arbitrary positive number we can write any byte outside of the PS2 emulator memory between the eboot.bin
gNStatusBuffer (0x0897810)
address and gNStatusBuffer
(0x0897810
) + UINT32_MAX
(0xFFFFFFFF
).
This can be seen in the function _handleIOPCDVDDrive
(0x00479200
) when the memory address is NCMD_STATUS
(0x1f402005
), as the given byte b
is wrote to gNStatusBuffer
(0x0897810
) at an offset of our controllable index gNStatusIndex
(0x0897890
).
The function PS::Breakout::writeOOB
can then be used to write a single byte outside of the emulator in the PlayStation 4 or PlayStation 5 memory space, by setting the gNStatusIndex
(0x0897890
) value to the relative offset between gNStatusBuffer
(0x0897810
) and the target address using the gSStatusBuffer
(0x0897820
) overflow, then triggering the write with NCMD_SEND
(0x1f402005
). Additionally, we can add helper writeOOB
functions for writing different integer types (uint8_t
, uint16_t
, uint32_t
, uint64_t
).
Arbitrary execute gadget and read EAX
We are now able to write N
bytes of data after the gNStatusBuffer (0x0897810)
address in PlayStation 4 or PlayStation 5 memory. The PlayStation 2 memory address 0x10000000
is for input/output registers and executes a function pointer located in the global variable gIORegisterReadHandlers
(0x60e7880
). As this global variable is defined beyond the gNStatusBuffer (0x0897810)
, we can overwrite the function pointer with any address we desire to execute native instructions on the PlayStation 4 or PlayStation 5. The function pointer is triggered by performing a read on the memory address 0x10000000
, which returns the resulting value of the EAX
register to our PlayStation 2 code. Currently however, we do not know the address of any instructions due to the usage of address space layout randomization (ASLR). A resolution to overcome this protection is discussed further on in this post.
Arbitrary execute gadget and write ESI
As well as a read input/output register, there is also a write input/output register when writing to the address 0x1F801000
in the PlayStation 2. Again, this executes a function pointer named gIORegisterWriteHandlers
(0x0ae7d98
) which is located in the global data section beyond the gNStatusBuffer(0x0897810)
address. We can change this function pointer to any address we desire, and writing a 32-bit integer to the address 0x1F801000
will trigger the function pointer to execute, whilst setting our input value into the ESI
register.
ROP gadgets with rp++
The PlayStation 4 and PlayStation 5 environment has non-executable (NX) memory regions enabled, which means we can only execute code in memory regions which are marked as executable. This restricts us to executing code within the .text
section of the eboot.bin
, which is marked as read-only on the PlayStation 4. Therefore, we cannot jump to the stack and execute the code directly as we did in mast1c0re: Part 2 – Arbitrary PS2 code execution.
We can overcome this protection by using a technique known as return-orientated programming (ROP) which involves jumping to instructions which are followed by an instruction which continues execution from a value popped off the stack. For example, the instructions “pop rax; ret;
” will pop the next eight bytes on the stack into the RAX
register, and then pop the following eight bytes on the stack into the RIP
(instruction pointer) register. This is known as a ROP gadget. As we will be able to control the data on the stack, we can chain these ROP gadgets together. For example the two gadgets “pop rax; ret;
” and “pop rbx; ret;
“, which together are called a ROP chain. As x86-64 instructions are different in their machine code byte lengths, instructions can be unintentionally found at a different offset to the intended x86-64 instruction, which allows us to find instruction chains which would not normally occur within an application.
The tool rp++ by 0vercl0k can be used to automatically identify instruction chains that result in an instruction that allows us to continue controlling the flow of execution by a pointer on the stack.
The following command can be used to generate a list of ROP gadgets that exist in the eboot.bin
file. We need to specify that the .text
address starts at 0x3FC000
which is 0x400000
minus the .text
offset within the ELF file of 0x1000
.
The following output is a small snippet of the generated ROP gadgets:
ASLR Bypass
Address space layout randomization (ASLR) is a binary protection which changes the base address of a process and it’s libraries (configuration dependent) each time the application is executed. This means that the addresses are not fixed in a single location and cannot be hard-coded into an exploit, like they were with the mast1c0re: Part 2 – Arbitrary PS2 code execution vulnerability, as ASLR was not present in that scenario. Although the base address is randomly generated each time the binary (or game in this instance) is executed, the lower bits of the address are consistently the same as the ASLR slide is page-aligned. This means that as the memory page size is 0x4000
for the PlayStation 4, the randomly generated addresses will always end in the same last three hex values as the least significant 14-bits are consistent.
EBOOT address leak
We are now able to execute and either read a value from EAX, or write a value to ESI by overwriting the gIORegisterReadHandlers
(0x60e7880
) or the gIORegisterWriteHandlers
(0x0ae7d98
) as previously discussed. However, due to ASLR we do not know the base address of the eboot.bin
, and therefore we need to determine that before we can execute ROP gadgets.
The original input/output register read handler pointer points to the function FUN_005a9d60
. This function contains a RET
instruction (0x005a9d91
) close to the function start address at as shown:
As the least-significant byte is always the same even though ASLR is enabled, we know the function pointer will end with the byte 0x60
. The upper address values “005a9
” however will not be consistent and will change each time the game is booted. We can overwrite the least-significant byte of the function pointer from 0x60
to 0x91
, which would change the function pointer to directly execute only the RET instruction. Additionally, we can write to this byte with ASLR enabled as the relative offset between the function pointer, the gIORegisterReadHandlers
(0x60e7880
) and the gNStatusIndex
(0x0897890
) remains the same regardless of the eboot.bin
base address as they are in the same memory mapping, and the write what where primitive is a relative write.
By doing this, we can execute only the RET
instruction and retrieve the value of the EAX
register, which contains the address of the gIORegisterReadHandlers
(0x60e7880
) global variable. Calculating the difference of the ASLR gIORegisterReadHandlers
address with the address of gIORegisterReadHandlers
when ASLR is disabled (0x60e7880
) gives us the ASLR slide value. Using this, we can calculate the real address of every address in the eboot.bin
by adding the required address with no ASLR to the ASLR slide value.
The following code leaks the eboot.bin
difference using the method described, and adds a helper defintiion EBOOT
which calculates the ASLR adjusted eboot.bin
address.
Stack address leak
We have leaked the eboot.bin
base address, however we currently do not know the base stack address as the ASLR slide is different to the eboot.bin
address. The stack address leak is required to restore corrupted registers and continue execution after the exploit has executed.
We can leak the stack address by executing a ROP gadget, which sets the value of the ESP
(stack pointer) into EAX
, and then retrieve it with the input/output read handler. Although there is no direct “mov eax, esp; ret ;
” gadget, we can use the gadget “add eax, esp ; ret ;
” as we know the value of EAX
is gIORegisterReadHandlers
(0x60e7880
+ ebootDiff). We can then minus the value of EAX
from the returned address to retrieve the original value of ESP
. ESP
only holds the least significant 32-bits of the stack pointer address. Fortunately, the most significant 32-bits is always 0x00000007
, therefore we can do a bitwise OR with the stack address and 0x0000000700000000
to obtain the 64-bit stack address as shown in the following code snippet:
LibKernel address leak
The libkernel.sprx
library is a dependency used by eboot.bin
and contains various functions and ROP gadgets that we can take advantage of. Again, this library uses ASLR and therefore we need to leak the base address in order to access functions and gadgets within the library. The eboot.bin
binary contains various stub functions which are filled with a pointer to the function in their respective libraries. One stub function is for the function sceKernelUsleep
(00763b30
) and is shown below:
The pointer which is filled in when the application is executed for this function is located at 0x083d1c0
within the eboot.bin
. By opening the PlayStation 4 firmware version 5.05 libkernel.sprx
in Ghidra, we can determine the sceKernelUsleep
function is located at offset 0x013b20
. Therefore, to calculate the base address of the libkernel.sprx
library, we can dereference the eboot.bin
pointer at 0x083d1c0
, then take away the sceKernelUsleep
function offset of 0x013b20
. It is important to note that this is firmware dependent due to the fixed sceKernelUsleep
function offset.
The following code calculates the base libkernel.sprx
address and adds a LIBKERNEL
definition helper:
ROP setup
We now have the ability to execute a single ROP gadget using the read or write input/output interrupt handlers. However, we need to change the value of RSP
(stack pointer) to point to a region of memory we can control from within the PlayStation 2 memory space, to minimize the usage of the read or write input/output interrupt handlers and the out of bounds write vulnerability.
To start off, the following helper functions are defined to allow us to convert memory addresses from the PlayStation 2 emulation address to the PlayStation 4 or PlayStation 5 memory address. Due to the emulation configuration, the PlayStation 2 base address is a fixed address of 0x8000000000
within the PlayStation 4 and PlayStation 5 memory map, even with ASLR enabled.
To setup the ROP chain, we need to change the value of RSP
(stack pointer) with a ROP gadget that uses ESI
, as that is the register we can control using the write interrupt handler. Therefore, the first ROP gadget we will use is:
This gadget will push our controllable value on to the stack, add two registers together, then call the address at [rsi + 0x3b]
. The main part of this gadget is pushing our controllable address on the stack, then the call instruction which will call a gadget at an offset of 0x3b
at an address we can write to. This address will be named STAGE_1
and the address chosen is 0x60F0000
in eboot.bin
as it contains a large amount of NULL bytes and the address fits inside the ESI
register (4 bytes). Although it would be desirable to set RSP
directly to PlayStation 2 memory, it is not possible as all PlayStation 2 memory addresses within the PlayStation 4 and PlayStation 5 start at address 0x8000000000
.
The next ROP gadget we will use is:
This will pop the return address from the previous call instruction off the stack into RCX
. It then executes fld
and clc
which can be ignored. Next, it pops the next value off the stack into RSP
(stack pointer), which is our previously pushed STAGE_1
(0x60F0000
) RSI
value. Finally, the ret
instruction pops the next instruction off the top of the stack, which now points to STAGE_1
(0x60F0000
) as that is the value of the RSP
(stack pointer) register.
The next gadget to be executed is wrote to STAGE_1
(0x60F0000
) before triggering the previous ROP chain:
This gadget allows us to set RSP
(stack pointer) to an arbitrary 8 byte address as we can write to the stack (STAGE_1
) at an offset of 0x08
which follows this instruction.
In memory, the data we write to the STAGE_1
(0x60F0000
) section is shown below:

The following function will trigger the ROP chain located in PlayStation 2 memory at address 0x0e00000
:
The previous function handles writing the data to the STAGE_1
(0x60F0000
) section and executing a single ROP chain command. However, in most cases we will want to execute ROP chains repeatedly from within the PlayStation 2 environment. We can do this by setting up the STAGE_1
(0x60F0000
) memory layout and overwriting the input/output write handler in advance before we need to execute a ROP chain.
We can see this in the following C++ code:
Additionally, the functions resetChain
and pushChain
are helper functions which allow us to easily append addresses or 64-bit values on to the ROP chain.
To execute the ROP chain, we need to trigger the input/output write handler by setting the address of STAGE_1
(0x60F0000
) to the write interrupt register 0x1F801000
. Callee-saved register values that were corrupted during the ROP chain setup are also restored before execution continues back to the PlayStation 4 or PlayStation 5 code. This is where we require the stack address leak as the original RBP
and RSP
register values were stack addresses.
Executing a ROP chain
Combining all of the vulnerabilities and leaks so far, we can create a breakout init
function which performs the necessary steps in order to setup the ROP chain execution. The input/output read handler is also restored to the original function pointer value as it is no longer required once the eboot.bin
and stack addresses have been leaked.
The following code shows a very simple demonstration of executing two seperate ROP chains, with the first setting the value of the RBX
register to 0x11223344
, and the second setting the RAX
register to 0x55667788
.
Setting register values
RAX, RBX, RCX, RDI, RSI
The next step is to create helper functions to set the value of registers in our ROP chain. For the registers RAX
, RBX
, RCX
, RDI
and RSI
this is quite simple, as the corresponding “pop <register>; ret ;
” gadgets exist within the eboot.bin
. For example, the code for setting the RAX register is as shown:
The gadgets for the other registers are:
RDX
Setting the value of RDX
requires a slightly more complicated chain and changes the value of registers RAX
, RBX
and RDX
. The gadget required is “mov rdx, rax ; call rbx ;
“, which means we first need to set the desired value of RDX
into the RAX
register. We can do this using the setRAX
helper defined previously. Next, the instruction is “call rbx
“, which will push RIP
(instruction pointer) to the stack, then execute the gadget placed in RBX
. Therefore, we can set the value of RBX
to the gadget “pop rbx ; ret ;
” which will pop the return pointer that was pushed onto the stack by the call instruction into RBX
, then return to continue executing the ROP chain.
R8
The gadget “mov r8, rbx ; call qword [rax+0x78] ;
” is used to set an arbitrary value into the R8
register. First, we must set the value of RBX
to the desired 64-bit value we want to be stored in R8
. Next, we need to set RAX
to an address which we can write a gadget address to, minus the 0x78
offset. Similar to setting RDX
, we set the gadget to “pop rax ; ret ;
” to pop the call’s return address from the stack.
R13
For setting R13
we use the gadget “mov r13, rax ; call qword [rbx+0x08] ;
“, which follows the same process as setting R8
, however with a different address offset of 0x08
.
R9
Setting the value of R9
uses the most complex gadget setup out of all the registers so far. It also changes the value of registers RAX
, RBX
, RDI
, R8
, R9
, and R13
. Therefore, we should set the R9
register when required before setting other registers to prevent overwriting required existing register values.
The gadget used is “or r9, rax ; movzx eax, dil ; shl rax, 0x04 ; mov qword [r8+rax], rcx ; mov qword [r8+rax+0x08], r9 ; ret ;
” and requires various constraints in order to successfully execute. As the r9
value is set by using a bitwise OR with the RAX
register, we need to first set the value of r9
to zero.
For this, we can use the gadget “and r9d, r13d ; jmp qword [rbx-0x260032D7] ;
” which performs a bitwise AND with r13
. By setting r13
to zero, we can ensure that r9
will also be set to zero. A pointer to a “ret;
” gadget is then set in RBX
at an offset of +0x260032D7
as shown:
Now that R9
is set to zero, we can setup the stack for the gadget “or r9, rax ; movzx eax, dil ; shl rax, 0x04 ; mov qword [r8+rax], rcx ; mov qword [r8+rax+0x08], r9 ; ret ;
” which requires setting RAX
to the required R9
value due to “or r9, rax
” instruction. Next, RDI
must be zero, so that it sets the value of EAX
to zero in the instruction “movzx eax, dil ;
“. After this, “shl rax, 0x04 ;
” will do nothing and RAX
will remain zero. The R8
register must be set to a pointer of a global address which can store the value of RCX
due to the next instruction “mov qword [r8+rax], rcx ;
“. Again, the next instruction “mov qword [r8+rax+0x08], r9 ;
” stores the value of R9
at R8 + 0x08
. Finally, the ret
instruction is reached and the ROP chain execution continues on the stack.
The complete function to set the value of R9
can be seen below:
Getting RAX value
The value of the RAX
register can be retrieved using the gadget “mov qword [rsi], rax ; ret ;
“. This requires the RSI
register to be set to a pointer of the target variable, which we can set using setRSI
.
Function calls
Now that we are able to set the value of most registers with various helper functions, our next step is to execute functions. The following table shows us the registers used for each parameter when calling a function:
Return | RAX |
Argument #1 | RDI |
Argument #2 | RSI |
Argument #3 | RDX |
Argument #4 | RCX |
Argument #5 | R8 |
Argument #6 | R9 |
Argument #7 (N = 0) | Stack+0x08 |
Argument #8 (N = 1) | Stack+0x10 |
Argument #N | Stack+0x08+(0x08 * N) |
The following executeAndGetResult
function pushes the function address on to the stack, followed by retrieving the return value from RAX, and then executes the ROP chain.
0-6 arguments
The following call function shows an example of executing a function with no arguments:
Likewise, the following function demonstrates calling a function with 6 arguments. The same functions exist for calling functions with arguments 1 to 5, setting only the required registers for those function calls.
7 arguments
Calling a function with 7 arguments requires pushing the 7th argument on to the stack. We therefore need to push the gadget “pop rcx ; ret ;
” after the function address in order to pop the 7th argument off the stack after execution the function has completed, in order to continue executing the ROP chain.
8 arguments
Similar to calling a function with 7 arguments, we need to push the additional 2 arguments on to the stack. In this case, the gadget “pop rcx ; rol ch, 0xF8 ; pop rsi ; ret ;
” is used to pop both argument 7 and argument 8 off the stack before continuing the ROP chain.
System calls
The only system call gadget I found that continues execution to the ROP chain was in the libkernel.sprx
binary. As previously discussed, leaking the base address of this binary is firmware dependent, therefore calling system calls is currently firmware dependent.
The following table shows us the registers used for each parameter when calling a system call:
Return | RAX |
Syscall Index | RAX |
Argument #1 | RDI |
Argument #2 | RSI |
Argument #3 | RDX |
Argument #4 | RCX |
Argument #5 | R8 |
Argument #6 | R9 |
An example of calling a system call with 3 arguments is shown below:
Restoring corruption
After executing arbitrary ROP chains within the PlayStation 4 or PlayStation 5, we need to restore any previously corrupted data.
First we need to restore the input/output interrupt write handler to the original function address. Then, we need to reset the gSStatusIndex
and gNStatusIndex
to zero.
Conclusion
We started the blog post series by exploring how to modify game save files for the PlayStation 2, and specifically modify the game save for Okage: Shadow King by calculating the CRC value.
Then, we developed a typical stack-based buffer overflow with no protections for the PlayStation 2, specifically targeting the game save profile name. We expanded upon that exploit by writing a small amount of custom assembly shellcode to load a PlayStation 2 ELF into memory and execute it.
Finally, we leveraged an out of bounds overflow within the PlayStation emulator which we could leverage to write memory at a relative offset beyond the overflow data. Using this write primitive, we overwrote an input/output read handler in order to execute gadgets and defeat ASLR. Then, we overwrote an input/output write handler in order to setup the ROP chain. Next, after writing a lot of small helper functions to dynamically build a ROP chain, we are able to execute any function or system call within the native PlayStation executable memory mappings.
What’s Next? That’s for you to decide.
Other developers can use this project to implement kernel exploits on all firmware versions which have a working kernel exploit, which would result in the ability to run homebrew on those firmware versions on the PlayStation 4. For the PlayStation 5, a kernel exploit would allow users to be able to achieve the same functionality as other vulnerabilities such as a webkit exploit, which I believe is currently mostly limited to enabling debug settings.
The complete mast1c0re project to build custom PlayStation 4 and PlayStation 5 payloads can be found at McCaulay/mast1c0re.
Massive thanks for the initial research from CTurtE with assistance from flatz, balika011, theflow0, chicken(s).
References
- mast1c0re: Hacking the PS4 / PS5 through the PS2 Emulator – Part 1 – Escape
- ps2tek – Documentation on PS2 Internals
- PS2DEV Open Source Project
- PS Dev Wiki – PS2 Emulation
- PS Dev Wiki – PS2 Classics Emulator Compatibility List
- PS Dev Wiki – PS2 Classics Emulator Configuration List
- PS Dev Wiki – PS4 Syscalls
- FreeBSD 9.1 Syscalls
- x64 Cheatsheet
- CTurt/PS4-SDK
- OpenOrbis-PS4-Toolchain