Sony PlayStation 4 protection analysis
- Transfer

Since there have not been any public statements regarding the PS4 hack for a long time, it is time to break the silence and talk a little about how far the progress has come with regard to hacking the PS4, as well as about the reasons that prevent us from moving forward.
In this article, I will touch on some security principles that apply to all modern systems, as well as share my findings made by performing ROP tests on my PS4.
If you are new to exploits, you should first read my last article on hacking DS games using the stack smash vulnerability in save files.
Download everything you need for your own experiments here, currently only firmware 1.76 is supported.
Famous PS4 Facts
As you most likely know, the PS4 uses a special eight-core x86-64 CPU from AMD, a lot of research has been published about its architecture, and even if this specific version of the processor is slightly different from the generally accepted standard, this is hardly noticeable. For example, PFLA ( Page Fault The Liberation Army ) at the 29C3 ( 29Th Chaos Communication Congress ) showed evidence of proof-of-concept that can realize the full Turing machine using only the page faults ( page fault occurred ) and x86 MMU, video is available on YouTube . This will be interesting to those who, having run the code in a virtual machine, while wanting to follow instructions on the host CPU.EurAsia news article under number 3251
Moreover, we are dealing not only with a well-documented CPU architecture - the software used in PS4 is mostly open source .
For us, the most important thing is that the Orbis OS, on which the console works, is based on FreeBSD and uses separate parts of NetBSD, repeating the situation with PS3 in this regard; in addition to FreeBSD 9.0, Mono VM and WebKit are used from other notable large software.
Entry Point - WebKit
WebKit is an open browser web rendering engine for iOS, Wii U, 3DS, PS Vita and PS4.
Despite the widespread use and maturity of the project, WebKit is not without some vulnerabilities; you can learn about most of them from the Pwn2Own records .
In particular, the browser PS4 firmware version 1.76 using WebKit, vulnerable to CVE-2012-3748 , a buffer overflow in the heap data ( heap-based buffer overflow ) in the method
JSArray::sort(...)
. In 2014, nas and Proxima announced that they were able to successfully port this exploit for use on the PS4 browser, and put the PoC code in public, which marked the beginning of the PS4 hacking process.
This code gives random access to read and write everything that the WebKit process can read / write, and this in turn can be used to dump modules and rewrite return addresses on the stack, allowing us to establish control over the instruction counter (for ROP).
Since then, many other vulnerabilities in WebKit have been discovered that supposedly allow dumping of modules and ROP on the latest PS4 firmware, but at the time of writing, none of these exploits were ported to PS4.
What is ROP (return oriented programming)?
Unlike primitive devices like DS and PSP, PS4 uses a kernel that controls the options of different memory areas. Memory pages marked with executable cannot be overwritten; pages marked with writable cannot be completed; this principle is known as Data Execution Prevention (DEP) .
For us, this means the impossibility of using a simple way: copying the payload to the memory and its subsequent execution. However, we can execute code that is already loaded into memory and marked as executable.
The mere possibility of jumping to one address is not particularly useful if we cannot write our own code at this address - this is why we will resort to ROP.
Return Oriented Programming (ROP) is just an improved version of the traditional “stack smashing” (buffer overflow attacks), but instead of overwriting the single value that the PC jumps to, we can chain together many different addresses, known as “gadgets”.
Usually A gadget is just the only design you want to follow
ret
. In x86_64 assembler, when execution reaches the instruction
ret
, a 64-bit value is popped off the stack and the PC jumps on it; since we can control the stack, we can make each instruction ret
jump to the next desired gadget. For example, starting from
0x80000
instructions may be stored:mov rax, 0
ret
And starting from the
0x90000
following instructions are stored:mov rbx, 0
ret
If we rewrite the return address on the stack so that it will
0x80000
follow 0x90000
, then as soon as the execution reaches the first instruction ret
, it will jump to mov rax, 0
, and immediately after that the next instruction ret
will pop out of the stack 0x90000
and jump to mov rbx, 0
. Thus, this chain will play into our hands and set both registers
rax
and rbx
0, as if we just wrote the code in one place and carried out consistently. ROP chains are not limited to a list of addresses; suppose the
0xa0000
following instructions come with :pop rax
ret
We can set the first element of the chain to
0xa0000
and the next element to any desired value for rax
. Gadgets are also not required to end with instructions
ret
; we can use gadgets ending in jmp
:add rax, 8
jmp rcx
Having done so that
rcx
points to the instruction ret
, the chain will be executed in the usual way:chain.add("pop rcx", "ret");
chain.add("add rax, 8; jmp rcx");
Sometimes you won’t be able to find exactly the gadget that you need, by itself - only with other instructions after it. For example, if you want to set
r8
to some value, but you only have this gadget, then you have to set r9
to some dummy value:pop r8
pop r9
ret
Although from time to time you will have to show your creativity abilities when writing ROP chains, nevertheless, it is generally accepted that when using a sufficiently large dump of code, the gadgets received will be enough for full Turing functionality; this makes ROP a viable DEP bypass method.
Gadget Search
The following metaphor will help you understand ROP.
Imagine that you are writing a new chapter in a book, while using only those words that were at the ends of the sentences of the previous chapters. Obviously, due to the principles of phrase construction, you are unlikely to find the words “and” or “or” at the end of one of the sentences - but we will need these connecting elements if we want to write something meaningful.
It is possible, however, that one of the sentences ended with the word “sand”. And, although according to the author’s intention, we should read this whole word beginning with the letter “s”, if we start our reading with “a”, then by pure chance we get a completely different word - “and”, which we needed.
These principles also apply to ROP.
Since the structure of almost all functions looks something like this:
; Сохранение регистров
push rbp
mov rbp, rsp
push r15
push r14
push r13
push r12
push rbx
subrsp, 18h; Тело функции
; Восстановление регистров
add rsp, 18h
pop rbx
pop r12
pop r13
pop r14
pop r15
pop rbp
ret
Therefore, you should expect to find only gadgets
pop
, or, less commonly xor rax, rax
, that set the value to 0 before returning. Comparison like
cmp [rax], r12
ret
makes no sense, since the result of the comparison is not used by the function. However, the likelihood of discovering such gadgets still remains.
X86_64 instructions are similar to words in that they have a variable length, and can mean completely different things depending on where the decoding starts.
The x86_64 architecture is a set of variable-length CISC instructions. Return-oriented programming on x86_64 takes advantage of the fact that the instruction set is very "dense" - in the sense that any arbitrary sequence of bytes can most often be interpreted as a valid set of x86_64 instructions.
- Wikipedia
To demonstrate this, take a look at the end of this function from the WebKit module:
000000000052BE0Dmoveax, [rdx+8]
000000000052BE10mov[rsi+10h], eax
000000000052BE13orbyteptr[rsi+39h], 20h
000000000052BE17ret
Now take a look at what the code will look like if we start decoding with
0x52be14
:000000000052BE14cmp[rax], r12
000000000052BE17ret
Although this code was never intended to be executed, it is located in the memory area, which was marked as “executable”, which makes it very attractive for use as a gadget.
Of course, it would be incredibly expensive to spend time looking for all possible ways to interpret the code before each instruction
ret
manually; existing utilities can do this for us. To search for ROP gadgets, I prefer to use rp ++ ; to generate a text file filled with gadgets just enter the command:rp-win-x64 -f mod14.bin --raw=x64 --rop=1 --unique > mod14.txt
Segmentation errors
If we try to execute a non-executable memory page, or try to write to a non-writable memory page, a segmentation error will occur.
For example, this is an attempt to execute code on a stack that is "read only" read-write (rw):
setU8to(chain.data + 0, 0xeb);setU8to(chain.data + 1, 0xfe);chain.add(chain.data);
And so - an attempt to write code that is "read only" and read (rx):
setU8to(moduleBases[webkit], 0);
If a segmentation error occurs, the message “Not enough free system memory” appears on the screen, and the page does not load:

