Summary
During the regular boot sequence, Huawei’s BootROM initializes the UFS hardware and the crypto engine in order to load and verify the next stage bootloader image from flash. However, when run in download mode, which maybe used for factory flashing and repair purposes, a connected host can communicate with the BootROM via USB.
In this case, the BootROM acts as a USB1.1 Serial-over-USB gadget, with a single data endpoint.
Based on kernel sources, the USB device appears to be a Synopsys DesignWare USB 3.0 controller.
Although the implementation of the USB stack in the Linux kernel and the BootROM is quite different (latter is orders of magnitude simpler), the offsets and the register map of the device can be learned from (drivers/usb/dwc3
):
# lsusb -d 12d1:3609 -v
Bus 003 Device 020: ID 12d1:3609 Huawei Technologies Co., Ltd. USB SER
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x12d1 Huawei Technologies Co., Ltd.
(...)
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x01 EP 1 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
Although the USB1.1 can transfer maximum 64 bytes a time (wMaxPacketSize
), a USB BULK OUT transaction may consist of multiple 64 byte transfers. These transactions are used to implement a custom Huawei protocol that runs on top of the serial layer (outside the context of this advisory).
These longer transactions get accumulated in a designated buffer, which in the case of Kirin 990 is 0x600
bytes in size.
The vulnerability lies in the BULK OUT chunk assembler part of the USB stack.
The USB endpoint transfer complete event handling function is located at 0x4aac
in the Kirin 990 SoC.
The relevant code snippet can be seen below.
void dwc3_ep_xfer_complete(usb_t *usb, endpoint_t *endpoint) {
unsigned int recv_buf_size;
unsigned int curr_trans_size;
unsigned short max_packet_size;
...
if (endpoint->direction == OUT) {
curr_trans_size = endpoint->transfer_size - (trb->status & 0xffffff);
recv_buf_size = usb->recv_buf_size;
max_packet_size = endpoint->max_packet_size;
endpoint->recv_size = endpoint->recv_size + curr_trans_size;
endpoint->trb_data = endpoint->trb_data + curr_trans_size;
usb->recv_buf_size = recv_buf_size + curr_trans_size;
endpoint->out_resource_index = 0;
if (
(max_packet_size <= curr_trans_size) &&
(usb->recv_buf_size - 5 < usb->file_download_length) // <<< BUG
) {
usb_prepare_for_bulk_in(usb, &usb->ep1);
return;
}
}
...
// callback here is the higher level protocol handler
if (endpoint->callback)
endpoint->callback(usb);
endpoint->recv_size = 0;
}
The curr_trans_size
denotes the currently received data size which is 64 at most.
Then, with the amount of data currently received the size accumulators (endpoint->recv_size
, usb->recv_buf_size
) are updated.
Also the endpoint ->trb_data
data pointer (which initially points to the beginning of the protocol message buffer) gets incremented by the current transfer size.
The BULK OUT transfer accumulation stops only when the received packet is not 64 bytes in length, which correctly reflects the USB specification.
When the BULK OUT transfer stops, the callback function for the higher layer protocol is called.
First let’s interpret the normal working of the condition around the second if.
The first half of the condition clearly checks for the USB specification transfer-finish condition.
The second half however is quite obscure.
The usb->file_download_length
value is zeroed at the initialization before entering the USB download functionality, and only gets updated from the download file length.
That means that at least during the first protocol message, the usb->recv_buf_size - 5 < usb->file_download_length
condition can not be satisfied, as the left-hand side is unsigned-compared to less than zero.
As this condition is not met, the execution flows to the callback function, with maximum 64 bytes already in place at the message buffer.
But in Huawei’s upper layer protocol, the first expected valid protocol messages comfortably fit into 64 bytes, thus even though that comparison is guaranteed to fail, the download functionality is not impacted by it.
The usb->file_download_length
field stores the total length of the subsequent download size, which is considerably greater than the designated message buffer size, which is 0x600
bytes: typical second stage bootloader image size is around 100kB.
Furthermore, neither the USB specification nor the BootROM USB implementation imposes the 0x600
byte limitation on the transfer size.
That means that with completely specification-compliant USB messages we can write over the bounds of the BULK OUT accumulation message buffer, once the usb->file_download_length
field is set to a sufficiently large (>0x600
) value.
Turning this bug into full code execution can be performed in two steps: first turn it into an arbitrary write, and second directly overwrite the stack. The sequential overwrite can reach various control structures that provide convenient ways to achieve the first. For more details, please see our upcoming BlackHat presentation.
Affected Devices (Verified)
-
Kirin 990
- Huawei Mate 30 Pro (LIO)
- Huawei P40 Pro (ELS)
- Huawei P40 (ANA)
-
Kirin 980
- Huawei Nova 5T (YAL)
Fix
Huawei security update June 2021 fixes this vulnerability.
Timeline
- 2021.05.20. Bug reported to Huawei PSIRT
- 2021.06.15. Huawei PSIRT confirms vulnerability, assigns CVE-2021-22429, confirms fix plans
- 2021.06.29. OTA distribution of the fix mitigating the vulnerability for Kirin 990 chipset based devices starts
- 2021.07.08. Huawei confirms that the security bulletin for the issue has been released