Skip to content

Switch System Flaws

switchbrew.org • October 22, 2026

3 Software 3.1 Bootloader 3.2 TrustZone 3.3 Kernel 3.4 BootImagePackage System Modules 3.5 System Modules 3.6 Internet Browser 3.6.1 Whitelist 3.7 NintendoSDK 3.7.1 nnSdk 3.7.2 NEX 3.7.3 Pia 3.7.3.1 Affected Games 3.7.4 ENL 3.8 Games

3.4 BootImagePackage System Modules

3.6 Internet Browser 3.6.1 Whitelist

3.7 NintendoSDK 3.7.1 nnSdk 3.7.2 NEX 3.7.3 Pia 3.7.3.1 Affected Games 3.7.4 ENL

3.7.3 Pia 3.7.3.1 Affected Games

3.7.3.1 Affected Games

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.

[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.

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.

The Tegra X1 security engine supports writing keyslot data to the engine with syntax as follows:

SECURITY_ENGINE->AES_KEYTABLE_ADDR = (keyslot 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.

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.

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 = , $c7 = .

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:

mac =

key = AES-ENCRYPT(hardware csecret 0x1, seed)

signature = AES-ENCRYPT(key, mac)

As mentioned above, when jumping to signed microcode the caller is expected to load hardware crypto register $c6 = , $c7 = .

However, TSEC ROM performs no validation on the input seed used to generate the signing key.

This leads to the following attack:

Attacker gains rop under any secure microcode payload with signature = S.

Attacker uses the "csigenc" instruction to obtain K = AES-ENCRYPT(hardware csecret 0x1, S).

Attacker jumps to their own microcode with $c6 = , $c7 = S

TSEC ROM calculates key = AES-ENCRYPT(hardware csecret 0x1, S) = K, and the signature check passes.

Attackers microcode is executed in secure mode as though it were signed by NVidia.

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.

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 = .

This has many uses/results, notably including dumping the "true" signature key (set signature = zeroes, perform csigenc using csecret 0x1).

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.

Flaws in this category pertain to the firmware running on hardware devices, such as wifi/bluetooth, etc. Firmware is generally uploaded by sysmodules.

Flaws in this category pertain to any bootloader component such as the package1ldr , the NX bootloader or the warmboot binary .

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().

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).

Warning: one could irreparably brick one's console by playing with this.

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.

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.

However, a usable brute force is on the order of >= ~2^80, so this is almost certainly not actually usable in any meaningful context .

Flaws in this category pertain exclusively to the Secure Monitor .

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.

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.

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.

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).

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 IStorageAccessor->Read will return uninitialized memory when the Write cmd was not previously used with the specified region.

