Hacking Hundreds of Wii Us at Once

2024-05-26 by Yannik Marchand

There used to be a flaw that could be used to gain code execution on hundreds of consoles at once. Almost all 3DS, Wii U and Switch games with online features depend on a single library for online play: NEX. This library happened to be vulnerable to a stack overflow.

Summary

Background

The NEX library is used in almost all 3DS, Wii U and Switch games that provide online features. It implements a client for various online services, such as leaderboards and match making.

Whenever a console establishes a connection with a game server, it is assigned a unique connection id. This id is used during match making to establish a peer-to-peer connection between consoles. An important step of establishing a peer-to-peer connection is NAT traversal. This is necessary because most consoles do not a have a public IP address. Instead, they are part of a local area network and can only access the internet through a router.

For NAT traversal, NEX uses a technique called UDP hole punching. This is a bit complicated, but the first few steps of UDP hole punching are implemented as follows:

  1. Let's say that Alice wants to establish a peer-to-peer connection with Bob after obtaining his connection id elsewhere.
  2. Alice sends a 'probe request' to the game server. In the request, Alice includes her own IP address and port, and the connection id of Bob.
  3. The server forwards the probe request to Bob.
  4. Bob parses the IP address and port of Alice in order to send a UDP packet directly to Alice.

In many cases, a console wants to establish a peer-to-peer connection with multiple different consoles at once. To optimize this process, the server accepts a list of connection ids in the probe initiation request, rather than a single connection id. The server forwards the probe request to every console in the list.

Vulnerability

There is a serious flaw in the code that parses the IP address. When Bob receives the IP address of Alice from the game server, his console converts it to a fixed-size buffer on the stack:

bool InetAddress::SetAddress(const qChar *ip) {
    /* ... */

    char buffer[256];
    StringConversion::T2Char8(ip, buffer, sizeof(buffer));

    uint32_t address = String2Address(buffer);

    /* ... */
}

The conversion is necessary because of an implementation detail. NEX uses qChar in many places, which is either a char or wchar_t depending on the platform. However, String2Address requires a char pointer regardless of the platform.

Let's see how the string conversion is implemented if qChar is defined as char, which is the case on the 3DS and Switch:

void StringConversion::T2Char8(const qChar *src, char *dest, size_t destLength) {
    strcpy(dest, src);
}

The destLength argument is completely ignored. Oops!

Now, let's see how the string conversion is implemented if qChar is defined as wchar_t, which is the case on the Wii U:

void StringConversion::T2Char8(const qChar *src, char *dest, size_t destLength) {
    UnicodeToUtf8(src, dest, destLength);
}

size_t UnicodeToUtf8(const wchar_t *src, char *dest, size_t destLength) {
    char *base = dest;
    char *limit = dest + destLength - 1;
    while (dest != limit) {
        wchar_t c = *src++;
        if (!c) break;
        else if (c < 0x80) {
            *dest++ = c;
        }
        else if (c < 0x800) {
            *dest++ = (c >> 6) | 0xC0;
            *dest++ = (c & 0x3F) | 0x80;
        }
        else {
            *dest++ = (c >> 12) | 0xE0;
            *dest++ = ((c >> 6) & 0x3F) | 0x80;
            *dest++ = (c & 0x3F) | 0x80;
        }
    }
    *dest = 0;
    return dest - base;
}

At first glance, this looks secure. The destLength argument is used as a limit, and when it is reached the remaining source characters are ignored. If the input buffer only contains ASCII characters, this actually works fine. However, there is an important oversight: when the source buffer contains a non-ASCII character, the destination pointer is incremented by two or three bytes in a single iteration. Since the bounds check is based on equality, it can be bypassed by inserting a non-ASCII character right before the limit is reached. This means that the bounds check can easily be defeated.

Exploit

By now, we are able to trigger a stack overflow. How can we turn this into remote code execution? On the Nintendo Switch, this is difficult because of ASLR. We can easily crash the game on the victim console, which is already a serious flaw. However, to gain remote code execution on the Switch, we would have to combine the stack overflow with a different vulnerability that lets us bypass ASLR.

On the 3DS and Wii U, this is much easier. The 3DS and Wii U have neither stack canaries nor ASLR. Although stack memory isn't executable, it is relatively easy to write a ROP chain. The biggest obstacle is that our character set is limited: we cannot use null bytes in our ROP chain, and we face various constraints if we want to use bytes above 0x80 on the Wii U. This makes the exploit a bit more difficult, but since all addresses are predictable and a large number of ROP gadgets are available, there is almost certainly a way around this.

The architecture behind the NAT traversal implementation allows us to send our exploit to hundreds of consoles at once. Although the connection id of another player is normally received during match making, it can be predicted by an attacker because it is a sequential id. In order to gain arbitrary code execution on many consoles at once, we can use the following approach:

  1. Build a ROP chain that does something interesting. Because there is no ASLR, the same ROP chain will work on all consoles.
  2. Establish a connection with the game server and write down our connection id.
  3. Call the 'request probe initiation' function on the game server. In the request:
    • We include a few hundred connection ids below our own connection id. Since connection ids are sequential, most of these will belong to active players.
    • Instead of including our own IP address, we send a large string to the server that includes our ROP chain.
  4. The server forwards our ROP chain to all consoles that have one of the connection ids that we sent to the server in step 3.
  5. We have successfully gained control over a large number of consoles at once.

Impact

The impact of this is severe. On the Switch, an attacker could easily cause a crash on all people that were playing a certain game online. On the 3DS and Wii U, an attacker could even gain arbitrary code execution on all people that were playing a certain game online. Arbitrary code execution allows an attacker do almost anything, from stealing credentials to wiping save data. Because almost all games with online features use NEX, almost all of them were vulnerable. This was a serious issue.

Mitigation

Although the fix seems easy ('just check the bounds properly'), this is hard to do in practice. Because the NEX library is statically linked into games, all games with online features would have to be patched. An update would have to be released for all these games. Players would have to be forced to update their game before being allowed to play online again.

Instead, Nintendo decided to patch this vulnerability on the server side. When the server receives a probe request, it verifies the size of the IP address string. If it is too large, the server simply ignores the request.