Switch System Flaws
Exploits are used to execute unofficial code (homebrew) on the Nintendo Switch. This page is a list of publicly known Switch system flaws.
For userland applications/applets flaws see here.
System flaws
Hardware
Flaws in this category pertain to the underlying hardware that powers the Switch.
This includes components shared across Tegra based devices such as the TSEC, the Security Engine, the GPU and so on.
Summary | Description | Fixed with hardware model/revision | Newest hardware model/revision this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|
CVE-2018-6242 (leveraged by the ShofEL2 and Fusée Gelée exploits) | The USB software stack provided inside the boot instruction rom (IROM/bootROM) contains a copy operation whose length can be controlled by an attacker. By carefully constructing a USB control request, an attacker can leverage this vulnerability to copy the contents of an attacker-controlled buffer over the active execution stack, gaining control of the Boot and Power Management processor (BPMP) before any lock-outs or privilege reductions occur. This execution can then be used to exfiltrate secrets and to load arbitrary code onto the main CPU Complex (CCPLEX) "application processors" at the highest possible level of privilege (typically as the TrustZone Secure Monitor at PL3/EL3). | HAC-001-01 (Mariko/Tegra214/Tegra210b01) (also fixed independently on Tegra186). | HAC-001 (Tegra210) | January 2018 | April 23, 2018 | shuffle2 and fail0verflow (originally), ktemkin and ReSwitched Team (independently), naehrwert (independently), hexkyz (independently), st4rk with Shiny Quagsire and Dazzozo (independently), and many others (independently). |
GMMU DMA attack | The Switch's GPU includes a separate MMU (GMMU) that is allowed to bypass the system's IOMMU (SMMU). By accessing the GPU's MMIO region and manipulating the page table entries in the GMMU, an attacker can read/write any portion of the DRAM (except memory carveouts).
[5.0.0+] Works around this hardware flaw by using memory pool partitioning. You can no longer escalate into sysmodules with GPU DMA because all their memory is allocated using heap that's carved out. HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by adding a new register which restricts what memory untranslated DMA requests may access. Untranslated GPU DMA may now only access the GPU carveout (physmem 0x80002000-0x80006000), which the GPU already has legitimate and exclusive access to. |
HAC-001-01 (Mariko/Tegra214/Tegra210b01) | HAC-001 (Tegra210) | Summer 2017 | December 28, 2017 | hexkyz, SciresM and qlutoo |
Weak Security Engine context validation | The Tegra X1 supports a "deep sleep" feature, where everything but DRAM and the PMC registers lose their content (and the SoC loses power). Upon awaking, the bootrom re-executes, restoring system state. Among these stored states is the Security Engine's saved state, which uses AES-128-CBC with a random key and all-zeroes IV. However, the bootrom doesn't perform a MAC on this data, and only validates the last block. This allows one to control most of security engine's state upon wakeup, if one has a way to modify the encrypted state buffer.
With a way to modify the encrypted state buffer, one can thus dump keys from "write-only" keyslots, etc. This also bypasses the SBK protection of the bootROM: indeed, at warmboot, bootROM will always clear keyslot 0xE to prevent malicious code from saving the SBK. Moving the SBK to another keyslot in the saved context renders this protection moot. HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by streamlining the context save process; security engine contexts are now saved to protected memory which the CPU cannot access or modify. |
HAC-001-01 (Mariko/Tegra214/Tegra210b01) | HAC-001 (Tegra210) | December 2017 | January 20, 2018 | SciresM and motezazer |
Security Engine keyslots vulnerable to partial overwrite attack |
The Tegra X1 security engine supports writing keyslot data to the engine with syntax as follows: SECURITY_ENGINE->AES_KEYTABLE_ADDR = (keyslot << 4) | (dword_index_in_keyslot); SECURITY_ENGINE->AES_KEYTABLE_DATA = readle32(key, dword_index_in_keyslot * 4); However, the Security Engine flushes writes to the internal key tables immediately when AES_KEYTABLE_DATA is written -- this allows one to overwrite a single dword of a key at a time, and thus brute force the contents of keyslots in time (2^32 * 8) = 2^35 instead of 2^256. |
None | HAC-001 (Tegra210) | Theorized Summer 2017 due to suggestive syntax, confirmed April 9, 2018 | April 9, 2018 | SciresM, almost surely others (independently). |
Poor validation of bootrom SDRAM configuration parameters leads to arbitrary writes in bootrom |
The Tegra X1 bootrom supports saving SDRAM parameters to scratch registers, and using the saved configuration to enable DRAM during warmboot. The code that parses these parameters does if (params->EmcBctSpareN) *params->EmcBctSpareN = params->EmcBctSpareNPlusOne for most N, without validating either the address or value written to it. There are other arbitrary writes in this code, as well (e.g. BootromPatch parameters intended for patching MISC registers do not check a relative offset to 0x7000000, etc). This allows a user with access to the PMC registers (via pre-sleep bpmp execution, or otherwise) to gain arbitrary bootrom code execution. HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by validating that the spare writes/bootrom patch before performing them. |
HAC-001-01 (Mariko/Tegra214/Tegra210b01) | HAC-001 (Tegra210) | 2017 | December 16, 2018 | Everyone (independently). |
TSEC ROM does not clear crypto registers after signature verification |
TSEC supports executing signed-microcode at a greater privilege level than normal payloads. When jumping to signed microcode, the caller is expected to load hardware crypto register $c6 = <signature>, $c7 = <seed (zero for all officially-signed microcode)>. TSEC ROM then calculates the expected signature and compares it to the user-supplied one in $c6. On match, the secure payload is executed, and on failure an exception is raised. However, TSEC ROM fails to clear the crypto registers used to calculate the expected signature in either of the success/failure cases. Thus, with some way of obtaining the contents of crypto registers (e.g. ROP under some secure payload), an attacker can dump intermediary values from signature calculation. With enough data/trial/error, this is enough to reconstruct the signature algorithm:
|
None | HAC-001 (Tegra210) | Late 2018/Early 2019 | August 2020 | qlutoo/hexkyz/shuffle2, SciresM/motezazer (independently). |
TSEC signature validation design flaw leads to fake-signing |
As mentioned above, when jumping to signed microcode the caller is expected to load hardware crypto register $c6 = <signature>, $c7 = <seed (zero for all officially-signed microcode)>. However, TSEC ROM performs no validation on the input seed used to generate the signing key. This leads to the following attack:
Thus an attacker who has exploited *any* secure payload may use this to obtain a "fake signature key", which can be used to sign and execute arbitrary microcode in secure mode. Note: this does not break the TSEC cryptosystem, as the csigenc mechanism relies on the signature of the executing microcode, and fakesigning produces different signatures from NVidia that cannot be controlled. |
None | HAC-001 (Tegra210) | Late 2018/Early 2019 | August 2020 | qlutoo/hexkyz/shuffle2, SciresM/motezazer (independently). |
ROP under TSEC secure bootrom via DMA engine stack overwrite (--xploit) | TSEC DMA engine does not stop when entering TSEC secure bootrom. By pointing TSEC DMA to current stack before secure bootrom entry, stack can be controlled.
One can then use blind ROP against the TSEC secure bootrom (which is execute only, and cannot be dumped). With sufficient effort, an attacker can construct a ROP chain that leads to csigcmp being executed with fully controlled arguments. This allows for arbitrary heavy secure mode code execution with the current signature set to an arbitrary value. This completely breaks the TSEC cryptosystem, by allowing one to obtain the result of csigenc with signature = <any desired value>. This has many uses/results, notably including dumping the "true" signature key (set signature = zeroes, perform csigenc using csecret 0x1). |
None | TSEC for all Tegra devices | Late 2018 | Jan 2021 | hexkyz/SciresM, Vale/Thog (independently), Tatsuko (independently), possibly others (independently). |
Boot straps are not relatched on watchdog resets (strapwn) | On boot, the BOOTSELECT, RCM and RAM_CODE straps are latched from external GPIO to determine which boot medium to use and verify from in bootrom. However, APB_MISC_PP_STRAPPING_OPT_A can be overwritten with arbitrary values following bootrom. Write access to PP_STRAPPING_OPT_A would otherwise be mundane, however these straps are not relatched during a watchdog reset (despite being latched during other software resets), allowing for arbitrary straps to be selected and executed in bootrom.
This allows setting NVPROD_UART on some hardware configurations where it would normally be unavailable (ie on Jetson Nano boards), but is otherwise mostly useless and/or useful for testing unintended boot options (such as USB Mass Storage boot) without having to move boot strap resistors. |
Unknown | HAC-001 (Tegra210) | May 2020 | April 30, 2021 | Shiny Quagsire |
Firmware
Flaws in this category pertain to the firmware running on hardware devices, such as wifi/bluetooth, etc. Firmware is generally uploaded by sysmodules.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Broadpwn (CVE-2017-9417) | See here and here. | Code execution on the wifi controller (untested on Switch). | 4.0.0 | 4.0.0 | Switch: July 2022 | Switch: July 30, 2022 | Switch: yellows8 |
Software
Bootloader
Flaws in this category pertain to any bootloader component such as the package1ldr, the NX bootloader or the warmboot binary.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Null-dereference in panic() | The Switch's stage 1 bootloader, on panic(), clears the stack and then attempts to clear the Security Engine. However, it does so by dereferencing a pointer to the SE in .bss (initially NULL), and this pointer doesn't get initialized until partway into the bootloader's main() after several functions that might panic() are called. Thus, a panic() caused prior to SE initialization would result in the SE pointer still being NULL when dereferenced.
The BPMP doesn't have an active MPU and the bus won't data abort on an invalid address, so no exception will be entered: it'll end up overwriting some exception vectors with NULL before halting. In 3.0.0, this was fixed by moving the security engine initialization earlier in main(), before the first function that could potentially panic(). |
Some exception vectors overwritten with NULL, before SBK/other keyslots are cleared. Probably useless for anything more interesting. | 3.0.0 | 3.0.0 | Early July, 2017 | July 30, 2017 | Everyone who diff'd 2.3.0 and 3.0.0 Package1 |
FUSE_DIS_PGM not written by package1 | The switch's hardware fuse driver contains a write-once bit in a register called "FUSE_DIS_PGM", which disables burning fuses until the next reboot. While Nintendo's bootloader code for waking up from sleep writes this on all firmware, the actual package1 initial bootloader forgets to write to it on cold reboot.
This isn't too big of a problem because another fuse is burnt on retail devices (production mode), which prevents burning *all* fuses other than ODM_RESERVED ones in hardware. This was fixed in 3.0.0 by writing to the register on cold boot (although the write happens in TZ instead of package1 where it should take place, possibly to obfuscate the fact that they made this mistake). |
Burning arbitrary ODM reserved fuses with TZ code execution, which should never be possible for non-bootloader code.
Warning: one could irreparably brick one's console by playing with this. |
3.0.0 | 3.0.0 | Late summer/early fall 2017 | December 31, 2017 | SciresM, motezazer |
maconstack (TSEC firmware leaves MAC on the stack) | Package1ldr loads a firmware blob into TSEC early on boot. This piece of code runs on the TSEC in Authenticated Mode and has the sole purpose of generating the per-console TSEC key (see Cryptosystem).
As a way to mitigate attacks, the TSEC firmware blob is split into 3 stages: Boot which is unencrypted and unsigned, KeygenLdr which is unencrypted but signed and Keygen which is encrypted and signed. Boot loads a static pre-generated signature into the Falcon's CPU crypto registers, loads KeygenLdr into the Falcon's CODE region and jumps to it. Execution will proceed into KeygenLdr in Heavy Secure Mode if, and only if, the loaded signature matches the one Falcon calculates internally for KeygenLdr. Among various things, KeygenLdr will attempt to do a "backwards" security check by calculating a CMAC over Boot and comparing it with a known hash stored in the TSEC firmware's key data (a small buffer stored after Boot's code). If the hashes don't match, execution aborts. KeygenLdr stores the calculated Boot's CMAC in the stack, but forgets to clear it. Since the stack is located in Falcon's DATA region, loading the TSEC firmware blob and dumping the DATA region afterwards (via MMIO) will reveal the calculated hash. This allows using KeygenLdr as an oracle to generate a valid CMAC for arbitrary Boot code. Replacing the CMAC in the TSEC firmware's key data region results in KeygenLdr accepting any Boot code, thus rendering this security measure useless. Additionally, since signed Falcon code can't be revoked without an hardware revision, an attacker can always reuse the flawed KeygenLdr code even if a fix is issued. |
Running TSEC firmware's KeygenLdr in a user controlled environment. | None | 5.0.2 | January 2018 | April 29, 2018 | hexkyz, Reisyukaku (independently), probably others (independently). |
Stack smash in TSEC firmware's KeygenLdr | Given that we can control the key data (which is not authenticated) and the Boot blob (see "maconstack"), as well as the fact Non-secure and Heavy Secure code share the same stack, we can use this to attack KeygenLdr. KeygenLdr uses memcpy to copy over a payload to DMEM to verify it, which can be abused to smash the stack (in DMEM) and write over the return address of said function. | ROP under KeygenLdr in Heavy Secure mode. | None | 8.0.1 | Early 2018 | May 21, 2019 | Everyone (independently). |
pk1ldrhax | Package1ldr decrypts and verifies the keyblob inside of the current BCT in order to get the package1 key, and then uses the package1 key to decrypt package1. It then validates package1 before jumping to it by checking the PK11 magic number, and that the section sizes sum to the expected size (and are individually less than the expected size).
However, package1ldr does not actually validate the package1 key against a fixed vector (much like kernel9loader forgot to do so on the 3ds). This would normally not matter, as keyblobs are validated -- however, with bootrom code execution one can dump SBK and forge keyblobs, and thus control the package1 key. Thus (in theory, but not in practice due to the size of the brute force required) one can replace the package1 key with garbage, causing package1 to decrypt into garbage, and hope that this garbage passes validation checks and that package1ldr jumping into the garbage will do something useful. This was fixed incidentally in 6.2.0, as pk1ldr does not use keyblob data to decrypt package1 any more. |
With a large enough brute force: arbitrary package1 code execution from coldboot.
However, a usable brute force is on the order of >= ~2^80, so this is almost certainly not actually usable in any meaningful context. |
6.2.0 | 6.2.0 | Early 2017 (as soon as plaintext package1ldr was first dumped) | November 20, 2018 | Everyone |
TrustZone
Flaws in this category pertain exclusively to the Secure Monitor.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Non-atomic mutexes | When an SMC is called, TrustZone sets a global variable to mark that an SMC is in progress, so that two SMCs using shared resources (like the security engine) do not trample on one another. On 1.0.0, this global variable was written using non-atomic writes, and thus a race condition is possible.
However, the SMC handler enforces that all SMCs must be called from core #3, unless the top-level handler ID is 1 (SMCs internal to the kernel). Thus, the only SMCs that can be run side-by-side are [any userland smc] and smcGetRandomBytesForKernel, and this turns out to not really be abusable. |
Mostly useless. Maybe some oob-write into unused (and thus useless) memory if running smcGetRandomBytesForKernel and smcGetRandomBytesForUser at the same time. | 2.0.0 | 2.0.0 | December 2017 (Probably earlier by others) | January 18, 2018 | SciresM, probably others (independently). |
jamais vu (non-secure world access to PMC MMIO and pre-deep sleep firmware) | On 1.0.0, one could map in the PMC registers in userland. In addition, am ran a little-kernel based firmware on the BPMP at runtime. With code execution under am, one could modify the BPMP's little-kernel firmware to hook deep sleep entry, and modify TrustZone/Security engine state.
This was fixed in 2.0.0 by making the PMC secure-world only, blacklisting the BPMP's exception vectors from being mapped, and thoroughly checking for malicious behavior on deep sleep entry. |
Arbitrary TrustZone code execution. | 2.0.0 | 2.0.0 | December, 2017 | January 20, 2018 | SciresM and motezazer |
Missed BPMP Exception Vector Writes | Starting in 2.0.0, the BPMP is asleep at runtime, and is turned on by TrustZone during smcCpuSuspend in order to initiate the deep sleep process. When it does so, it is held in RESET, and TrustZone attempts to write to the BPMP exception vectors at 0x6000F200 to register EVP_RESET = lp0_entry_fw_crt0, and all other EVPs to a function that simply reboots. However, while they successfully write EVP_RESET, they miss all the other vectors, accidentally writing to the 0x6000F004-0x6000F020 region instead of the 0x6000F204-0x6000F220 region they want to write to. This results in all the exception vectors for the BPMP other than RESET being "undefined" (attacker controlled).
With some way of causing an exception vector to be taken at the right time, this would give pre-sleep code execution (and thus arbitrary TrustZone code execution, via the security engine flaw). However, none of the abort vectors are really triggerable, and interrupts are disabled for the BPMP when it is taken out of reset. Thus, this is useless in practice. This was fixed in 4.0.0 by writing to the correct registers. |
Theoretically: Arbitrary TrustZone code execution. In practice: Useless. | 4.0.0 | 4.0.0 | January, 2018 | February 23, 2018 | SciresM and motezazer, naehrwert, hexkyz, probably others (independently). |
TSEC has access to the secure kernel carveout | TrustZone is responsible for managing security carveouts to prevent DMA controllers from accessing the carveout which contains the kernel, sysmodules, and other critical operating system data.
Until 8.0.0, the list of devices that could access the carveout included the TSEC. However, the TSEC can bypass the SMMU when in authenticated mode by writing to a certain register. Thus, pwning nvservices would allow one to take over the TSEC, and use it to write to normally protected mmio/memory. In 8.0.0, this was fixed by removing TSEC access, and adding TSECB access (TSECB cannot bypass the SMMU). |
With access to the TSEC mmio (nvservices ROP) and code execution in TSEC Heavy Secure mode, kernel code execution, probably. | 8.0.0 | 8.0.0 | 2017 (when TrustZone code plaintext was first obtained). | April 15, 2019 | Everyone |
deja vu (insufficient system state validation on suspend leads to pre-sleep BPMP code execution) | Jamais Vu was fixed in 2.0.0 by making the PMC secure-world only, blacklisting the BPMP's exception vectors from being mapped, and thoroughly checking for malicious behavior on deep sleep entry, since gaining pre-sleep code execution on the BPMP compromises the system.
However, the state validation performed by Nintendo's Secure Monitor was insufficient to prevent pre-sleep execution from being obtained. Prior to 6.0.0, one could use a DMA controller that had access to IRAM and was not held in reset (there were multiple) to race TrustZone's writes to the BPMP firmware in IRAM, and thus overwrite Nintendo's firmware with an attacker's to gain pre-sleep code execution. 6.0.0 addressed this by performing TrustZone state MAC writes and locking PMC scratch *before* turning on the BPMP, fixing the original Jamais Vu exploit entirely. In addition, the BPMP firmware in TrustZone's .rodata is now memcmp'd to the actual data after it is written to IRAM. This mitigates race attacks that modify the firmware. However, Nintendo both forgot to validate the BPMP exception vectors after writing them, and forgot to hold in reset a DMA controller that can write to the BPMP's exception vectors. AHB-DMA is not blacklisted by kernel mapping whitelist (Nintendo probably forgot it, because the TX1 TRM does not really document that it's present, although the MMIO works as documented in older (Tegra 3 and before) TRMs). Thus, with kernel code execution (or some other way of accessing AHB-DMA, e.g. nspwn on <= 4.1.0, TSEC hax, or other arbitrary mmio access flaws), one can DMA to the BPMP's exception vectors as they are written, causing TrustZone to start the BPMP executing an attacker's firmware at a different location than TrustZone intends/validates. This was fixed in 8.0.0 by blocking AHB-DMA arbitration and verifying it is held in reset during suspend, and thus there are no more devices that can write to the relevant MMIO at the right time. |
Arbitrary TrustZone/BootROM code execution, by using either the original Jamais Vu flaw (prior to 6.0.0 or a warmboot bootrom exploit (any firmware where pre-sleep execution can be gained). | 8.0.0 | 8.0.0 | December 2017 | April 15, 2019 | SciresM, motezazer and ktemkin, naehrwert (independently), almost certainly others (independently) |
TrustZone allows using imported RSA exponents with arbitrary modulus | TrustZone supports "importing" RSA private exponents for use by userland -- these are stored encrypted with TrustZone only keydata in NAND, and decrypted only to TZRAM. This prevents a console that has compromised userland from learning the private exponents of these keys and doing calculations with them offline. In practice, this is used for FS (gamecard communications), ES (drm), and SSL (console client cert communications).
However, the actual SMC API only imports the RSA exponent, and not the modulus, which is passed separately by userland in each call. There is no validation done on the modulus passed in -- this means that userland can pass in any message and modulus it chooses, and obtain the result of (message ^ private exponent) % modulus back from the secure monitor. By choosing a prime number modulus P such that P has "smooth" order (totient(P) == P-1 is divisible only by "small" primes), one can efficiently use the Pohlig-Hellman algorithm to calculate the discrete logarithm of such a result directly, and thus obtain the private exponent. This is mostly useless in practice, given the general availability of other exploits to obtain these decrypted exponents. This was fixed in 10.0.0 by importing the modulus in addition to the exponent for the ES device key and ES client cert key. For backwards compatibility reasons the SSL key and Lotus key still only import the exponent. StorageExpMod also now validates that the exponentiation of "DDDDD..." about the provided modulus by the imported exponent and then the fixed public exponent returns "DDDDD...", and returns invalid argument if validation fails. |
With userland privileges sufficient to use an imported RSA key: obtaining that RSA key's private exponent. | 10.0.0 | 10.0.0 | August 14, 2019 | August 14, 2019 | SciresM |
Kernel
Flaws in this category pertain exclusively to the HorizonOS Kernel.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Syscall Infoleaks | Many syscalls leaked kernel pointers on sad paths (for example svcSetHeapSize and svcQueryMemory), until they landed a bunch of fixes in 2.0.0. | Nothing really. | 2.0.0 | 2.0.0 | ? | ||
svcWaitSynchronization/svcReplyAndReceive bad cleanup on error | If there is a page fault when fetching handles from the userspace array, it cleans up by dereferencing all objects despite having only loaded first N. Allows the attacker to make arbitrary decrefs on any kernel synchronization object, and thus can be used to get UAF. Haven't actually been tried on real HW though, but should work (tm). | Kernel code execution | 2.0.0 | 2.0.0 | 24 April | qlutoo | |
Bad irq_id check in CreateInterruptEvent | CreateInterruptEvent syscall is designed to work only for irq_id >= 32. All irq_ids < 32 are "per-core" and reserved for kernel use (watchdog/scheduling/core communications).
On 1.0.0 you could supply irq_id < 32 and it would write outside the SharedIrqs table. |
You can register irq's in the Core3Irqs table, and thus register per-core irqs for core3, that are normally reserved for kernel. Useless. | 2.0.0 | 2.0.0 | ~October | 17 October | qlutoo |
Kernel .text mapped executable in usermode | Prior to 3.0.2 the kernel .text was mapped in usermode as executable. This can be used for usermode ROP for bypassing ASLR, but SVCs/IPC are not usable by running kernel .text in usermode. | Executing kernel .text in usermode | 3.0.2 | 3.0.2 | 34c3 (December 28, 2017) | qlutoo | |
Memory Controller not properly secured | The Switch OS originally had the memory controller not set to be accessible only by the secure-world, which was problematic because insecure access can compromise the kernel.
This was fixed partially in 2.0.0 by blacklisting the memory controller from being mapped by user-processes, and was fixed entirely in 4.0.0 by making the memory controller TZ-only and making all kernel accesses go through smcReadWriteRegister. |
With some way to access the memory controller MMIO, arbitrary kernel code execution. | 4.0.0 | 4.0.0 | January 2018 | January 2018 | SciresM, yellows8 |
Potential svcWaitForAddress thread use-after-free | Between 4.0.0, where svcWaitForAddress was introduced, and 7.0.0, there was a second intrusive rbtree node in KThread for the WaitForAddress tree (the key being (address, priority), sorted lexicographically). Unlike the WaitProcessWideKeyAtomic tree, the kernel forgot to reinsert the WaitForAddress node when the thread's priority changed (priority inheritance and/or SetPriority), breaking the rbtree invariants; and since the kernel walks through the entire tree to remove intrusive nodes, you could cause threads to stay in the tree even after their deletion.
7.0.0 fixed the issue by using the same intrusive node for both trees. The thread/node knows which tree it is in, and the latter is correctly updated when thread priority changes. |
It unluckily didn't look exploitable | 7.0.0 | 7.0.0 | July 2018 | February 2019 | TuxSH |
FIRM-package System Modules
Flaws in this category pertain to any of the built-in system modules.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Service access control bypass (sm:h, smhax, probably other names) | Prior to 3.0.1, the service manager (sm) built-in system module treats a user as though it has full permissions if the user creates a new "sm:" port session but bypasses initialization. This is due to the other sm commands skipping the service ACL check for Pids <= 7 (i.e. all kernel bundled modules) and that skipping the initialization command leaves the Pid field uninitialized.
In 3.0.1, sm returns error code 0x415 if Initialize has not been called yet. |
Acquiring, registering, and unregistering arbitrary services | 3.0.1 | 3.0.1 | May 2017 | August 17, 2017 | Everyone |
Overly permissive SPL service | The concept behind the switch's Secure Monitor is that all cryptographic keydata is located in userspace, but stored as "access keys" encrypted with "keks" that never leave TrustZone. The spl ("security processor liaison"?) service serves as an interface between the rest of the system and the secure monitor. Prior to 4.0.0, spl exposed only a single service "spl:", which provided all TrustZone wrapper functions to all sysmodules with access to it. Thus anyone with access to the spl: service (via smhax or by pwning a sysmodule with access) could do crypto with any access keys they knew.
This was fixed in 4.0.0 by splitting spl: into spl:, spl:mig, spl:ssl, spl:es, and spl:fs. |
Arbitrary spl: crypto with any access keys one knows. For example, one could use the SSL module's access keys to decrypt their console's SSL certificate private key without having to pwn the SSL sysmodule. | 4.0.0 | 4.0.0 | Summer 2017 (after smhax was discovered). | December 23, 2017 | Everyone |
Single session services not really single session | Several "critical" services (like fsp-ldr, fsp-pr, sm:m, etc) are meant to only ever hold a single session with a specific sysmodule. However, when a sysmodule dies, all its service session handles are released -- and thus killing the holder of a single session handle would allow one (via sm:hax etc) to get access to that service.
This was fixed in 4.0.0 by adding a semaphore to these critical single-session services, so that even if one gets access to them an error code will be returned when attempting to use any of their commands. |
With some way to access these services and kill their session holders (like expLDR): dumping sysmodule code, arbitrary service access, elevated filesystem permissions, etc. | 4.0.0 | 4.0.0 | May/June 2017 (basically immediately after smhax was discovered) | December 30, 2017 | Everyone |
nspwn | fsp-ldr command 0 "MountCode" takes in a Content Path (retrieved from NCM by Loader), and returns an IFileSystem for the resulting ExeFS. These content paths, are normally NCAs, but MountCode also supports a number of other formats, including ".nsp" -- which is just a PFS0.
When a path ending in ".nsp" is parsed by MountCode, the PFS0 is treated as a raw ExeFS. Because there is no NCA header, the ACID signatures are not validated -- and because there are no other signatures in a PFS0, this results in no signature checking happening at all. The actual .nsp handling is eventually done by {content mounting function} called by MountCode and other FS commands. Thus, by placing an ExeFS (NSOs + "main.npdm") and setting one's desired title ID to "@Sdcard:/some_title.nsp" or "@User:/some_title.nsp" etc one can launch arbitrary unsigned code, with arbitrary unsigned NPDMs. This appears to have been fixed by only allowing .nsp when the input fstype==7 for the internal content-mounting function, returning 0x2EE202 otherwise. |
With access to "lr": Arbitrary code execution with full system privileges. | 5.0.0 | 5.0.0 | Late 2017 | April 23, 2018 | Everyone |
Single null-byte stack overflow in Loader ContentPath parsing | Previously, loader content path parsing looked like this, where path_from_lr was up to 0x300 bytes and not necessarily null-terminated:
char nca_path[0x300] = {0}; strcat(nca_path, path_from_lr); for (int i = 0; nca_path[i]; i++) { if (nca_path[i] == '\\') { nca_path[i] = '/'); } } Thus, a content path of the maximum length (0x300 bytes) would result in strcat writing a NULL terminator past the end of the nca_path buffer. This was fixed in 6.0.0, the new code looks like this: char nca_path[0x300]; strncpy(nca_path, path_from_lr, sizeof(nca_path)); for (int i = 0; i < sizeof(nca_path) && nca_path[i]; i++) { if (nca_path[i] == '\\') { nca_path[i] = '/'); } }
|
With access to "lr": single null-byte stack overflow in Loader. Maybe (but probably not) loader code execution. | 6.0.0 | 6.0.0 | September 2, 2018 | September 19, 2018 | SciresM |
System modules vulnerable to selective downgrade attacks | Horizon has no mechanism for specifying the specific title version to Loader on process creation.
Observing this, one can note that after a system update one could install a downgraded version of a specific system module (e.g. nvservices) while leaving the rest of the OS at the same version. Unless there was some breaking API change, this allows one to make a console vulnerable once more to an exploit in a sysmodule by downgrading it and nothing else. This was fixed in 8.1.0 by incrementing a version field in NPDM, and checking it against a hardcoded list for certain titles in Loader's process creation func. |
With access to content installation commands (or a vulnerable lower version to selectively install newer titles), reintroducing bugs in vulnerable system modules on newer firmware versions. | 8.1.0 | 8.1.0 | When FIRM was first dumped in 2017. | June 17, 2019 | Everyone |
Broken RNG for Loader ASLR | The RNG used for generating the ASLR slide is only seeded with 32bits, with the data from svcGetInfo. Hence, one could bruteforce the seed if one has infoleaks from any programs. This can be successfully bruteforced with at least 2 sample codebin addrs from different programs (with only 1 sample a lot of invalid seeds are found), however in some cases more than 1 seed might be found.
With [15.0.0+] Loader now uses csrng_GenerateRandomBytes for determining the ASLR slide. See also loader-aslr-solver. |
Breaking ASLR for all non-KIP processes, allowing predicting the main-codebin base addr for all non-KIP processes until the next reboot. | 15.0.0 | 15.0.0 | January 30, 2022 (presumably found much earlier?) | October 11, 2022 | Everyone |
System Modules
Flaws in this category pertain to any non-built-in system module.
Summary | Description | Successful exploitation result | Fixed in system version | Last system version this flaw was checked for | Timeframe this was discovered | Public disclosure timeframe | Discovered by |
---|---|---|---|---|---|---|---|
Broken RNG used by ns | The code generating the sd seed and the data for the sd private/private1 file, all use nn::os::GenerateRandomBytes, not csrng. The sd-seed is generated first, then private, then private1. This allows deriving sd-seed from private since this uses TinyMT, as long as the system shipped from factory on [2.0.0+]. private1 is only useful if the system shipped with [4.0.0+].
There's various other code in ns using nn::os::GenerateRandomBytes as well. This includes the code generating ns_systemseed when it doesn't exist. ns_systemseed is generated at some point after the various sd-seed-related code (both are called from the same func). Hence, ns_systemseed can be recovered with the above method as well, if it wasn't recreated at some point without regenerating the above nand-save used with the above. With [15.0.0+] ns now uses csrng_GenerateRandomBytes for sd-seed/private and ns_systemseed, etc. This only matters when the file is newly generated, which is usually only for factory-fresh systems which ship with this version. |
Generation of a system's sd-seed allowing decryption of the NAX0 layer of data on SD, derived using the private file from SD. Applies to systems which factory-shipped with a system-version prior to 15.0.0 (that is, [2.0.0-14.1.2]). | 15.0.0, for newly generated files | 15.0.0 | December ~12, 2021 | October 11, 2022 | yellows8 |
Infoleak with Joy-Con HidCommand PairingIn | The joycon protocol handler for PairingIn copies data from stack to the response cmd-buf for sending PairingOut. Only the first byte is set to a type value, the rest is uninitialized stack data.
This was fixed with [15.0.0+] by directly writing to the response data without using stack data. |
Infoleak of hid stack via a bluetooth/uart message+response with a connected hid controller. This returns addrs for the main-codebin/stack, which allows defeating ASLR. | 15.0.0 | 15.0.0 | September 4, 2020 | October 10, 2022 | yellows8 |
bsdsockets ioctl SIOCGIFMEDIA input can contain ptr | Originally bsd ioctl SIOCGIFMEDIA used the user-specified ifmediareq structure directly from the input buffer. This includes a ptr. This ptr probably isn't actually used?
With [5.0.0+] the structure used as input for the ioctl was changed to using |
Useless? | 5.0.0 | 13.1.0 | February 14, 2022 | February 14, 2022 | yellows8, probably others |
bsdsockets ioctl SIOCGIFCONF infoleak | Originally bsd ioctl SIOCGIFCONF was handled by setting the data in IPC outbuf0 to the size/addr of IPC outbuf1. These buffers are HipcAutoSelect, so if buf1 is small enough for HipcPointer (otherwise it would be HipcMapAlias) the IPC-buf-ptr leaked into outbuf0 would be located in the codebin-region. Since this is done before the actual ioctl-handling, it doesn't matter whether the fd is valid.
This was fixed in [5.0.0+] by using a tmp struct on stack instead of buf0. |
bsdsockets-sysmodule codebin-region addr infoleak, which allows defeating ASLR. | 5.0.0 | 13.1.0 | February 14, 2022 (probably earlier) | February 14, 2022 | yellows8, probably others |
bluetooth BSA gatt_process_notification stack buffer overflow | gatt_process_notification is the GATT handler for processing notification/indication messages. gatt_process_notification does memcpy to stack from the input bt msg data, without size validation. The input len param isn't validated in this func either - if the remaining len following op_code is less than 2, a negative value will be used for the data copy to stack.
These were fixed by adding a bounds check for the size, size==0 is also checked for now. |
Bluetooth-sysmodule stack buffer overflow, with data received from a bluetooth message | 13.2.1 | 13.2.1 | November 2021 | January 19, 2022 | yellows8 |
ssl CVE-2021-43527 | CVE-2021-43527, see also here and here.
Using BigSig where the server cert sig is RSA-PSS results in the remote server throwing {no shared cipher} error when Switch connects. If however one creates a rootCA using BigSig (RSA-PSS), which then signs a server cert where the server key is RSA (not PSS), the vuln can be triggered (if the rootCA is trusted, via using the import service-cmd). It's unknown whether there's other ways to trigger the vuln. The crash occurs in VFY_Begin when using the previously overwritten data. A bitsize of |
Heap buffer overflow in ssl, overwriting data including a ptr to an object which is later used to load a funcptr. | 13.2.1 | 13.2.1 | Switch: December 1-2, 2021 | Switch: January 19, 2022 | |
bluetooth EventInfo infoleak | The various funcs which send messages to the thread which handles writing to EventInfo, didn't clear the stack msgbuf. Hence, the various get-EventInfo cmds could return leaked stack data. This likely affected most (?) get-EventInfo cmds, besides CircularBuffer-GetHidReportEventInfo.
This only matters for events where there's uninitialized regions of the EventInfo, such as events with variable-size data without a memset. This was fixed by clearing the msgbuf in a number of funcs. |
Bluetooth-sysmodule stack infoleak, which allows defeating ASLR | 13.0.0 | 13.1.0 | During initial diff. Added to this page on: December 12, 2021 | yellows8 | |
Infoleak with hid:sys SetButtonConfigStorage{name}Deprecated | These cmds pass a stack ptr for the StorageName when calling the internal func. Nothing is written to this StorageName. Hence, stack infoleak (data is copied as a NUL-terminated string), which can be later read by the GetButtonConfigStorage{name} cmds.
This was fixed by removing the Deprecated cmds in 13.0.0. |
Infoleak of hid stack from a StorageName readable via GetButtonConfigStorage{name}, up to the NUL-terminator. | 13.0.0 | 13.0.0 | December 11, 2020 | September 27, 2021 | yellows8 |
bluetooth WriteHidData/WriteHidData2/SetHidReport unchecked memcpy size | WriteHidData/SetHidReport copies the input struct to stack, then passes it to the funcptr/vfunc call. WriteHidData2 passes the input buffer addr directly to the funcptr/vfunc call. The called func eventually copies the input data to the stack struct using the specified size without validating it.
This requires access to the btdrv service, only hid and btm have access. This was fixed with 12.1.0 in WriteHidData/SetHidReport by doing a fixed-size copy into another tmp struct, with the size field being clamped to a maximum of 0x2BB afterwards. This struct is then used when calling the vfunc. The vfuncs called by WriteHidData/WriteHidData2/SetHidReport were also updated to clamp the size to the required maximum value. |
Stack buffer overflow | 12.1.0 | 12.1.0 | July 16, 2020 | July 6, 2021 | yellows8 |
bluetooth stack buffer overflow with HID DATA packets | The BSA (bt-stack) func bta_hh_co_data copies data from a HID DATA packet to stack without checking the size, then sends it over Uipc. [7.0.0+] The user Uipc callback also copies the input data to stack without checking the size, then sends it to the sharedmem CircularBuffer.
With [12.0.2+] this was fixed in bta_hh_co_data by clamping the size to a maximum of 0x2BB. The aforementioned buffer overflow in the Uipc callback can't be triggered since at that point the size was already clamped. Before this bta_hh_co_data func is reached, there is no validation of the size (such as comparing against the L2CAP MTU) when Basic Mode is being used. Actually triggering this requires using a data-size larger than the normal L2CAP MTU. This can be done by for example, using raw HCI to send the packet from the remote bluetooth device. Note that when the remote device is configured as an audio device for [12.0.0+] where BluetoothDevicesSettings.TrustedServices was only ever set for audio since system-boot, it is not possible for the remote device to connect to the Switch for HID. |
ROP under bluetooth via HID DATA packet sent by a paired HID bluetooth device. This can be triggered at any time while not in sleep-mode, when not in airplane-mode. The earliest is while the Nintendo Switch logo screen is displayed during system boot. | 12.0.2 | 12.0.2 | July-August 2020 | May 11, 2021 | yellows8 |
bluetooth GetAdapterProperty/SetAdapterProperty unchecked memcpy size | GetAdapterProperty copies data from stack to the output buffer using the buffer size, without checking the size (when not handling the Name type). SetAdapterProperty copies data to stack from the input buffer using the buffer size, without checking the size.
This requires access to the btdrv service, only hid and btm have access. This was fixed with 12.0.0 by replacing the buffer data with a fixed-size-struct. |
Stack infoleak with GetAdapterProperty, stack buffer overflow (and hence ROP) with SetAdapterProperty. | 12.0.0 | 12.0.0 | July 17, 2020 | April 7, 2021 | yellows8 |
Bluetooth A-63146698 | A-63146698 / CVE-2017-0785. See also here. | Bluetooth-sysmodule stack infoleak, which allows defeating ASLR (note: not tested on hw). | 5.0.0 | 11.0.0 | Switch: December 2020 | Switch: December 25, 2020 | Switch: yellows8 |
Bluetooth sdp_server.cc process_service_search() continuation request p_req validation | With [5.0.0+], the following was added to the if-block prior to loading cont_offset from p_req: (p_req + sizeof(cont_offset) > p_req_end) (which verifies that cont_offset is within message bounds).
|
Bluetooth-sysmodule out-of-bounds read from heap, probably not useful since the read value must match a state field, etc. | 5.0.0 | 11.0.0 | Switch: December 2020 | Switch: December 25, 2020 | Switch: yellows8 |
hid:sys ButtonConfig s32 array-index not validated | The input s32 array-index for hid:sys ButtonConfig cmds 1255-1270 was originally not validated. Using a negative or >=5 index results in accessing out-of-bounds data, with an array stored on stack.
[10.1.0-10.2.0] Each of these cmds will now Abort if the s32 is negative or >=5. [11.0.0+] Now an unsigned compare is used, with 0 or an error being immediately returned when the value is invalid. |
hid infoleak, out-of-bounds mem-write anywhere in hid address-space relative to the stack array (with constraints on the data). | 10.1.0 | 11.0.1 | April 18, 2020 | July 14, 2020 | yellows8 |
AM IDisplayController ClearCaptureBuffer OOB | The captureBuf is used as an array index without proper validation. There is code validating it, but on failure it just skips over a code-block, with code using captureBuf still being used afterwards. Then this is used to write bools into a global array, one of which is from the command input.
This was fixed with [9.1.0+] by requiring captureBuf = 0-1. |
OOB bool writes into an array | 9.1.0 | 13.1.0 | ~July 31, 2019 | January 26, 2022 | yellows8 |
AM IDisplayController TakeScreenShotOfOwnLayer OOB | The captureBuf is used as an array index without validation. Data used from this array includes calling a funcptr from the array entry, if set. Eventually this is also used to write bools into this array, one of which is from the command input.
With [5.0.0+] a func is eventually called to get a ptr determined by the input captureBuf, with nullptr being returned for captureBuf>=0x10. The caller will Abort if nullptr was returned. |
OOB array access | 5.0.0 | 13.1.0 | ~July 31, 2019 | January 26, 2022 | yellows8 |
AM IStorage infoleak | Originally the buffer allocated by CreateStorage using the specified input size was not cleared. With [8.0.0+] this was fixed by adding a memset() for the buffer after successful allocation.
Hence, IStorage->IStorageAccessor->Read will return uninitialized memory when the Write cmd was not previously used with the specified region. |
Infoleak from the main AM heap, allowing defeating ASLR by reading addresses from previously allocated objects. | 8.0.0 | 8.1.0 | December 2018 | August 9, 2019 | yellows8 |
Out-of-bounds array read for BCAT_Content_Container secret-data index | The BCAT_Content_Container secret-data index is not validated at all. This is handled before the RSA-signature(?) is ever used. Since the field is an u8, a total of 0x800-bytes relative to the array start can be accessed.
This is not useful since the string loaded from this array is only involved with key-generation. |
Unknown | 2.0.0 | August 4, 2017 | August 6, 2017 | Shiny Quagsire, yellows8 (independently) | |
OOB Read in NS system module (pl:utoohax, pl:utonium, maybe other names) | Prior to 3.0.0, pl:u (Shared Font services implemented in the NS sysmodule) service commands 1,2,3 took in a signed 32-bit index and returned that index of an array but did not check that index at all. This allowed for an arbitrary read within a 34-bit range (33-bit signed) from NS .bss. In 3.0.0, sending out of range indexes causes error code 0x60A to be returned. | Dumping full NS .text, .rodata and .data, infoleak, etc | 3.0.0 | 3.0.0 | April 2017 | On exploit's fix in 3.0.0 | qlutoo, ReSwitched Team (independently) |
Unchecked domain ID in common IPC code | Prior to 2.0.0, object IDs in domain messages are not bounds checked. This out-of-bounds read could be exploited to brute-force ASLR and get PC control in some services that support domain messages. | 2.0.0 | 2.0.0 | ~July 2017 | 20 July 2017 | hthh | |
expLDR (sysmodule handle table exhaustion) | Most sysmodules share common template code to handle IPC control messages. The command DuplicateSession (type 5 command 2)'s template code will abort() if it fails to duplicate a session's handle for the requester. Because many sysmodules have limited handle table size (smaller than the browser/other entrypoints), repeatedly requesting to duplicate one's session will cause the sysmodule to run out of handle table space and abort, causing the service to release all its handles cleanly. | Sysmodule crashes. Most usefully, crashing ldr allows access to fsp-ldr and crashing pm allows access to fsp-pr. Useless after 4.0.0, which mitigated a number of single-session service access issues. | Unfixed | 4.1.0 | 24 June 2017 | 8 March 2018 | daeken |
Transfer Memory leak in nvservices system module | The nvservices sysmodule does not clear most of its transfer memory prior to release. | The calling process can read key bits of memory, including breaking ASLR (by revealing the image base) and exposing the address of other transfer memory to set up attacks. More details here: transfermeme (nvservices info leak) by daeken | 6.0.0 | 6.0.0 | June 2017 | 16 October 2018 | qlutoo and hexkyz,
daeken (independently) |
OOB write in audio system module | Prior to 2.0.0, the AppendAudioOutBuffer and AppendAudioInBuffer IPC commands would blindly increment the appended buffers' count while using said count value as an index to where the user data should be copied into. This resulted in an 0x28 bytes, user controlled, out-of-bounds memory write into the audio sysmodule's memory space.
Combined with the GetReleasedAudioOutBuffer or GetReleasedAudioInBuffer commands, this could also be used as an 8 byte infoleak. In 2.0.0, the commands now return error code 0x1099 if the number of unreleased buffers exceeds 0x1F. |
Code execution under audio sysmodule | 2.0.0 | 2.0.0 | November 2, 2018 | hexkyz, probably others (independently). | |
nvhax (memory corruption in nvservices system module) | Prior to 6.2.0, the nvservices ioctl NVGPU_GPU_IOCTL_WAIT_FOR_PAUSE would take a single "pwarpstate" argument which would be interpreted by nvservices as a memory pointer for writing 2 "warpstate" structs (one for each Streaming Multiprocessor).
This resulted in nvservices attempting to blindly memcpy into this user supplied address and trigger a crash. However, if paired with an infoleak, this could be used to arbitrarily write 0x30 bytes anywhere in nvservices' memory space. Additionally, the "warpstate" struct itself was never initialized, which means nvservices would leak the 0x30 bytes from the stack. By invoking other ioctls it was also possible to partially control the stack contents and achieve a usable arbitrary memory write primitive. In 6.2.0, NVGPU_GPU_IOCTL_WAIT_FOR_PAUSE now takes 2 inline "warpstate" structs instead of a "pwarpstate" pointer, thus effectively avoiding the bad memcpy. |
Code execution under nvservices sysmodule | 6.2.0 | 6.2.0 | April 5, 2017 | November 24, 2018 | hexkyz |
Infoleak in nvservices system module | The nvservices ioctl NVMAP_IOC_ALLOC takes an optional argument "addr" which allows the calling process to pass a pointer to user allocated memory for backing a nvmap object. If "addr" is left as 0, nvservices uses the transfer memory region (donated by the user during initialization) instead, when allocating memory for the nvmap object.
By design, freeing the nvmap object by calling the ioctl NVMAP_IOC_FREE returns, in its "refcount" argument, the user address previously supplied if the reference count reaches 0. However, prior to 6.2.0, the case where the transfer memory region is used to allocate the nvmap object was not taken into account, thus resulting in NVMAP_IOC_FREE leaking back an address from within the transfer memory region mapped in nvservices' memory space. In 6.2.0, NVMAP_IOC_FREE no longer returns the address when the transfer memory region is used instead of user supplied memory. |
Combined with other vulnerabilities: Defeating ASLR in nvservices sysmodule. | 6.2.0 | 6.2.0 | April 2017 | November 24, 2018 | Everyone |