[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.

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.

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.

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.

This was fixed by removing the Deprecated cmds in 13.0.0 .

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.

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 $((16384 + 32 + 64 + 64 + 64)) is only enough to overwrite cx->hashcx, to fully overwrite cx->hashobj an additional 0xC-bytes (additional 96 bits) is needed. Note that partial overwrite isn't an option: this is the func that initializes those fields to begin with, it just does deinit first before initializing hashcx/hashobj (prior to that these fields would be all-zero when not overwritten by the buf-overflow).

These were fixed by adding a bounds check for the size, size==0 is also checked for now.

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.

This was fixed with [9.1.0+] by requiring captureBuf = 0-1.

This was fixed in [5.0.0+] by using a tmp struct on stack instead of buf0.

With [5.0.0+] the structure used as input for the ioctl was changed to using int ifm_ulist[1] instead of int *ifm_ulist (which is unused). The input structure is copied to a tmp struct which is used as the original ifmediareq structure, with ifm_ulist always NULL. The user can still specify a non-zero ifm_count value, however that's not useful with ifm_ulist being always NULL.

This was fixed with [15.0.0+] by directly writing to the response data without using stack data.

Thus, with a few NRO mapping addresses, one could learn the TinyMT state and derive all /future RNG outputs, breaking NRO aslr for all processes.

With [15.0.0+] ro now uses csrng_GenerateRandomBytes to determine the random map address for NROs.

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. This would also apply after being deleted during {System Settings -> Formatting Options -> Initialize Console}, and also with a refurbished console.

This requires the AV functionality added with [13.0.0+], however this func is only reachable with [14.0.0+] where the required functionality was enabled.

This requires message data that's larger than the MTU, so fragmentation must be used, or manually send the ACL data to bypass the MTU.

This can be triggered via an AVRC message with opcode=0x0 (vendor). The above type 0xC is reached via AVRC ctype 0..4, while 0xD is reached with ctype>=0x9.

With [15.0.0+] the size value for the memcpy (which is also written to the request struct) is clamped to a max value.

With [15.0.0+] error code 0x1906B is now returned if "Count" is larger than 8.

With [14.0.0+] size=0 is now properly handled, and also the state for the second addr from GetCodeAddress is now properly initialized.

ShareReportRing maps tmem and writes the ring buffer/count field into object state. PopulateRing also eventually initializes these fields, with the buffer being allocated from ExpHeap instead of tmem. These fields are not cleared during object creation from OpenUsbEp.

GetXferReport after validating the cmd input, just uses object state assuming it was initialized. This runs code which is the same as the user-process code handling the tmem ringbuf.

Therefore, by skipping using PopulateRing and then using GetXferReport the sysmodule will use an uninitialized ringbuf ptr, and an uninitialized count field. If one could control these fields by doing ExpHeap allocations prior to OpenUsbEp so that {target fields} would be located at {IClientEpSession ring fields}, then one could read usb-sysmodule memory at the target buffer address.

See here for ringbuf format. The sysmodule will Abort if read_index is >= {ring count field from object state}. Otherwise it copies an entry from that index to output, and updates read_index.

This is probably tricky to abuse as the ringbuf ptr has to be valid, and {see above} (likewise for write_index when the report-ringbuf-writing func runs).

PostBufferAsync/BatchBufferAsync also use seperate object ring fields which are left uninitialized from OpenUsbEp. Targeting this would be tricky with the ring restrictions - this would allow writing data to a ring addr however.

Pre-4.0.0 (only 2.0.0 checked) is not affected by these. The ring fields in the object are cleared during object creation (no memset of the entire object however). GetXferReport would null-deref if PopulateRing was skipped. PostBufferAsync/BatchBufferAsync will throw an error if PopulateRing was skipped. Pre-4.0.0 also has different ring handling as well.

[16.0.0+] The IClientEpSession init func now clears the remaining previously uninitialized fields. The cmds using the ring fields still don't check for NULL, so using GetXferReport/PostBufferAsync/BatchBufferAsync without PopulateRing will just trigger null-deref. Even if the ptr were somehow valid but ring-count field was left at 0, this would then Abort due to: if (ring_count

Additional memwrites are also done to the above outbuf following the initial memcopy. This can be avoided if the buffer doesn't contain bytes with values 3-6 (if using values in that range is really needed, the cmd input StorageId param can be set to the required value so that the specified value doesn't trigger the memwrite). Value 6 shouldn't be used anyway (see above).

ns EstimateSizeToMove first calls the same func which does the copy above (outbuf is also located on stack), then it calls another func. Hence, same vuln here.

As of [17.0.0+] an error is thrown if the input array count is larger than 8 (size of the stack dst-array).

ISender Send when the above linked-list is empty, runs a switch-statement with (inval>>8)&0xFF . This uses another linked-list where the ptrs are initially {within ISender obj}. No space is allocated in the ISender obj for the linked-list object-data. Therefore using Send with val 1 {RF chunk from freeing an object} -> {module object from IPmModule Initialize}. Then one would use the Send vuln to corrupt the RF chunk, changing the size to a larger value. Then one would trigger an object allocation (probably same object which was previously freed), then another object for overwriting the module object (ISender would work) with ptrs at the target offsets in the module object. Then once IPmModule GetRequest is used, the returned u32s would be the high-u32 from ptrs. Due to alignment requirements with each allocation, it isn't possible to shift the allocations in order to leak ptr low-u32.

[17.0.0+] Now throws an error if the input count for OpenSender is 0.

With [17.0.0+] the second block of csel code which previouly essentially used the clamped size from above, was replaced with code which properly clamps to the max-size constant.

This will leak audio-sysmodule stack into the output buffer as long as the state/input checks pass (for the remainder of the buffer following the string NUL-terminator).

