Vita sceNetIoctl use-after-free
This describes the first public PS Vita kernel exploit. The vulnerability is a use-after-free in the SceNetPs module, combined with an infoleak to defeat KASLR this allows for arbitrary code to be executed in kernel mode. All Vita devices and firmwares before 3.61 are affected.
The bug’s present in sceNetIoctl
function. Let’s go.
Implementation
Let’s take a look at how sceNetIoctl(s, flags, umem)
works. I’ll be providing links to ioctl
from NetBSD because it looks very similar; and there also will be some pseudo-code which does not match Vita version exactly, but is close enough.
The arguments are:
s
: socket id/descriptorflags
: ioctl numberumem
: pointer to user memory
First it checks that the kernel heap has enough memory (at least 5 free blocks of size 0x800 bytes or more) and then calls another function which is the real meat of the syscall.
if ( check_heap_space_available(0x800, 5) )
res = ioctl(s, flags, umem);
else
res = -55;
A few words about how SceNetPs works. One of the first things most network-related syscalls do is lock a global SceNetPs mutex (which I am going to call g_network_mutex
). Indeed, instead of fine-grained locking it just has a global mutex, which is probably terrible for performance, but I digress.
So the next step of our ioctl
implementation is locking g_network_mutex
. After that, a pointer to the socket is retrieved by its descriptor from the global socket table.
sce_psnet_bnet_mutex_lock(&g_network_mutex, 0);
socket = get_socket_by_id(s, &ret, 0);
Next, a buffer for copying from/to user is allocated. The logic is same as in NetBSD. Notice the KM_SLEEP
flag here: it means that alloc will never return NULL
, it guarantees success by waiting/rescheduling the thread until more kernel memory becomes available.
memsz = (flags >> 16) & 0x1FFF;
if ( memsz > 0x80 ) {
heapmem = malloc(memsz, 0, 8); // 0 here is equivalent to KM_SLEEP
mem = heapmem;
} else {
heapmem = 0;
mem = &stackbuf;
}
Following that, depending on flags
, either user data is copied in or the buffer is zeroed.
if ( (flags & 0x80000000) && memsz )
ret = u2k_copy(umem, mem, memsz); // args = src, dst, size
else if ( (flags & 0x40000000) && memsz )
memset(mem, 0, memsz);
else if ( flags & 0x20000000 )
*(void **)mem = umem;
Then, a function with ID id = (flags & 0xFF00) >> 8
is executed. There are two hardcoded IDs and if there’s no match, a generic function from socket vtable is executed, in pseudocode with missing casts:
vptr = *(uint32_t*)(socket + 24);
fptr = *(uint32_t*)(vptr + 28);
err = fptr(socket, 11, flags, kmem);
|
+--------------------------------------------+
|
// Spoiler Alert: we're going to gain control over this pointer
We’re at the end now, time to free allocated heap memory, unlock the mutex and return the result error code to the caller:
if ( !err && (flags & 0x40000000) && memsz)
err = k2u_copy(mem, umem, memsz); // args = src, dst, size
free(heapmem);
sce_psnet_bnet_mutex_unlock(&g_network_mutex);
return err;
Now that we’re done with the boring description of how SceNetPs ioctls work, let’s get to the fun part.
The bug
There are multiple problems in this code that eventually lead to code execution:
- no reference counting! what if our poor socket gets freed during the operation of ioctl? This is the real bug which results in UaF, however, it helps that:
- the heap free check is insufficient: it checks for five blocks sized >=0x800 but we can cause an allocation of 0x1FFF bytes at most
- when malloc function is unable to fulfill a request, instead of returning NULL, it will wait for more memory to become available (due to Vita analogue of the
KM_SLEEP
param passed to it)
Of course, before malloc goes to sleep, it also unlocks the g_network_mutex
.
N.B. the bug is strictly Vita-only, other implementations (NetBSD, OpenBSD) do proper reference counting.
Exploit
So here’s our exploitation strategy:
- thread #1: Allocate a socket
- thread #1: Fill up
SceNetPs
heap memory - thread #1: Call an
ioctl
on the socket. Now this thread will wait for heap memory to become available - thread #2: Free the socket
- thread #2: Overwrite freed socket memory with controlled data
- thread #2: Free
SceNetPs
heap memory so that the first thread wakes up. - thread #1: Code exec!
allocate a socket
This is harder than it sounds. The reason is that SceNetPs uses a memory pool, a single-linked list of free blocks of the same size, to allocate sockets. However:
- When the pool is full, instead of adding a freed socket’s memory to the pool, it calls
free()
on it. - When the pool is empty, instead of allocating new socket from the pool, it calls
malloc()
.
Since we want to overwrite socket structure with controlled memory (i.e. with not-a-socket), we need to maintain these condition:
- before allocating our socket, empty the pool by allocating a lot of sockets
- before freeing the socket, fill up the pool by freeing a lot of sockets
fill up heap memory
The best primitive I was able to find that can be used to fill up the heap is a dump
. For our exploit its real purpose doesn’t matter. The int sceNetDumpCreate(char *name, int len, int flags)
function eventually leads to a dump = malloc(len + 0x40)
which is perfect for filling up the heap.
It should be noted that we also need to make at least 5 holes sized 0x800, in order for the heap free check to pass. This is achieved in the HENkaku exploit by allocating 10 dumps sized 0xF00 and then freeing even ones (0th, 2nd, 4th, 6th, …).
call an ioctl
Self-descriptive. Just call sceNetIoctl
with proper arguments, making sure the ioctl number is invalid so that instead of a generic function, the fptr
gets called, and memsz
is 0x1FFF, the largest allowed value.
Since we’ve ensured there’s not enough heap memory to allocate a single block of 0x1FFF bytes, this thread is now sleeping and waiting for more kernel heap memory to become available.
free the socket
Now that the first thread has fallen asleep, unlocking g_network_mutex
in the process, the second thread can free the socket the ioctl is operating on right now.
overwrite memory with controlled data
For that, I’ve used sceNetControl(int a1, int a2, void *ubuf, int argsize)
. When passed proper flags, it will allocate a buffer = malloc(argsize)
and then copy in user data. At the end of the function, the buffer
is freed but it doesn’t really matter because free()
doesn’t destroy our data.
With an argsize
that matches the socket object size and proper kernel heap spraying, the allocated buffer will have the same address as the freed socket.
free heap memory
Just free some of the dumps allocated in the second step. When there’s a contiguous block of 0x1FFF free bytes available, the first thread will wake up and give us …
code exec
Since we control the whole socket struct, we can set vptr
to whatever value we’d like, and as such gain control over fptr
as well. However! There are some exploit mitigations that we still have to bypass.
Bypassing the mitigations
KASLR & SMEP/SMAP-like
Vita’s architecture is ARM which doesn’t have anything named SMEP or SMAP, however these terms are widely used and understood. (On Vita such functionality is implemented using the Domain Access Control Register).
Basically:
- the kernel cannot execute user executable code
- the kernel cannot read or write user memory (outside of utility functions specifically made for that purpose)
As a result, we cannot just point vptr
into user memory, or fptr
into user code.
We bypass both KASLR & DACR protections with one kernel stack disclosure in the sceIoDevctl
system call:
- to bypass KASLR, leak a
SceSysmem
address - to bypass DACR, leak kernel thread stack base. Then plant our data into the kernel stack
The same sceIoDevctl
system call with different arguments is used for planting data into the kernel stack.
XN/NX
Now that we have our data in the kernel space, to bypass the execute never bit (only executable memory can be executed), build a ROP chain in kernel space and pivot to it.
Since we’ve only leaked SceSysmem base, we can only use gadgets from that kernel module, but that’s more than enough.
That’s it
You can find source code for the exploit here. Bring your own kernel ROP chain.