## Vulnerability Disclosure: Fusée Gelée This report documents Fusée Gelée, a coldboot vulnerability that allows full, unauthenticated arbitrary code execution from an early bootROM context via Tegra Recovery Mode (RCM) on NVIDIA's Tegra line of embedded processors. As this vulnerability allows arbitrary code execution on the Boot and Power Management Processor (BPMP) before any lock-outs take effect, this vulnerability compromises the entire root-of-trust for each processor, and allows exfiltration of secrets e.g. burned into device fuses. Quick vitals:                     | | --------------------|-------------------------------------------------------- *Reporter:* | Katherine Temkin (@ktemkin) *Affiliation:* | ReSwitched (https://reswitched.tech) *E-mail:* | k@ktemkin.com *Affects:* | Tegra SoCs, independent of software stack *Versions:* | believed to affect Tegra SoCs released prior to the T186 / X2 *Impact:* | early bootROM code execution with no software requirements, which can lead to full compromise of on-device secrets where USB access is possible *Disclosure* | public disclosure planned for June 15th, 2018 #### Vulnerability Summary 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). #### Public Disclosure Notice This vulnerability is notable due to the significant number and variety of devices affected, the severity of the issue, and the immutability of the relevant code on devices already delivered to end users. This vulnerability report is provided as a courtesy to help aid remediation efforts, guide communication, and minimize impact to users. As other groups appear to have this or an equivalent exploit-- [including a group who claims they will be selling access to an implementation of such an exploit](http://team-xecuter.com/team-xecuter-coming-to-your-nintendo-switch-console/)-- it is the author and the ReSwitched team's belief that prompt public disclosure best serves the public interest. By minimizing the information asymmetry between the general public and exploit-holders and notifying the public, users will be able to best assess how this vulnerability impacts their personal threat models. Accordingly, ReSwitched anticipates public disclosure of this vulnerability: * If another group releases an implementation of the identified vulnerability; or * On June 15th, 2018, whichever comes first. ### Vulnerability Details The core of the Tegra boot process is approximated by the following block of pseudo-code, as obtained by reverse-engineering an IROM extracted from a vulnerable T210 system: ```C // If this is a warmboot (from "sleep"), restore the saved state from RAM. if (read_scratch0_bit(1)) { restore_warmboot_image(&load_addr); } // Otherwise, bootstrap the processor. else { // Allow recovery mode to be forced by a PMC scratch bit or physical straps. force_recovery = check_for_rcm_straps() || read_scratch0_bit(2); // Determine whether to use USB2 or USB3 for RCM. determine_rcm_usb_version(&usb_version); usb_ops = set_up_usb_ops(usb_version); usb_ops->initialize(); // If we're not forcing recovery, attempt to load an image from boot media. if (!force_recovery) { // If we succeeded, don't fall back into recovery mode. if (read_boot_configuration_and_images(&load_addr) == SUCCESS) { goto boot_complete; } } // In all other conditions if (read_boot_images_via_usb_rcm(, &load_addr) != SUCCESS) { /* load address is poisoned here */ } } boot_complete: /* apply lock-outs, and boot the program at address load_address */ ``` Tegra processors include a USB Recovery Mode (RCM), which we can observe to be activated under a number of conditions: * If the processor fails to find a valid Boot Control Table (BCT) + bootloader on its boot media; * If processor straps are pulled to a particular value e.g. by holding a button combination; or * If the processor is rebooted after a particular value is written into a power management controller scratch register. USB recovery mode is present in all devices, including devices that have been production secured. To ensure that USB recovery mode does not allow unauthenticated communications, RCM requires all recovery commands be signed using either RSA or via AES-CMAC. The bootloader's implementation of the Tegra RCM protocol is simple, and exists to allow loading a small piece of code (called the *miniloader* or *applet*) into the bootloader's local Instruction RAM (IRAM). In a typical application, this *applet* is `nvtboot-recovery`, a stub which allows further USB communications to bootstrap a system or to allow system provisioning. The RCM process is approximated by the following pseudo-code, again obtained via reverse engineering a dumped IROM from a T210: ```C // Significantly simplified for clarity, with error checking omitted where unimportant. while (1) { // Repeatedly handle USB standard events on the control endpoint EP0. usb_ops->handle_control_requests(current_dma_buffer); // Try to send the device ID over the main USB data pipe until we succeed. if ( rcm_send_device_id() == USB_NOT_CONFIGURED ) { usb_initialized = 0; } // Once we've made a USB connection, accept RCM commands on EP1. else { usb_initialized = 1; // Read a full RCM command and any associated payload into a global buffer. // (Error checking omitted for brevity.) rcm_read_command_and_payload(); // Validate the received RCM command; e.g. by checking for signatures // in RSA or AES_CMAC mode, or by trivially succeeding if we're not in // a secure mode. rc = rcm_validate_command(); if (rc != VALIDATION_PASS) { return rc; } // Handle the received and validated command. // For a "load miniloader" command, this sanity checks the (validated) // miniloader image and takes steps to prevent re-use of signed data not // intended to be used as an RCM command. rcm_handle_command_complete(...); } } ``` It is important to note that a full RCM command *and its associated payload* are read into 1) a global buffer, and 2) the target load address, respectively, before any signature checking is done. This effectively grants the attacker a narrow window in which they control a large region of unvalidated memory. The largest vulnerability surface area occurs in the `rcm_read_command_and_payload` function, which accepts the RCM command and payload packets via a USB bulk endpoint. For our purposes, this endpoint is essentially a simple pipe for conveyance of blocks of binary data separate from standard USB communications. The `rcm_read_command_and_payload` function actually contains several issues-- of which exactly one is known to be exploitable: ```C uint32_t total_rxd = 0; uint32_t total_to_rx = 0x400; // Loop until we've received our full command and payload. while (total_rxd < total_to_rx) { // Switch between two DMA buffers, so the USB is never DMA'ing into the same // buffer that we're processing. active_buffer = next_buffer; next_buffer = switch_dma_buffers(); // Start a USB DMA transaction on the RCM bulk endpoint, which will hopefully // receive data from the host in the background as we copy. usb_ops->start_nonblocking_bulk_read(active_buffer, 0x1000); // If we're in the first 680-bytes we're receiving, this is part of the RCM // command, and we should read it into the command buffer. if ( total_rxd < 680 ) { /* copy data from the DMA buffer into the RCM command buffer until we've read a full 680-byte RCM command */ // Once we've received the first four bytes of the RCM command, // use that to figure out how much data should be received. if ( total_rxd >= 4 ) { // validate: // -- the command won't exceed our total RAM // (680 here, 0x30000 in upper IRAM) // -- the command is >= 0x400 bytes // -- the size ends in 8 if ( rcm_command_buffer[0] >= 0x302A8u || rcm_command_buffer[0] < 0x400u || (rcm_command_buffer[0] & 0xF) != 8 ) { return ERROR_INVALID_SIZE; } else { left_to_rx = *((uint32_t *)rcm_command_buffer); } } } /* copy any data _past_ the command into a separate payload buffer at 0x40010000 */ /* -code omitted for brevity - */ // Wait for the DMA transaction to complete. // [This is, again, simplified to convey concepts.] while(!usb_ops->bulk_read_complete()) { // While we're blocking, it's still important that we respond to standard // USB packets on the control endpoint, so do that here. usb_ops->handle_control_requests(next_buffer); } } ``` Astute readers will notice an issue unrelated to the Fusée Gelée exploit: this code fails to properly ensure DMA buffers are being used exclusively for a single operation. This results in an interesting race condition in which a DMA buffer can be simultaneously used to handle a control request and a RCM bulk transfer. This can break the flow of RCM, but as both operations contain untrusted data, this issue poses no security risk. To find the actual vulnerability, we must delve deeper, into the code that handles standard USB control requests. The core of this code is responsible for responding to USB control requests. A *control request* is initiated when the host sends a setup packet, of the following form: Field |         Size    | Description ----------|:----:|----- direction | 1b | if '1', the device should respond with data type | 2b | specifies whether this request is of a standard type or not recipient | 5b | encodes the context in which this request should be considered;
for example, is this about a `DEVICE` or about an `ENDPOINT`? request | 8b | specifies the request number value | 16b | argument to the request index | 16b | argument to the request length | 16b | specifies the maximum amount of data to be transferred As an example, the host can request the status of a device by issuing a `GET_STATUS` request, at which point the device would be expected to respond with a short setup packet. Of particular note is the `length` field of the request, which should *limit* -- but not exclusively determine-- the *maximum* amount of data that should be included in the response. Per the specification, the device should respond with either the *amount of data specified* or the *amount of data available*, whichever is less. The bootloader's implementation of this behavior is conceptually implemented as follows: ```C // Temporary, automatic variables, located on the stack. uint16_t status; void *data_to_tx; // The amount of data available to transmit. uint16_t size_to_tx = 0; // The amount of data the USB host requested. uint16_t length_read = setup_packet.length; /* Lots of handler cases have omitted for brevity. */ // Handle GET_STATUS requests. if (setup_packet.request == REQUEST_GET_STATUS) { // If this is asking for the DEVICE's status, respond accordingly. if(setup_packet.recipient == RECIPIENT_DEVICE) { status = get_usb_device_status(); size_to_tx = sizeof(status); } // Otherwise, respond with the ENDPOINT status. else if (setup_packet.recipient == RECIPIENT_ENDPOINT){ status = get_usb_endpoint_status(setup_packet.index); size_to_tx = length_read; // <-- This is a critical error! } else { /* ... */ } // Send the status value, which we'll copy from the stack variable 'status'. data_to_tx = &status; } // Copy the data we have into our DMA buffer for transmission. // For a GET_STATUS request, this copies data from the stack into our DMA buffer. memcpy(dma_buffer, data_to_tx, size_to_tx); // If the host requested less data than we have, only send the amount requested. // This effectively selects min(size_to_tx, length_read). if (length_read < size_to_tx) { size_to_tx = length_read; } // Transmit the response we've constructed back to the host. respond_to_control_request(dma_buffer, length_to_send); ``` In most cases, the handler correctly limits the length of the transmitted responses to the amount it has available, per the USB specification. However, in a few notable cases, the length is *incorrectly always set to the amount requested* by the host: * When issuing a `GET_CONFIGURATION` request with a `DEVICE` recipient. * When issuing a `GET_INTERFACE` request with a `INTERFACE` recipient. * When issuing a `GET_STATUS` request with a `ENDPOINT` recipient. This is a critical security error, as the host can request up to 65,535 bytes per control request. In cases where this is loaded directly into `size_to_tx`, this value directly sets the extent of the `memcpy` that follows-- and thus can copy up to 65,535 bytes into the currently selected `dma_buffer`. As the DMA buffers used for the USB stack are each comparatively short, this can result in a _very_ significant buffer overflow. To validate that the vulnerability is present on a given device, one can try issuing an oversized request and watch as the device responds. Pictured below is the response generated when sending a oversized `GET_STATUS` control request with an `ENDPOINT` recipient to a T124: ![Reading a chunk of stack memory from a K1](stack_read.png) A compliant device should generate a two-byte response to a `GET_STATUS` request-- but the affected Tegra responds with significantly longer response. This is a clear indication that we've run into the vulnerability described above. To really understand the impact of this vulnerability, it helps to understand the memory layout used by the bootROM. For our proof-of-concept, we'll consider the layout used by the T210 variant of the affected bootROM: ![Bootrom memory layout](mem_layout.png) The major memory regions relevant to this vulnerability are as follows: * The bootROM's *execution stack* grows downward from `0x40010000`; so the execution stack is located in the memory *immediately preceding* that address. * The DMA buffers used for USB are located at `0x40005000` and `0x40009000`, respectively. Because the USB stack alternates between these two buffers once per USB transfer, the host effectively can control which DMA buffer is in use by sending USB transfers. * Once the bootloader's RCM code receives a 680-byte command, it begins to store received data in a section of upper IRAM located at address `0x40010000`, and can store up to `0x30000` bytes of payload. This address is notable, as it is immediately past the end of the active execution stack. Of particular note is the adjacency of the bootROM's *execution stack* and the attacker-controlled *RCM payload*. Consider the behavior of the previous pseudo-code segment on receipt of a `GET_STATUS` request to the `ENDPOINT` with an excessive length. The resulting memcpy: * copies *up to* 65,535 bytes total; * sources data from a region *starting at the status variable on the stack* and extending significantly past the stack -- effectively copying mostly *from the attacker-controllable RCM payload buffer* * targets a buffer starting either `0x40005000` or `0x40009000`, at the attacker's discretion, reaching addresses of up to `0x40014fff` or `0x40018fff` This is a powerful copy primitive, as it copies *from attacker controlled memory* and into a region that *includes the entire execution stack*: ![Effect of the vulnerability memcpy](copy_span.png) This would be a powerful exploit on any platform; but this is a particularly devastating attack in the bootROM environment, which does not: * Use common attack mitigations such as stack canaries, ostensibly to reduce complexity and save limited IRAM and IROM space. * Apply memory protections, so the entire stack and all attacker controlled buffers can be read from, written to, and executed from. * Employ typical 'application-processor' mitigation strategies such as ASLR. Accordingly, we now have: 1. The capability to load arbitrary payloads into memory via RCM, as RCM only validates command signatures once payload receipt is complete. 2. The ability to copy attacker-controlled values over the execution stack, overwriting return addresses and redirecting execution to a location of our choice. Together, these two abilities give us a full arbitrary-code execution exploit at a critical point in the Tegra's start-up process. As control flow is hijacked before return from `read_boot_images_via_usb_rcm`, none of the "lock-out" operations that precede normal startup are executed. This means, for example, that the T210 fuses-- and the keydata stored within them-- are accessible from the attack payload, and the bootROM is not yet protected. #### Exploit Execution The Fusée Launcher PoC exploits the vulnerability described on the T210 via a careful sequence of interactions: 1. The device is started in RCM mode. Device specifics will differ, but this is often via a key-combination held on startup. 2. A host computer is allowed to enumerate the RCM device normally. 3. The host reads the RCM device's ID by reading 16 bytes from the EP1 IN. 4. The host builds an exploit payload, which is comprised of: 1. An RCM command that includes a maximum length, ensuring that we can send as much payload as possible without completing receipt of the RCM payload. Only the length of this command is used prior to validation; so we can submit an RCM command that starts with a maximum length of 0x30298, but which fills the remaining 676 bytes of the RCM command with any value. 2. A set of values with which to overwrite the stack. As stack return address locations vary across the series, it's recommended that a large block composed of a single entry-point address be repeated a significant number of times, so one can effectively replace the entire stack with that address. 3. The program to be executed ("final payload") is appended, ensuring that its position in the binary matches the entry-point from the previous step. 4. The payload is padded to be evenly divisible by the 0x1000 block size to ensure the active block is not overwritten by the "DMA dual-use" bug described above. 5. The exploit payload is sent to the device over EP1 OUT, tracking the number of 0x1000-byte "blocks" that have been sent to the device. If this number is _even_, the next write will be issued to the lower DMA buffer (`0x40005000`); otherwise, it will be issued to the upper DMA buffer (`0x40009000`). 6. If the next write would target the lower DMA buffer, issue another write of a full 0x1000 bytes to move the target to the upper DMA buffer, reducing the total amount of data to be copied. 7. Trigger the vulnerable memcpy by sending a `GET_STATUS` `IN` control request with an `ENDPOINT` recipient, and a length long enough to smash the desired stack region, and preferably not longer than required. A simple host program that triggers this vulnerability is included with this report: see `fusee-launcher.py`. Note the restrictions on its function in the following section. ### Proof of Concept Included with this report is a set of three files: * `fusee-launcher.py` -- The main proof-of-concept accompanying this report. This python script is designed to launch a simple binary payload in the described bootROM context via the exploit. * `intermezzo.bin` -- This small stub is designed to relocate a payload from a higher load address to the standard RCM load address of `0x40010000`. This allows standard RCM payloads (such as `nvtboot-recover.bin`) to be executed. * `fusee.bin` -- An example payload for the Nintendo Switch, a representative and well-secured device based on a T210. This payload will print information from the device's fuses and protected IROM to the display, demonstrating that early bootROM execution has been achieved. **Support note:** Many host-OS driver stacks are reluctant to issue unreasonably large control requests. Accordingly, the current proof-of-concept includes code designed to work in the following environments: * **64-bit linux via `xhci_hcd`**. The proof-of-concept can manually submit large control requests, but does not work with the common `ehci_hcd` drivers due to driver limitations. A rough rule of thumb is that a connection via a blue / USB3 SuperSpeed port will almost always be handled by `xhci_hcd`. * **macOS**. The exploit works out of the box with no surprises or restrictions on modern macOS. Windows support would require addition of a custom kernel module, and thus was beyond the scope of a simple proof-of-concept. To use this proof-of-concept on a Nintendo Switch: 1. Set up an Linux or macOS environment that meets the criteria above, and which has a working `python3` and `pyusb` as well as `libusb` installed. 2. Connect the Switch to your host PC with a USB A -> USB C cable. 3. Boot the Switch in RCM mode. There are three ways to do this, but the first-- unseating its eMMC board-- is likely the most straightforward: 1. Ensure the Switch cannot boot off its eMMC. The most straightforward way to to this is to open the back cover and remove the socketed eMMC board; corrupting the BCT or bootloader on the eMMC boot partition would also work. 2. Trigger the RCM straps. Hold VOL_UP and short pin 10 on the right JoyCon connector to ground while engaging the power button. 3. Set bit 2 of PMC scratch register zero. On modern firmwares, this requires EL3 or pre-sleep BPMP execution. 4. Run the `fusee-launcher.py` with an argument of `fusee.bin`. (This requires `intermezzo.bin` to be located in the same folder as `fusee-launcher.py`.) ``` sudo python3 ./fusee-launcher.py fusee.bin ``` If everything functions correctly, your Switch should be displaying a collection of fuse and protected-IROM information: ![exploit working](switch_hax.jpg) ### Recommended Mitigations In this case, the recommended mitigation is to correct the USB control request handler such that it always correctly constrains the length to be transmitted. This has to be handled according to the type of device: * **For a device already in consumer hands**, no solution is proposed. Unfortunately, access to the fuses needed to configure the device's ipatches was blocked when the ODM_PRODUCTION fuse was burned, so no bootROM update is possible. It is suggested that consumers be made aware of the situation so they can move to other devices, where possible. * **For new devices**, the correct solution is likely to introduce an new ipatch or new ipatches that limits the size of control request responses. It seems likely that OEMs producing T210-based devices may move to T214 solutions; it is the hope of the author that the T214's bootROM shares immunity with the T186. If not, patching the above is a recommended modification to the mask ROM and/or ipatches of the T214, as well.