With [18.0.0+] data is written directly to the outbuf instead of the stack tmpbuf.

With btm comprimised, one could return a large output count and trigger a stack buffer overflow with data following that global array, however exploiting this would be difficult since that data would be uncontrolled (can't directly control it from this cmd at least).

A stack infoleak can be obtained with this as well (assuming the above output array isn't full).

Even though the name has "ForDebug", there's no checks which would trigger an error / return early (this also always returns 0).

[18.0.0+] now clears the output buffer, and also now prints strings into the buffer instead of writing binary data (overflow no longer possible).

It's unknown whether there's a way to actually control this data with a large enough enough size.

See 18.1.0 for the diff/fix.

If one could somehow determine the data which was returned by nn::os::GenerateRandomBytes during seeding (which is likely difficult), the global RNG would be broken.

With [19.0.0+] nn::os::GenerateRandomBytes usage was replaced with spl GenerateRandomBytes.

The exact location in the workmem will vary depending on the input params - these objects are dynamically allocated in the workmem. The following will leak vtables: Sink, Effect.

If the initialization func fails, the tmem is unmapped without clearing it first. It's unknown whether there's a way to actually trigger an infoleak with this however. With [19.0.0+] it's now cleared on failure.

With [19.0.0+] the dtor now clears the workmem when needed.

Also note that a field is loaded from dest_MixInfo which is used as a splitter_id, so splitters need to be initialized where count is large enough for that id.

With [19.0.0+] after getting the mix_id (loop-index/input) it now does: if (mix_id = nn::audio::server::MixContext::GetCount()) continue;

[20.0.0+] Removed the vtable ptr, with data intended for the user-process being moved from tmem+0x8 to +0x0. Also, instead of calling memset, funcs are called for manually clearing tmem.

This was fixed with [20.0.0+] by clamping the size.

[21.0.0+] Now validates the advert-size with sizeof(NetworkInfo.AdvertiseData).

For more details see here .

Since the highest DeviceTypeInternal is value 30, using >=31 will load an OOB ptr. This ptr is written to state, and also immediately passed to a called func. As long as ptr is valid it should be fine with this func.

This functionality is also used eventually by ApplyHdlsNpadAssignmentState and ApplyHdlsStateList.

It's unknown whether there's a way to exploit this. Also note that hid:dbg is not normally accessible to retail titles.

[21.0.0+] Arrayindex=0 is now used when the input is invalid.

With bluetooth-classic via L2CAP, a hard-coded MTU of 0x205 is sent in the configure request. However the code handling received configure requests will set payload_size to 0x2A0 if no MTU is specified, or the input MTU if it's within range 0x30..0x2A0. Hence, sending data large enough for buffer overflows requires bluetooth-classic via L2CAP + manually sending large ACL data.

[15.0.0+] gatt_l2cif_config_ind_cback which handles the received configure-requests with bluetooth-classic mentioned above, now uses MTU range 0x30..0x205 with the default MTU being 0x205. It is therefore no longer possible to trigger the previously mentioned buffer-overflows with bluetooth-classic.

The size used with memcpy is (u16)(insize-4), so when insize is less than 4 the copy size will be {negative value masked to u16}. This will therefore eventually crash when the stacktop is reached during memcpy.

[15.0.0+] Paritially fixed due to corrected MTU handling (doesn't apply to negative-copysize). [21.0.0+] Fully fixed with proper size validation.

The data is copied to obj_impl+0x8+0x28, with each entry being 0x20-bytes. The event handle returned by AttachAvailabilityChangeEvent is at obj_impl+0x8+0xB8+0x14 (Same with nfc/nfp interfaces). This therefore means +0xA4 in the input buffer will overwrite the handle returned by that cmd, allowing one to leak any handle with the specified value. This can be done with count=0x6. The object is large enough that this count will only overwrite data within the current object. However during the dtor it will use ptrs which were corrupted with this (located before the event), so one must avoid closing the session unless the input data included valid ptrs.

This can be exploited by just using a 0xC0-byte (array_count=0x6) input buffer with Initialize where each u32 is the target nfc handle value, then using cmd GetAvailabilityChangeEventHandle to leak the handle.

[22.0.0+] This was fixed by clamping the count to a maximum of 0x4.

This could be used to copy data OOB from the input buffer into workmem, to leak data when using a HipcPointer buffer with RequestUpdateAuto.

[23.0.0+] This was fixed by validating the count.

With a smaller than expected outbuf it would also write data OOB following the outbuf, however this is likely not much use with the data being uncontrolled (?). Also note that the IPC buffer sizes are only checked after usage of the IPC buffers are already finished, at which point this validation doesn't matter.

[23.0.0+] This was fixed. The inbuf/outbuf sizes must be at least the size of AudioRendererUpdateDataHeader (0x40), and the inbuf size is validated against the AudioRendererUpdateDataHeader. The InfoUpdater::Update* funcs moved the size-validation/etc code to the start of the func (minimum size is checked instead of exact match). The outbuf size is also now validated here when needed. After each Update func returns, it will also throw an error if the relevant size field is non-zero (size field from AudioRendererUpdateDataHeader is larger than expected). Additional outbuf_size validation was also added once finished prior to calling CheckConsumedSize.

This enters a loop, the loop count is not validated. Id fields are loaded from input, without validation. *(prev_context+constant) = ptr+cur_id*sizeof(context); prev_context is initially the context ptr for the current context (selected via user input however it's properly validated), however after that it's set to the ptr which was used for the cur_id write. Abusing this is probably tricky since the target data would have to be exactly at {location}.

This essentially initializes a linked-list of ptrs using the user-specified list of ids as the arrayindex. This list is used elsewhere, such as nn::audio::server::SplitterContext::UpdateInternalState. With just 1 entry in the list, UpdateInternalState will crash if the resulting ptr hits unmapped mem.

nn::audio::server::SplitterContext::UpdateInternalState (which is called by nn::audio::server::CommandGenerator::GenerateVoiceCommands, eventually triggered async by RequestUpdate) loops through the the linked-list setup above. When u8 entry+24 and +25 are 1, this copies data. When continuing it then clears u8 +25 and loads the ptr. If one could set the list-id-entry so that it uses controlled data for this linked-list entry, this would then allow copying data {from/to entry data}. It's not possible to directly set the OOB splitter-data via UpdateSplitterInfo since the id is properly validated (and that uses vfunc-call anyway).

This requires at least REV2 for splitters support.

[23.0.0+] This was fixed by adding a func which handles fully validating the splitters user-input.

Using OOB value will therefore require valid ptrs where-ever the entry lands.

Note that nn::audio::server::SplitterContext::GetInfo is also used prior to the above with the same user-input field, for getting the SplitterInfo for use with nn::audio::server::SplitterInfo::GetDestinationCount.

[23.0.0+] This now verifies that dest_splitter_id is not negative, instead of -1. A bounds check was also added for this.

The returned ptr is then used with nn::audio::server::ISplitterDestinationData::GetMixId, if it's not invalid (0x7fffffff), it is then passed to nn::audio::common::EdgeMatrix::Connect(int, int) as the last param. The value from GetMixId originates from user-input data when the ISplitterDestinationData was last updated. The first input int patam for Connect is state_mixid. As there's no validation, this would allow ORRing a bitfield OOB.

The above loops through the Destinations for the specified splitter (AudioRendererMixInfoIn.dest_splitter_id).

Note that UpdateConnection will the skip the above and just call nn::audio::common::EdgeMatrix::Connect with when an user-input field is not 0x7fffffff (AudioRendererMixInfoIn.dest_mix_id).

[23.0.0+] nn::audio::server::InfoUpdater::UpdateMixes additionally now verifies that dest_mix_id is not negative.

Note that nn::audio::common::EdgeMatrix::RemoveEdges(int) (this is the only caller) is also used with the state_mixid early in UpdateConnection. RemoveEdges is the only way to clear bits in the EdgeMatrix.

With AudioRendererParameter.submix_count=0 and with MixInfo.mix_id=0, this essentially means the mix_id used above is the bit-pos to set relative to the EdgeMatrix buffer. Since with submix_count=0 the buffer is 0x8-bytes, you can therefore do the following for OOB bit-set for example: d.mix_id = ((0x8 + ) press A with the already selected top button -> press the R button.

Demo: Main- -> select button on the right side -> press A.

Press A -> select the bottom entry in the list.

Select the SEGA icon -> press A.

This will then trigger launching the online-WebApplet with the plain-http " URL.

With game-update v1.1.3 the whitelist no longer allows plain-http. The plain-http links appear to have been changed to https.

This section documents vulnerabilities for NSOs in NintendoSDK.

This section documents vulnerabilities for nnSdk (sdknso).

The hid-sysmodule code which writes data here does handle it properly: size is clamped to a max size, and the data-read uses a fixed-size anyway (hence there's no way to trigger this sdknso vuln with the hid-sysmodule tmem writing code).

This could only be exploited if one directly writes to the tmem when one has previously compromised hid-sysmodule, without using the normal tmem-writing func for this.

There are only a few apps which use hidbus.

As loading the handle from msghdr+0 isn't really useful, this is really only usable with cmds which return multiple handles of the same type (copy/move). The only known cmds which return multiple handles return interface and Event but those don't apply here (separate move/copy handles).

This can be used by a compromised sysmodule to target processes using the services hosted by that sysmodule - this is likely useless.

With 23.x.x it will now Abort if the relevant handle-count is invalid (Copy/Move).

The tipc client code likewise originally didn't validate the HIPC HeaderData/SpecialHeaderData as needed, with 23.x.x it will now Abort with a Result if invalid.

The AddrInfo deserialization ( nn::socket::resolver::serializer::DNSSerializer::FromBufferInternal (addrinfo&, unsigned char const*, unsigned long) , etc) has handling for when ai_family isn't recognized. The input ai_addr is memcpy'd (with user-input size) to &addrinfo->ai_addr, instead of the ai_addr buffer which was just allocated.

To avoid crashing due to corrupting ai_addr, one can set the first 8-bytes of the input ai_addr data to all-zero.

This is used by bsd GetAddrInfoRequest/GetAddrInfoRequestWithOptions, and by nnSdk when deserializing the output data from GetAddrInfo*. For the first addrinfo with the former it's located on stack, however the rest is allocated on heap (nnSdk uses heap regardless).

This was fixed with nnSdk 23.x by throwing an error when ai_family is unrecognized instead.

This section documents client-side vulnerabilities for NEX .

This section documents vulnerabilities for Pia .

In v5.11.3 (exact starting version unknown) the fixes aren't present for the below vulns which were fixed in v5.9.3, while in v5.18.98 these are present (exact starting version unknown). This probably indicates that the vuln fixes were backported from a newer Pia version to v5.9.3 (v5.9.6 is equivalent to v5.9.3 for the vuln-fixes listed below).

The Pia packet handlers are only active when the game is using multiplayer. LanProtocol is only active in the games which are actively using the LAN-mode option (not Ldn) - only certain games support LAN-mode. The LanProtocol Pia packet handler can be reached while in a lobby or searching for one.

Most Pia packets require an active StationProtocol connection to be active with {InetAddr which the packet was received from}, otherwise the packet is filtered out. The only protocols which don't use filtering are the following: NatTraversalProtocol, LanProtocol, StationProtocol, LocalProtocol.

Note that broadcast IP-dest Pia packets are accepted - this can be used to target every device on the network which is using Pia (which is really only useful with {above protocols} due to the filtering mentioned above, unless one also handles StationProtocol).

Note that there's various requirements before it would actually reach the memcpy, such as must return true.

This is called from nn::pia::session::MeshProtocol::ParseConnectionReport().

ParseConnectionReport uses a state ptr for object nn::pia::session::RelayRouteManageJob, it will return if not set. nn::pia::session::Mesh::Initialize handles setup for this, depending on an input field from nn::pia::session::Mesh::Setting. These settings originate from , which is called by user-code with the needed settings. ParseConnectionReport is therefore only usable if the game explicitly enables the Relay functionality.

In fixed versions immediately after the StationIndex validation it now does: if(statefield+0x10 , returning if ret is false.

Then it calls nn::pia::lan::LanProtocol::ReceivedFragmentData::Receive(), with the memcpy'd buffer/size from the above LanSessionMessage, and other fields from LanSessionMessage. This eventually memcpys the input buffer to object+{offset}+{chunksize_field}*inputu8, there is no validation for size or inputu8 (except for the above size check). Hence, if the u8 is large enough, this would result in a heap buffer overflow.

In fixed versions ReceivedFragmentData::Receive added a bunch of validation before the memcpy.

Hence, stack buffer overflow. Note that there's similar loop code in nearby funcs, which do validate the count properly.

In fixed versions the arraycount field is now validated.

SessionProtocol uses ReliableSlidingWindow MessageHeader, with a maximum message size of 0x100. The allocated size used for the above u64 array is also 0x100-bytes. Hence, when triggering a buf overflow the data after the buffer is uncontrolled data from the SessionProtocol object.

In fixed versions immediately after grabbing a packet, it now checks the crypto flag. If it's plaintext the packet is dropped.

This can be used to send a plaintext Pia packet without needing to handle encryption, especially useful if the session-key can't be obtained (online-play matchmaking). This could be combined with other vulns if wanted.

SetStationDataList will update state and immediately return if the join was denied. It will also validate the num_mesh_stations field against state. ParseJoinResponse also essentially verifies that the message was received from the host device.

The input buffer size is ignored.

The num_fragments field must be value 1 or <=3 otherwise it will return, there's two seperate code blocks handling these.

Other than the checks at the start, there's no validation for the index fields. So large enough values could result in OOB-reads.

When handling multiple fragments, it will loop through the stationinfo list. There is no validation for the u8 count field or the baseindex field. It calls a vfunc from obj baseptr+index*{entrysize} with data from the buffer, where index starts with the above baseindex field. Afterwards, an u8 is copied into an u32 array (with certain versions an u16 is deserialized into an u16 array).

nn::pia::session::ProcessUpdateMeshJob::UpdateStationDataList is (eventually) called from nn::pia::session::MeshProtocol::ParseUpdateMesh , which has similar issues to the above.

Note that ParseJoinResponse/ParseUpdateMesh essentially require the message to be received from the host device.

With fixed versions (v5.18.98, exact version unknown) various validation was added. Additional/updated validation was added in a later version (v5.31.0, exact version unknown).

The mechanism for generating the Pia SessionKey for LAN has also changed over time.

The LAN non-Pia-encapsulated packets were also originally sent in plaintext, however at some point it was changed to mostly encrypted.

However, nn::pia::transport::UnreliableProtocol::Receive will clamp the size from the list entry to the outbuf size when doing the memcpy. So this is probably useless.

It's unknown whether there's a version where more data could be overwritten, and whether that would be useful.

This is fixed in v5.31.0, exact version unknown. The message is dropped if too large in Dispatch.

With v6.20.1 (exact version unknown - fix isn't present in v5.32.0), the code using Scan* now clears the input ScanFilter properly. With v6.25.1 (exact version unknown - fix isn't present in v6.23.3), the code using CreateNetwork*/ConnectNetwork* now clears the input SecurityConfig properly.

This section documents game-updates where the Pia version was bumped to a version which includes vuln-fixes, for Pia versions relevant to the above section.

Splatoon 2 (see page)

Super Mario Party (see page)

Mario Tennis™ Aces v3.1.1 updated Pia to v5.9.6, previously v5.9.0.

Mario Kart 8 Deluxe v4.0.0 updated Pia to v5.9.6, previously v5.9.0.

This section documents vulnerabilities for ENL . A framework used by Nintendo games including Mario Kart 8 Deluxe, Splatoon 2 / 3, Mario Maker 2, and more.

Fun fact, this library appears to re-use network code and concepts from older Nintendo titles such as Mario Kart 7 and some Wii multiplayer games.

The function will try to fetch the content transporter by ID using enl::TransportManager::getContentTransporter(unsigned char const &) , it returns NULL if there's no content transporter with the same ID

NOTE: The function may be inlined

Then it will try to call a virtual method: virtual size_t readyReceiveStream(enl::RamReadStream&, enl::Buffer*, size_t) , dereferencing the pointer to fetch the vtable ptr

Pseudocode of the function before it was fixed

There's another one more interesting but it will have to wait a bit :)

Note: there is more uninitialized data in the packet, but the memory address is probably the most interesting part. The vulnerability was fixed by clearing the application data with zeros, before filling in the information.

This stack infoleak was also present in the ldn AdvertiseData.

With v1.1.1 that data is now cleared.

Extracted Entities

Domains (1)