The reason for the output of this message may be something else - for example, executing an incorrect instruction or an unrealized system call - but most often it crawls out precisely because of a segmentation error.
ASLR
Address Space Layout Randomization (ASLR) is a security technology used in operating systems that randomly changes the location of important structures in the process address space, namely: the image of the executable file, loaded libraries, heap and stack. Because of it, the base addresses of the modules change every time you launch your PS4.
I received evidence that in the oldest firmware versions (1.05) ASLR was disabled , but it appeared somewhere around 1.70. Note that ASLR for the kernel is disabled, at least for firmware version 1.76 and below, and this will be proved further.
For most exploits, ASLR will be a problem, because if you do not know the addresses of the gadgets in memory, then you will not guess what needs to be written to the stack.
Fortunately for us, we are not limited to writing static ROP chains. We can use JavaScript to read the module table, which will help us get the base addresses of the loaded modules. Using these addresses, we can calculate the addresses of all our gadgets before completing the ROP chain, bypassing ASLR.
The module table also includes module file names:
- WebProcess.self
- libkernel.sprx
- libSceLibcInternal.sprx
- libSceSysmodule.sprx
- libSceNet.sprx
- libSceNetCtl.sprx
- libSceIpmi.sprx
- libSceMbus.sprx
- libSceRegMgr.sprx
- libSceRtc.sprx
- libScePad.sprx
- libSceVideoOut.sprx
- libScePigletv2VSH.sprx
- libSceOrbisCompat.sprx
- libSceWebKit2.sprx
- libSceSysCore.sprx
- libSceSsl.sprx
- libSceVideoCoreServerInterface.sprx
- libSceSystemService.sprx
- libSceCompositeExt.sprx
Despite the fact that PS4 for the most part uses the [Signed] PPU Relocatable Executable ([S] PRX) format for modules, lines referring to the [Signed] Executable and Linking Format ([S] ELF) object files are seen in the libSceSysmodule.sprx dump ) - bdj.elf, web_core.elf and orbis-jsc-compiler.self.
This combination of modules and objects resembles the one used in the PSP and PS3.
A complete list of all available modules (and not just those loaded by the browser) can be found in
libSceSysmodule.sprx
. We can download and dump some of them thanks to several special system calls for the authorship of Sony, which will be discussed later.JuSt-ROP
Using JavaScript to write and execute dynamic ROP chains gives us a huge advantage over a regular buffer overflow attack.
In addition to bypassing ASLR, we can read the browser user agent, and substitute a different ROP chain for a different version of the browser, giving our exploit the highest degree of compatibility possible.
We can even use JavaScript to read memory at our gadget addresses in order to make sure they are correct, which gives us almost perfect reliability.
Dynamic spelling of ROP chains makes sense compared to their preliminary generation by a script.
For these reasons, I created my own JavaScript framework for writing ROP chains, JuSt-ROP .
JavaScript pitfalls
JavaScript uses the IEEE-754 double-precision representation of numbers (64 bits) . This gives us 53 bits of accuracy (the VT_R8 mantissa has only 53 bits), which means that it is not possible to display each 64-bit value - for some of them it will be necessary to apply the approximation.
If you just need to set the 64-bit number to some small value, sort of
256
, then it setU64to
will cope with the task. But for cases when you need to write a buffer or data structure, there is a possibility that individual bytes will be written incorrectly if they were written in blocks of 64 bits. Instead, you should write data in 32-bit blocks (remembering that PS4 uses little-endian order) to make sure that each byte is identical.System calls
Interestingly, PS4 uses the same call format as Linux and MS-DOS for system calls, with arguments stored in registers, rather than the traditional UNIX way (which FreeBSD uses by default) when the arguments are stored on the stack:
Register | Value |
---|---|
rax | System call number |
rdi | Argument 1 |
rsi | Argument 2 |
rdx | Argument 3 |
r10 | Argument 4 |
r8 | Argument 5 |
r9 | Argument 6 |
We can try to make any system call using the JuSt-ROP method:
this.syscall = function(name, systemCallNumber, arg1, arg2, arg3, arg4, arg5, arg6) {
console.log("syscall " + name);
this.add("pop rax", systemCallNumber);
if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
this.add("pop rbp", stackBase + returnAddress - (chainLength + 8) + 0x1480);
this.add("mov r10, rcx; syscall");
}
Using system calls can tell us a lot about the PS4 core. Moreover, using system calls is the only way we can interact with the kernel, and could potentially perform a kernel exploit.
If you reverse engineer the modules to identify some of Sony’s special system calls, you can find an alternative call format:
Register | Value |
---|---|
rax | 0 |
rdi | System call number |
rsi | Argument 1 |
rdx | Argument 2 |
r10 | Argument 3 |
r8 | Argument 4 |
r9 | Argument 5 |
Apparently, Sony did so for easy compatibility with a function calling convention, for example:
unsignedlongsyscall(unsignedlong n, ...){
registerunsignedlong rax asm("rax");
asm("mov r10, rcx");
rax = 0;
asm("syscall");
return rax;
}
Using this approach, they can make any system call from C.
When writing ROP chains, we can use the following convention:
// Обе команды возвращают ID текущего процесса:
chain.syscall("getpid", 20);
chain.syscall("getpid", 0, 20);
It is useful to remember this in case you can choose the most convenient of the available gadgets.
getpid
One single system call, number
20
, getpid(void)
is already capable of telling us a lot about the kernel. The very fact that this system call works tells us that Sony did not even bother to mix the system call numbers, as required by the " security through obscurity " technique (and under the BSD license they could do this without publishing new system numbers on the Internet calls).
Thus, we automatically got our hands on a list of system calls that you can try to make in the PS4 core.
Secondly, by calling
getpid()
, restarting the browser, and then calling it again, we get a return value of 1 greater than the previous one. Although FreeBSD supports PID randomizationsince version 4.0, sequential PID allocation is the default behavior. Apparently, Sony did not bother to strengthen the protection here, as it did in projects like HardenedBSD .How many system calls are there?
The last system call in FreeBSD 9 is
wait6
followed by a number 523
; all the numbers above are Sony's special system calls. Attempting to call any of spetsilno Sony system calls will return an error without correct arguments
0x16
, "Invalid argument"
; however, any compatible system calls, or unrealized system calls, will result in an error "There is not enough free system memory"
. Through trial and error, I found out that the system call under the number
617
is the last Sony call, all calls are not implemented further. Based on this, we can make a logical conclusion that in the PS4 kernel there are 85 special system calls (617 - 532) for the authorship of Sony.
This is significantly less than it was in the PS3, which had almost 1000 system calls in total. Well, even if this indicates less scope for potential attack vectors, it will be easier for us to document all the challenges.
We are going further. Nine of these 85 system calls always return 0x4e, ENOSYS, which means a simple thing - these calls only work on test devices for developers, leaving us with only 76 useful calls.
Of these 76, libkernel.sprx refers to only 45 (all applications that are not part of the kernel use this module to make system calls). In total, the developer has only 45 available special system calls.
Interestingly, although only 45 calls were intended for use (since libkernel.sprx has wrappers for them), some of the remaining 31 are still available from the browser process. It is possible that in these unintentionally abandoned calls, the probability of finding a vulnerability is much higher, since their testing of time clearly took the least.
libkernel.sprx
In order to understand how special system calls are used by the kernel, the main thing is to remember that this is just a modification of the standard FreeBSD 9.0 libraries.
Here is an excerpt
_libpthread_init
from the file thr_init.c
:/*
* Check for the special case of this process running as
* orin place of init as pid = 1:
*/
if ((_thr_pid = getpid()) == 1) {
/*
* Setup a new session for this process which is
* assumed to be running as root.
*/
if (setsid() == -1)
PANIC("Can't set session ID");
if (revoke(_PATH_CONSOLE) != 0)
PANIC("Can't revoke console");
if ((fd = __sys_open(_PATH_CONSOLE, O_RDWR)) < 0)
PANIC("Can't open console");
if (setlogin("root") == -1)
PANIC("Can't set login to root");
if (_ioctl(fd, TIOCSCTTY, (char *) NULL) == -1)
PANIC("Can't set controlling terminal");
}
The same function can be found on the offset
0x215F0
from libkernel.sprx
. This is how the above code looks in the libkernel dump:call getpid
mov cs:dword_5B638, eax
cmp eax, 1
jnz short loc_2169F
call setsid
cmp eax, 0FFFFFFFFh
jz loc_21A0C
lea rdi, aDevConsole ; "/dev/console"
callrevoketest eax, eax
jnz loc_21A24
lea rdi, aDevConsole ; "/dev/console"
mov esi, 2
xor al, al
callopen
mov r14d, eax
test r14d, r14d
js loc_21A3C
lea rdi, aRoot ; "root"
call setlogin
cmp eax, 0FFFFFFFFh
jz loc_21A54
mov edi, r14d
mov esi, 20007461h
xor edx, edx
xor al, al
call ioctl
cmp eax, 0FFFFFFFFh
jz loc_21A6C
Reversing module dumps for analyzing system calls
libkernel is not fully open: it contains a large amount of Sony's own code that could reveal their system calls.
Although the analysis process will differ depending on the selected system call, for some of them it is quite simple to find out the composition of the arguments that are passed to the call.
A system call wrapper will be declared somewhere in libkernel.sprx and will almost always follow the following pattern:
000000000000DB70 syscall_601 proc near
000000000000DB70 mov rax, 259h
000000000000DB77 mov r10, rcx
000000000000DB7A syscall
000000000000DB7C jb short error000000000000DB7E retn
000000000000DB7F
000000000000DB7F error:
000000000000DB7F lea rcx, sub_DF60
000000000000DB86 jmp rcx
000000000000DB86 syscall_601 endp
Note that an instruction
mov r10, rcx
does not necessarily mean that a system call takes at least 4 arguments; this instruction is at all system calls wrappers, and even those that do not take any arguments - for example, getpid
. Once you find the wrapper, you can look at the xrefs to it:
0000000000011D50 mov edi, 10h
0000000000011D55 xor esi, esi
0000000000011D57 mov edx, 1
0000000000011D5C call syscall_601
0000000000011D61 test eax, eax
0000000000011D63 jz short loc_11D6A
A good idea would be to look for a few more, just to make sure that the registers have not been changed for anything unrelated:
0000000000011A28 mov edi, 9
0000000000011A2D xor esi, esi
0000000000011A2F xor edx, edx
0000000000011A31 call syscall_601
0000000000011A36 test eax, eax
0000000000011A38 jz short loc_11A3F
We see how with the enviable constancy of the first three registers from the system call agreement (rdi, rsi, and rdx), so that we can quite confidently declare that the call takes three arguments.
To understand, here's how we play these calls with JuSt-ROP:
chain.syscall("unknown", 601, 0x10, 0, 1);
chain.syscall("unknown", 601, 9, 0, 0);
Like most system calls, these calls will return 0 if successful, as can be seen in the code above, where
jz
the transition after test
a returns value. Finding out something more complex than the number of arguments will require a much deeper analysis of the code before and after the call to understand the context, but the above should be enough for you to start.
Bruteforce system calls
Despite the fact that reverse engineering of module dumps is the most reliable way to identify system calls, some of them are not mentioned in dumps, so we are forced to analyze them “blindly”.
If we assume that a certain system call can take a certain set of arguments, then we can bruteforce all system calls that return a certain value (0 for success) with the selected arguments, and ignore all those that return an error.
We can also transfer zeros for all arguments, and brute force, all system calls that return helpful error like
0xe
, "Bad address"
that indicate that the call is received at least one pointer.First, we need to run the ROP chain as soon as the page loads. We can do this by hanging our function on an
onload
element body
:<bodyonload="exploit()">
Next we need to make a special system call depending on the value from the HTTP GET. Although this can be done using JavaScript, I use PHP for simplicity:
var Sony = 533;
chain.syscall("Sony system call", Sony + <?phpprint($_GET["b"]); ?>, 0, 0, 0, 0, 0, 0);
chain.write_rax_ToVariable(0);
As soon as the system call is completed, we can check the return value, and if it does not give us anything interesting, redirect to the following system call:
if(chain.getVariable(0) == 0x16) window.location.assign("index.php?b=" + (<?phpprint($_GET["b"]); ?> + 1).toString());
Running a page with? B = 0 at the end will launch bruteforce from the first Sony system call.
Although this method requires a considerable amount of experimentation, we can confidently say that it will allow you to find several system calls that you can partially identify.
System call 538
As an example, let's look at the 538 system call without relying on dumps of any modules.
Here are the return values, depending on what is passed as the first argument:
- 0 - 0x16, "Invalid argument"
- 1 - 0xe, "Bad address"
- A pointer to 0 is initially 0x64, but with each page refresh, the value increases by 1.
Other potential arguments you can try to substitute are PID, stream ID, and file descriptor.
Despite the fact that most system calls return 0 on successful execution, some calls return a value that increases with each new call - apparently, these calls allocate some resource, such as a file descriptor.
The next step will be to monitor the data before and after making the system call in order to understand whether anything was written to them.
Since there are no changes in the data, we can with good conscience assume that this is input.
Then try to feed the method a long string as the first argument. You should try this with every input that you can detect, since there is a chance of detecting buffer overflows.
writeString(chain.data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0);
We obtain as the value of the return
0x3f
, ENAMETOOLONG
. Alas, we see that the system call correctly restricts the name (32 bytes including a delimiter NULL
), but now we know that the method expects a string, not a structure. Well, now we have a few ideas on what this challenge can do. The most obvious option is some kind of action related to the file system (for example, a special version
mkdir
or open
), but this version is unlikely to suit us - after all, the resource is allocated even before we write any data in the index. Let's try to check if the first parameter is a path. We break it into several characters
/
and see if this allows us to pass a long string to the method:writeString(chain.data, "aaaaaaaaaa/aaaaaaaaaa/aaaaaaaaaa");chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0);
Since this call will also return 0x3f, we can assume that the first argument is not the way; this is the name for something that will be stored in memory and will receive a sequential identifier .
After analyzing other system calls, I was able to find that all of the following have the same behavior:
- 533
- 538
- 557
- 574
- 580
Using the information received it is almost impossible to guess what exactly these system calls do, but if you conduct other tests, you will gradually reveal the secret. I will save you a little time - the system call 538 allocates memory for the event flag (and takes not only a name as a parameter).
With basic knowledge of how the kernel works, you can guess and then check what memory is allocated for system calls — semaphores, mutexts, and so on.
Dump additional modules
We can dump additional modules as follows:
- Download module
- Get the base address of the module
- Dump module.
I took on the tedious labor of loading and dumping every possible module from the browser with my hands and published the results on psdevwiki . All modules marked “Yes” can be dumped using this method.
To load the module, we need to use the function
sceSysmoduleLoadModule
from libSceSysmodule.sprx + 0x1850
. The first parameter is the identifier of the loaded module, the other three simply pass 0. The JuSt-ROP method below is useful for making this call:
this.call = function(name, module, address, arg1, arg2, arg3, arg4, arg5, arg6) {
console.log("call " + name);
if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
this.add("pop rbp", stack_base + return_va - (chainLength + 8) + 0x1480);
this.add(module_bases[module] + address);
}
So, for download we
libSceAvSetting.sprx (0xb)
use:chain.call("sceSysmoduleLoadModule", libSysmodule, 0x1850, 0xb, 0, 0, 0);
Like most system calls, this should return 0 on success. To see the identifier of the module allocated in the memory, we can use one of Sony's system calls number 592 to get a list of loaded modules:
var countAddress = chain.data;
var modulesAddress = chain.data + 8;
// Системный вызов 592, getLoadedModules(int *destinationModuleIDs, int max, int *count);
chain.syscall("getLoadedModules", 592, modulesAddress, 256, countAddress);
chain.execute(function() {
var count = getU64from(countAddress);
for(var index = 0; index < count; index++) {
logAdd("Module: 0x" + getU32from(modulesAddress + index * 4).toString(16));
}
});
Executing this code without loading other additional modules will display the following list:
0x0, 0x1, 0x2, 0xc, 0xe, 0xf, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1e, 0x37, 0x59
However, if we run it after loading the 0xb module, we will see an additional element, 0x65. Remember - the identifier of the module is not the same as the identifier of the loaded module.
Now we can use another Sony system call number 593, which takes the identifier of the loaded module and the buffer, and fills the buffer with information about the loaded module, including its base address. Since the identifier of the loaded module is always 0x65, we can “hardcode” it into our chain, instead of storing the result from the list of modules.
The buffer must start with a structure size to be returned, otherwise, it returns an error
0x16
, "Invalid argument"
:setU64to(moduleInfoAddress, 0x160);
chain.syscall("getModuleInfo", 593, 0x65, moduleInfoAddress);
chain.execute(function() {
logAdd(hexDump(moduleInfoAddress, 0x160));
});
If successful, 0 will be returned, and the buffer will be filled with a structure that can be read like this:
var name = readString(moduleInfoAddress + 0x8);
var codeBase = getU64from(moduleInfoAddress + 0x108);
var codeSize = getU32from(moduleInfoAddress + 0x110);
var dataBase = getU64from(moduleInfoAddress + 0x118);
var dataSize = getU32from(moduleInfoAddress + 0x120);
Now we have everything you need for a module dump!
dump(codeBase, codeSize + dataSize);
There is another Sony system call, number 608, which works in a manner similar to 593, but provides slightly different information about the loaded module:
setU64to(moduleInfoAddress, 0x1a8);
chain.syscall("getDifferentModuleInfo", 608, 0x65, 0, moduleInfoAddress);
logAdd(hexDump(moduleInfoAddress, 0x1a8));
It is not known what this information may mean.
Exploring the file system
PS4 uses standard FreeBSD 9.0 system calls to read files and directories.
However, while reading certain directories seems to
/dev/
work, reading others, for example, /
does not. I don’t know why this happens, but if used
gendents
instead of read
for directories, then everything will work more reliably:writeString(chain.data, "/dev/");chain.syscall("open", 5, chain.data, 0, 0);chain.write_rax_ToVariable(0);
chain.read_rdi_FromVariable(0);
chain.syscall("getdents", 272, undefined, chain.data + 0x10, 1028);
Here is the resulting memory:
0000010: 07000000100002056469707377000000 ........dipsw...
0000020: 08000000100002046e75 6c6c 00000000 ........null....
0000030: 09000000100002047a65 726f 00000000 ........zero....
0000040: 030100000c00 0402666400000b00 0000 ........fd......
0000050: 10000a05 737464696e00 00000d00 0000 ....stdin.......
0000060: 10000a06 7374646f 757400000f00 0000 ....stdout......
0000070: 10000a06 737464657272000010000000 ....stderr......
0000080: 10000205646d 656d 3000000011000000 ....dmem0.......
0000090: 10000205646d 656d 3100000013000000 ....dmem1.......
00000a0: 1000020672616e64 6f6d 000014000000 ....random......
00000b0: 10000a07 7572616e 646f 6d00 16000000 ....urandom.....
00000c0: 1400020b 646563695f73 74646f75 7400 ....deci_stdout.
00000d0: 170000001400020b 646563695f73 7464 ........deci_std
00000e0: 65727200180000001400020964656369 err.........deci
00000f0: 5f74 7479320000001900000014000209 _tty2...........
0000100: 646563695f74 7479330000001a00 0000 deci_tty3.......
0000110: 14000209646563695f74 747934000000 ....deci_tty4...
0000120: 1b00 000014000209646563695f74 7479 ........deci_tty
0000130: 350000001c00 000014000209646563695...........deci
0000140: 5f74 7479360000001d00 000014000209 _tty6...........
0000150: 646563695f74 7479370000001e00 0000 deci_tty7.......
0000160: 1400020a 646563695f74 747961300000 ....deci_ttya0..
0000170: 1f00 00001400020a 646563695f74 7479 ........deci_tty
0000180: 62300000200000001400020a 64656369 b0.. .......deci
0000190: 5f74 747963300000220000001400020a _ttyc0..".......00001a0: 646563695f73 7464696e 000023000000 deci_stdin..#...00001b0: 0c00 0203627066002400000010000a04 ....bpf.$.......
00001c0: 6270663000000000290000000c00 0203 bpf0....).......
00001d0: 686964002c00 0000140002087363655f hid.,.......sce_
00001e0: 7a6c 6962000000002e00 000010000204 zlib............
00001f0: 6374747900000000340000000c00 0202 ctty....4.......
0000200: 67630000390000000c00 020364636500 gc..9.......dce.
0000210: 3a00 0000100002056462676763000000 :.......dbggc...
0000220: 3e00 00000c00 0203616a 6d00 41000000 >.......ajm.A...
0000230: 0c00 020375766400420000000c00 0203 ....uvd.B.......
0000240: 76636500450000001800020d 6e6f 7469 vce.E.......noti
0000250: 6669636174696f6e 3000000046000000 fication0...F...
0000260: 1800020d 6e6f 74696669636174696f6e ....notification
0000270: 310000005000000010000206757362631...P.......usbc
0000280: 746c 0000560000001000020663616d65 tl..V.......came
0000290: 72610000850000000c00 0203726e 6700 ra..........rng.
00002a0: 070100000c00 040375736200 c900 0000 ........usb.....
00002b0: 10000a07 7567656e 302e 340000000000 ....ugen0.4.....
00002c0: 00000000000000000000000000000000 ................
Some of these devices can be read, for example, reading will
/dev/urandom
fill the memory with random data. You can also parse this memory and get a list of entities; take a look at
browser.html
from the repository that acts as a file manager:
Alas, because of the sandbox, we do not have full access to the file system. Attempt to read files or directories that exist , but access to them is limited, you will return error 2
ENOENT
, "No such file or directory"
. True, we can still get access to various interesting things - encrypted save files, trophies and account information - I will tell you more about them in my next articles.Sandbox
The problem with the operation of system calls is not limited to individual paths - there are other reasons why they cannot be completed.
Most often, the forbidden system call will simply return an error 1
EPERM
, "Operation not permitted"
; This statement is true for calls like ptrace
, because other system calls will not work for a variety of reasons. Compatible system calls are disabled. For example, if you want to call
mmap
, you must use the system call number 477 , not 71 or 197 ; otherwise, you will get a segfault. Other system calls, like
exit
, will also cause a segmentation fault :chain.syscall("exit", 1, 0);
An attempt to create SCTP-socket returns an error
0x2b
, EPROTONOSUPPORT
indicating that the SCTP-sockets were off in PS4 kernel://int socket(intdomain, inttype, int protocol);
//socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
chain.syscall("socket", 97, 2, 1, 132);
And, although a call
mmap
with PROT_READ | PROT_WRITE | PROT_EXEC
will return a valid pointer, the flag PROT_EXEC
will be ignored. Reading its protection will return 3 (RW), and any attempt to execute memory will result in segfault:chain.syscall("mmap", 477, 0, 4096, 1 | 2 | 4, 4096, -1, 0);
chain.write_rax_ToVariable(0);
chain.read_rdi_FromVariable(0);
chain.add("pop rax", 0xfeeb);
chain.add("mov [rdi], rax");
chain.add("mov rax, rdi");
chain.add("jmp rax");
The list of open source software used in PS4 does not contain specialized sandbox software like Capsicum , so PS4 either uses “clean” jails from FreeBSD , or relies on its own proprietary system for isolating environments (which is unlikely).
Jail
We can prove the existence of active use of jail s from FreeBSD in the PS4 kernel using the auditon system call, which cannot be performed in an isolated jailed environment:
chain.syscall("auditon", 446, 0, 0, 0);
The first thing a system call
audition
does is check jailed
here , and if so, returns ENOSYS:if (jailed(td->td_ucred))
return (ENOSYS);
Otherwise, the system call will most likely return
EPERM
from mac_system_check_auditon
here :error = mac_system_check_auditon(td->td_ucred, uap->cmd);
if (error)
return (error);
Or from
priv_check
here :error = priv_check(td, PRIV_AUDIT_CONTROL);
if (error)
return (error);
The farthest where the system call can reach will be immediately after
priv_check
, here , before returning EINVAL
due to the argument length of 0:if ((uap->length <= 0) || (uap->length > sizeof(union auditon_udata)))
return (EINVAL);
Since
mac_system_check_auditon
they priv_check
never return ENOSYS
, getting checked jailed
is the only option when it returns ENOSYS
. When executing the chain, returns
ENOSYS
( 0x48
). This tells us that the sandbox system used by the PS4 is at least jail-based, as it uses validation
jailed
.FreeBSD 9.0 kernel exploits
It makes little sense to look for new vulnerabilities in the FreeBSD 9.0 kernel sources , since several kernel exploits have been found since the release in 2012 to which PS4 could be potentially vulnerable.
We can drop some of them right away:
FreeBSD 9.0-9.1 mmap / ptrace - Privilege Escalation Exploit - will not work, because we do not have access to the system call
ptrace
. FreeBSD 9.0 - Intel SYSRET Kernel Privilege Escalation Exploit - will not work because the PS4 uses an AMD processor.
FreeBSD Kernel - Multiple Vulnerabilities - perhaps the first vulnerability from this kit will work, but the other two rely on SCTP sockets, which are disabled in the PS4 kernel, as mentioned earlier.
Fortunately, there are a few smaller vulnerabilities that can lead us to something interesting.
getlogin
One vulnerability that is easy to try is the use of the getlogin system call to leak a small amount of kernel memory .
The getlogin system call is intended for copying the username of the current session into user memory, however, due to a bug, the buffer is always copied completely, and not just the size of the name string. This means that we can read some uninitialized data from the kernel, which may come in handy.
Note that the system call (49) is actually
int getlogin_r(char *name, int len);
, not char *getlogin(void);
. So, let's try to copy some kernel memory into the unused portion of user memory:
chain.syscall("getlogin", 49, chain.data, 17);
Alas, we can’t pull out more than 17 bytes, because:
Username length is limited by MAXLOGNAME (from <sys / param.h>) characters, currently it is 17 characters, including empty ones.- FreeBSD Man Pages
After completing the chain, the return value is 0, which implies that the system call has completed! Great start. Now take a look at the memory we pointed to:
Before executing the chain:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00
After completing the chain:
726f6f7400 fe ff ff 08626182 ff ff ff ff
00
After decoding the first four bytes in ASCII:
root
It turns out that the browser runs from the root! This is a surprise.
But what’s even more interesting is that the leaked memory resembles a pointer to something in the kernel, which remains the same with each run of the chain; this evidence confirms Yifanlu's theory that the PS4 does not have ASLR ( address space randomization ) protection at the kernel level!
Total
Judging by the information gathered, the PS4 kernel is very similar to the FreeBSD 9.0 stock kernel. It is important to note that the changes found relate more to changes to the standard kernel configuration than to code modifications. Although Sony added some of its own special system calls to the kernel, the rest of the kernel seems to have remained almost untouched.
For these reasons, I am inclined to believe that PS4 has the same “juicy” vulnerabilities as in the FreeBSD 9.0 kernel!
Unfortunately, most kernel exploits cannot be run from the WebKit entry point due to sandbox restrictions (which are most likely controlled by the standard FreeBSD jails mechanism). Alas, there is no reason to hope for the publication of private exploits for FreeBSD 9, so until suddenly a new one comes out, we are forced to work with what we have. I assume that it is possible to exploit the PS4 kernel using some of the existing memory corruption error vulnerabilities, but it will definitely be difficult.
The best approach here is reverse engineering all the modules that you can dump, in order to document the maximum possible number of special system calls from Sony; intuition tells me that with them the chance to achieve good luck will be higher than with standard FreeBSD system calls. Jaicrab
recently discovered two UART ports on the PS4 , which tells us about the potential interest of hardware hackers in the console. Although the role of hardware hackers was usually in dumping the RAM system (as it was with DSi ), this time we managed to do this ourselves thanks to the WebKit exploit - however, there is the possibility of detecting a kernel vulnerability that will be "turned on" by the hardware, like this was with the original hypervisor hack in PS3 by geohot. However, this does not negate the fact that the PS4 kernel exploit will most likely be done due to a system call vulnerability.
Thanks for reading!
If you notice a mistake, or want to offer clarification / correction, then do not hesitate to write to me in the LAN.