Technical Advisory – ARM MbedOS USB Mass Storage Driver Memory Corruption

Vendor: ARM
Vendor URL: https://os.mbed.com/
Versions affected: Prior to 5.15.2
Systems Affected: ARM Mbed OS
Author: Ilya Zhuravlev
Risk: High

Summary:

The ARM Mbed operating system contains a USB Mass Storage driver (USBMD), which allows emulation of a mass storage device over USB. This driver contains a three (3) memory safety vulnerabilities, allowing adversaries with physical access to corrupt kernel memory or disclose kernel memory contents.

Location:

drivers/source/usb/USBMSD.cpp

Impact:

When the USB mass storage driver is enabled in devices that run the Mbed OS, an adversary with physical access is able to corrupt and disclose kernel memory contents, potentially leading to code execution.

Details:

The code in USBMSD.cpp is responsible for processing SCSI commands sent over USB and responding to them. The following sections describe three vulnerabilities that arise due to how these commands are handled.

Vulnerability #1

If, at the start of a transfer, the base address (_addr) is set up to be greater than the total size of the block device (_memory_size), when the USB mass storage driver attempts to adjust the read or write size, an integer underflow will occur, as shown in the below code snippet taken from the memoryRead function:

void USBMSD::memoryRead(void)
{
    uint32_t n;
    n = (_length > MAX_PACKET) ? MAX_PACKET : _length;
    if ((_addr + n) > _memory_size) {
        n = _memory_size - _addr;
        _stage = ERROR;
    }
    // we read an entire block
    if (!(_addr % _block_size)) {
        disk_read(_page, _addr / _block_size, 1);
    }
    // write data which are in RAM
    _write_next(&_page[_addr % _block_size], MAX_PACKET);
    _addr += n;
    _length -= n;
    _csw.DataResidue -= n;
    if (!_length || (_stage != PROCESS_CBW)) {
        _csw.Status = (_stage == PROCESS_CBW) ? CSW_PASSED : CSW_FAILED;
        _stage = (_stage == PROCESS_CBW) ? SEND_CSW : _stage;
    }
}

The code then calls disk_read, which eventually calls the read method of the underlying BlockDevice class. Depending on the underlying implementation this may lead to a read of memory past the end of the block device’s buffers.

Furthermore, when disk_read returns, this function also fails to handle the case where _addr is greater than _memory_size. If _addr is greater than _memory_size, after the size check fails, an underflow occurs when the value of n is calculated which is assigned a large value. At the end of the function, a failure flag is set into _csw which is later sent to the host.

Vulnerability #2:

The WRITE10 and WRITE12 commands are implemented by the memoryWrite function. As the size of the USB packet, MAX_PACKET (64), is much less than the storage block size, before the data is flushed to the underlying storage, this function accumulates it in the _page[] buffer (size determined by memory geometry, but likely at least 512 bytes). The relevant part is reproduced below:

void USBMSD::memoryWrite(uint8_t *buf, uint16_t size)
{
    if ((_addr + size) > _memory_size) {
        size = _memory_size - _addr;
        _stage = ERROR;
        endpoint_stall(_bulk_out);
     }
    // we fill an array in RAM of 1 block before writing it in memory
    for (int i = 0; i < size; i++) {
        _page[_addr % _block_size + i] = buf[i];
    }
    // if the array is filled, write it in memory
    if (!((_addr + size) % _block_size)) {
        if (!(disk_status() & WRITE_PROTECT)) {
            disk_write(_page, _addr / _block_size, 1);
        }
    }
    _addr += size;
    _length -= size;
    _csw.DataResidue -= size;
    if ((!_length) || (_stage != PROCESS_CBW)) {
        _csw.Status = (_stage == ERROR) ? CSW_FAILED : CSW_PASSED;
        sendCSW();
    }
}

If at function entry _addr is misaligned, e.g. 511, then during the copy from buf into _page it could overflow the _page array. Specifically, with the max USB payload size being 0x40 bytes, the copy would overflow by up to 0x3F bytes.

The exploitability of the issue would then depend on the exact layout of object variables generated by the compiler.

Vulnerability #3:

The WRITE10 and WRITE12 commands are implemented by the memoryWrite function, while the VERIFY10 command is implemented by the memoryVerify function. Both functions deal with data sent in by the host and, in the case of memoryWrite, this data is written to the underlying storage, while in the case of the memoryVerify function, the data is compared with the existing contents of the storage.

The data transfer starts with the infoTransfer function, which parses the Command Block included within the Command Block Wrapper and extracts address and total length of the transfer. Then, as new data comes in over USB, either memoryWrite or memoryVerify are executed. Both of these functions have the same code to deal with invalid input, however in both places the input sanitization checks are performed improperly:

if ((_addr + size) > _memory_size) {
    size = _memory_size - _addr;
    _stage = ERROR;
    endpoint_stall(_bulk_out);
}

The code above attempts to limit the size of the incoming data so that the total does not exceed _memory_size. Both _addr and size are controlled by the attacker, with _addr being an arbitrary value aligned to 512 bytes, and size being an arbitrary value up to 64. In the case where _addr is greater than _memory_size, the calculated size would underflow and, being an unsigned 16-bit variable, can become a value up to 0xFE00.

Then, in case of memoryWrite the data is written into a temporary _page buffer as follows:

// we fill an array in RAM of 1 block before writing it in memory
for (int i = 0; i < size; i++) {
    _page[_addr % _block_size + i] = buf[i];
}

When size is greater than MAX_PACKET (64), reading from buf[i] would reference out-of-bounds memory. When size is greater than _block_size (memory geometry dependent, but likely at least 512), writing to _page[_addr%_block_size+i] would write out-of-bounds into the object’s memory.

The issue could then be exploited either to leak stack memory contents (by setting the size between 64 and 512 bytes), or to corrupt global kernel memory (by setting the size larger than 512 bytes). As the contents of a stack buffer buf past index 64 are not directly controlled by the attacker, the exploitation of the memory corruption issue is non-trivial and might be impossible, depending on the exact memory layout.

Recommendation:

Upgrade to the v5.15.2 LTS for MbedOS.

Vendor Communication:

  • Feb 17, 2020: Realization that NCC-ZEP-024, NCC-ZEP-025, NCC-ZEP-026 also affect MbedOS.
  • Feb 23, 2020: Initial contact with ARM to identify security contact.
  • Mar 03, 2020: Full details of the 3 issues disclosed to ARM security team.
  • Mar 04, 2020: ARM acknowledged receipt of issues.
  • Mar 04, 2020: Meeting with ARM to discuss details and answer questions.
  • Mar 24, 2020: Reached out to ARM for updates on remediation.
  • Mar 24, 2020: ARM responds with a link to the fixes (master branch).
  • Apr 01, 2020: ARM confirms fixes were merged (backported to v5.15).
  • Apr 01, 2020: NCC Group asked if there are CVE numbers assigned.
  • Apr 23, 2020: NCC Group asked again if there are CVE numbers assigned.
  • May 28, 2020: NCC Group advised ARM that we plan to publish an advisory.
  • June 9, 2020: ARM confirms that CVE numbers will not be assigned.
  • June 11, 2020: Publication date of this advisory.

Thanks to:

Rob Wood, for assisting with the disclosure process.

About NCC Group:

NCC Group is a global expert in cyber security and risk mitigation, working with businesses to protect their brand, value and reputation against the ever-evolving threat landscape.

With our knowledge, experience and global footprint, we are best placed to help businesses identify, assess, mitigate and respond to the risks they face.

We are passionate about making the Internet safer and revolutionizing the way in which organizations think about cybersecurity.