原文始发于mccaulay：mast1c0re: Part 3 – Escaping the emulator
mast1c0re: Part 3 – Escaping the emulator
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 18.104.22.168”. This means that we can still use mymc or mymcplus to extract the
BASCUS-97129.psu file, and then use pypsu to extract the
└─$ 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
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
Apollo Main Menu
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.
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
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
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
For example, when writing to the fixed memory address
SCMD_STATUS), the emulator appends the given byte value to an array. A global variable I named
0x08978a0) keeps track of the current index in the array
The code within
eboot.bin which handles the
0x1f402017) command can be seen in the function
0x00479200) as shown below:
0x0897820) array has a size of 16 bytes and there is no bounds checking on the
0x08978a0) value, therefore by triggering the command more than 16 bytes we can write arbitrary bytes beyond the
sStatusBufferOverflow function fills the
0x0897820) array with 16 (
0x10) null bytes, then proceeds to overflow the buffer with the given
overflow data of length
As you may have noticed from the previous function, the
0x08978a0) is reset to zero before overflowing the buffer. This is done by sending an invalid command argument to
0x1f402016) which sets the
0x08978a0) value to zero in the
0x00479200) function. We need to wait for the command to finish processing by checking the
0x1f402017) value and waiting until the
0x80) flag is false. Additionally, we need to flush the
0x1f402016) result by reading the data buffer from
0x1f402018). A similar set of operations is done to reset the
PS::Breakout::resetNStatusIndex as shown below.
We are now able to overflow the
0x0897820) and corrupt global values directly beyond the buffer. After 0x60 bytes of other global variables, we can overwrite the
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
0x0897890) value using the buffer overflow vulnerability.
By setting the
0x0897890) to an arbitrary positive number we can write any byte outside of the PS2 emulator memory between the
gNStatusBuffer (0x0897810) address and
This can be seen in the function
0x00479200) when the memory address is
0x1f402005), as the given byte
b is wrote to
0x0897810) at an offset of our controllable index
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
0x0897890) value to the relative offset between
0x0897810) and the target address using the
0x0897820) overflow, then triggering the write with
0x1f402005). Additionally, we can add helper
writeOOB functions for writing different integer types (
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
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
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
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
The following output is a small snippet of the generated ROP gadgets:
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
0x60e7880) or the
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
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
0x60e7880) and the
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
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
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
0x60e7880 + ebootDiff). We can then minus the value of
EAX from the returned address to retrieve the original value of
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
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
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:
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
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
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
clc which can be ignored. Next, it pops the next value off the stack into
RSP (stack pointer), which is our previously pushed
RSI value. Finally, the
ret instruction pops the next instruction off the top of the stack, which now points to
0x60F0000) as that is the value of the
RSP (stack pointer) register.
The next gadget to be executed is wrote to
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
0x60F0000) section is shown below:
The following function will trigger the ROP chain located in PlayStation 2 memory at address
The previous function handles writing the data to the
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
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
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
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
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
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
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:
Setting the value of
RDX requires a slightly more complicated chain and changes the value of registers
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.
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 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
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
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:
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
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
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:
|Argument #7 (N = 0)||Stack+0x08|
|Argument #8 (N = 1)||Stack+0x10|
|Argument #N||Stack+0x08+(0x08 * N)|
executeAndGetResult function pushes the function address on to the stack, followed by retrieving the return value from RAX, and then executes the ROP chain.
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.
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.
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.
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:
An example of calling a system call with 3 arguments is shown below:
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
gNStatusIndex to zero.
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.
- 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