It's annual Wii U exploit time! 😄
Image of the Wii U connection test screen on the GamePad. |
After reverse engineering parts of the Wii Us' NET stack for another project I was working on, I realized it's using a modified version of NicheStack.
NicheStack is a TCP/IP stack developed by InterNiche Technologies and is designed for use in embedded systems.
INFRA:HALT
If you end up searching for NicheStack on the internet, one of the first things you'll find is a security research report called INFRA:HALT, published by Forescout Research Labs and
JFrog Security Research. This report contains a set of 14 vulnerabilities, which all affect various parts of NicheStack.
While most of these vulnerabilities "only" lead to a DoS, two vulnerabilities might lead to remote code execution. One of them affects the HTTP server, which is not used on the Wii U. The other one sounds a lot more interesting though...
CVE-2020-25928
CVE-2020-25928 is a vulnerability in the DNS client, which is also in use on the Wii U. It has a CVSS v3.1 Score of 9.8 and results in a heap buffer overflow which can lead to remote code execution.
The code described by the INFRA:HALT write-up looks something like this:
As we can see dnc_set_answer is called for each record with type 0xC (PTR).
The dnc_set_answer implementation does something like this:
As we can see the DNS client basically copies the record data into a fixed size buffer on the heap without checking the size.
At this point I was interested and decided to take a look if the Wii U implementation suffers from the same issue. To my surprise the Wii U implementation looks something like this instead:
The affected dnc_set_answer call is explicitly not called for the type 0xC (PTR) records. Instead a function called dnc_copyin is called, which does proper bounds checks.
So someone (at Nintendo?) fixed this several years before INFRA:HALT was discovered and disclosed? Well, they tried!
As we can see from the full code above there are two places where dnc_set_answer is called. The first one for answer records and the second one for additional records pointing to the first answer. But they only added the check for the first one?!
The second one still blindly calls dnc_set_answer without doing any size checks.
Exploiting it on the Wii U
After writing a quick PoC server to confirm this actually works on a Wii U, I decided to try and exploit this.
Since the dns_querys struct is stored on the heap, this is a basic heap overflow. The Wii U added a few additional fields to the dns_querys struct, which we can overwrite. Due to the NET stack running on the ARM co-processor, the Wii U later copies this entire struct back to the PPC side using an address stored in a reply struct. Unfortunately to overwrite the pointer to this reply struct, we would need to spoof a reply struct and point to it. Since the heap layout is determined by various network setup specific factors we have no idea where in the heap this dns_querys struct is allocated. So we need a way to store a buffer at a location in memory which we know the address of.
Introducing DNS over TCP
This doesn't seem to be part of NicheStack, but the Wii U features support for DNS over TCP instead of UDP. After two attempts of using UDP, the Wii U will switch to TCP if the truncated flag was set in the DNS header. For TCP support more fields were added to the dns_querys struct, one of them is a pointer to a buffer in which the received TCP data is stored.
So just overwrite this with an address in the stack and we can receive data into the stack? Unfortunately, no. Before storing data into this buffer it is resized using IOS_HeapRealloc (basically realloc), if it isn't NULL. If the heap block is already the same size as the reallocated size, the same pointer is returned though. This allows us to take over already allocated blocks inside of the heap but limits us to stay in bounds of the heap. If we point the receive buffer to one of the already allocated packet buffers at the start of the heap, which will always have the same address, we can receive data into a known memory location (limited by the MTU and TCP/IP fragmentation).
Remember the reply struct which is pointed to by the dns_querys struct? We now have a known memory location at which we can create a fake reply struct which points the reply buffer to anywhere in IOS-NET memory. This causes the entire dns_querys struct to be copied to anywhere we want and we can control approximately 256 bytes in this struct!
Memory layout showing dns_querys copying. |
Creating a ROP Chain
Since the Wii U features no-execute on the ARM side, we need to write a ROP chain which performs a kernel exploit. Doing this within 256 bytes is tricky, but it's enough to open up a TCP socket, connecting to a server and receiving data from it into memory. We're abusing some buffering of the TCP stack implementation here, but it works!
We can now perform a stack pivot into the next ROP chain.
I wish more IOS modules had a gadget to load the stack pointer from the stack! |
In this ROP chain we perform a kernel exploit as described in the previous write-ups, and copy the kernel binary from the buffer we received over the socket in the previous ROP chain.
We now have kernel code execution!
Presenting: DNSpresso
Since everything on the Wii U needs to be coffee related this implementation is called DNSpresso :p
You can find the GitHub repository with usage instructions here.