Nintendo Switch nvservices Info Leak
In this post I’m going to discuss a Nintendo Switch bug I submitted to the Nintendo bug bounty program a few months ago, which they fixed recently (in 6.0, I believe, though I haven’t tested this myself).
The Switch runs on a custom OS called Horizon. It’s a really sleek, simple microkernel, and because of that, the majority of key functionality that would normally be in the kernel is actually in a userland service. To communicate between services or from an app/game to services, you use IPC: Get a handle to a service (by an <=8 character name, e.g. ‘ssl’), then send messages to it. Each message consists of some amount of data and some number of objects, which are typically kernel objects. Kernel objects are things like transfer memory, shared memory, event handles, etc. The details here aren’t important, with one exception: transfer memory.
Transfer memory is a special type of shared memory, where a process allocates a block of RAM and then sends that over to another process for use. This can either be shared (the original process can still read/write to it) or pure transfer (the original process can’t see or touch that block of memory in any way).
Last bit of context: sysmodules (system modules, which host the IPC services) have only a static block of memory to use. They don’t have a heap and can’t allocate memory outside of what they request on startup (the bss segment of the binary).
The Setup #
nvservices sysmodule for the Switch is, in essence, the Linux NVidia driver ported to Horizon, running in userland and modified so that it talks IPC rather than using the typical ioctl interface it’d use on Linux. It’s a huge hack, but overall works well. However, this driver allocates a bunch of objects for every connection, and for every device you open within that connection session.
Since sysmodules can’t really dynamically allocate RAM, they use transfer memory to get around this limitation. When you start talking to nvservices, you hand it a transfer memory that only it can read. It allocates all its objects within that transfer memory.
The Bug #
The transfer memory that you allocate must be non-readable by the original process, so everything inside it surely must be safe, right? Well …
Transfer memory dump from my Switch
They didn’t consider that once you disconnect from the session (thus causing nvservices to release its transfer memory handle), you can just release your own transfer memory handle, returning that RAM back to your process. Untouched.
Within this memory, you have all kinds of pointers to various objects. Let’s talk about two.
Object #1: Breaking ASLR #
Device channel objects (e.g. a handle to
/dev/nvhost-gpu) contain a pointer to a vtable, which is stored in .rodata in the binary. So breaking ASLR is trivial:
Allocate a 1MB transfer memory (this is important for the offsets I’m providing here; each different size puts things in different places, due to their allocator. Otherwise, the concept is identical) and then use it to initialize a connection to
nvdrv. Open an instance of
/dev/nvhost-gpu on that connection. Then disconnect and close your transfer memory handle. Read the address from offset +0xc000 in the transfer memory: congrats, you have a pointer to a vtable. Simply subtract the base of the vtable (0x61f910 on Switch OS 2.3, for instance) and you’ve got the base address of
Object 2: Finding your own transfer memory #
Because your transfer memory ends up mapped to a different place in the address space every time, you’d think it’s impossible to know where you’re allocated. Nope.
Create two 1MB transfer memories, then two connections to
nvdrv using one transfer memory each. Then close the second connection you made, and close its respective transfer memory handle. Read the address from +0x8008, subtract 0x8000, and you have the address where your first transfer memory block is mapped into
This is critical if you need to be able to reference a known block of data somewhere in the process. A typical exploitation process is something like: allocate one transfer memory with a bunch of data you want to reference (e.g. a ROP payload and/or vtable that points into the image base you got from the ASLR break), then one to leak the address of the first, then a third that is set up to trigger a bug using the address of the first.
This isn’t the simplest bug, it’s not the most critical bug ever found (or even the most critical on the Switch), but I’m really proud of it. I had a lot of fun with this one and I’m glad to be able to talk publicly about it! You can see a little test script that works for Switch OS 2.3; other versions will almost certainly require different offsets.
- Cody Brocious (Daeken)