USB Fuzzing Basics: From fuzzing to bug reporting

We recently begun to search bugs in USB host stacks using one of our tool based on the Facedancer. This article first presents our fuzzing approach followed by a practical example of a bug in Windows 8.1 x64 full-updated. The goal of this article is not to redefine state-of-the-art USB fuzzing, nor to give a full description of our fuzzing architecture, but rather to narrate a scenario which starts from fuzzing and ends up with a bug report.

Fuzzing approach

Our fuzzing architecture is based on a Facedancer [1] and Umap tool [2] to which we added some features:

  • Traffic capture in PCAP for the emulated device;
  • Traffic replay from a recorded PCAP;
  • Packet mutation based on Radamsa [3].

USB basics

The goal of the article is not to describe how USB works in detail, but some knowledge is still required for a better understanding. When a device is connected, the host issues standard requests [4] to the device to retrieve information (vendor id, product id, available features, ...) about it. It does so in order to configure it and to load the appropriate driver(s) into the OS. This information is called descriptors [5]. These requests/descriptors are exchanged on the special endpoint 0: every new standard device connected must respond to requests sent to it. Endpoints are logical links between a device interface and the USB host stack. An interface is composed with one or more endpoints, and offers class functions (HID, mass storage, ...) or specific functions.

A fuzzing instance example

We emulated a USB mass storage device and dumped the traffic exchanged. Then, we decided to fuzz the configuration descriptor, and particularly the bNumEndpoints field.

The mutation simply consisted in replacing this byte by a random one. After some time, we triggered a BSOD on Windows 8.1 x64. Here, our mutated descriptor is sent to the host in the packet framed in red. After replaying several times the packets sequence with the mutated descriptor, we surmised that the BSOD was triggered just after the host sent the Set Configuration request framed in orange.

Device enumeration

In Wireshark, the mutated descriptor looks like:

Mutated descriptor

The crashdump analysis was pretty much useless because the kernel pool memory was corrupted: every time, it crashed at a different location. We desperatly continued to inject the packet and at some point, Windows BSOD gave us the following problem location: USBSTOR.sys.

The driver name is explicit: it is the mass storage driver. So we looked into it.

Reversing the mass storage driver

After downloading the symbols of USBSTOR.sys, we loaded it into IDA Pro. Luckily, the symbols are easily comprehensible and we quickly found the interesting function: USBSTOR_SelectConfiguration()

The first basic block shows a call to an export of usbd.sys: USBD_CreateConfigurationRequestEx() that returns a pointer to a URB_FUNCTION_SELECT_CONFIGURATION structure. According to the MSDN [6], this "routine allocates and formats a URB to select a configuration for a USB device". URBs are structures used by client drivers to describe the request they want to send to devices [7].

The second basic block does a call to USBSTOR_SyncSendUsbRequest() and takes as first parameter the URB previously created. When the call to this function is made, the request is sent through the USB stack and then physically from the host controller to the device. If we break on the USBSTOR_SyncSendUsbRequest() call, we observe that it is not this call that crashes the system.

Basic block 1

If we look into the USBD_CreateConfigurationRequestEx() function, we see that it copies the bNumEndpoints field (that we set to 0 during the fuzzing) from the USB_INTERFACE_DESCRIPTOR structure to the NumberOfPipes field of the USBD_INTERFACE_INFORMATION structure. The USB_INTERFACE_DESCRIPTOR structure was initialized during the enumeration process and will not be studied in this article.

Basic block 2

Now, we go back to USBSTOR.sys, after the call to USBD_CreateConfigurationRequestEx(). RDI points to:

struct _URB_SELECT_CONFIGURATION {
  struct URB_HEADER  Hdr;
  PUSB_CONFIGURATION_DESCRIPTOR ConfigurationDescriptor;
  USBD_CONFIGURATION_HANDLE     ConfigurationHandle;
  USBD_INTERFACE_INFORMATION    Interface;
};

and R14 points to:

typedef struct _USBD_INTERFACE_INFORMATION {
  USHORT                Length;
  UCHAR                 InterfaceNumber;
  UCHAR                 AlternateSetting;
  UCHAR                 Class;
  UCHAR                 SubClass;
  UCHAR                 Protocol;
  UCHAR                 Reserved;
  USBD_INTERFACE_HANDLE InterfaceHandle;
  ULONG                 NumberOfPipes; // Our bNumEndpoints = 0 is here !
  USBD_PIPE_INFORMATION Pipes[1];
} USBD_INTERFACE_INFORMATION, *PUSBD_INTERFACE_INFORMATION;
Basic block 3

Now, the _USBD_INTERFACE_INFORMATION structure is copied to RCX. Few instructions later, we get back this pointer into RAX.

Basic block 3

The pseudocode of these instructions is:

ECX <- endpoint number                  If the number of endpoint is zero
ECX <- ECX-1                            ECX <- 0-1 = 0xffffffff
R8 <- (RCX*3*8)+80                      R8->(0xffffffff*3*8)+80
memset(@dest, 0x0, R8)                  memset(@dest, 0x0, 0x1800000038)

Here, we have a memset with a size that equals to 0x1800000038. This has for consequence a non exploitable kernel pool overflow.

Windows 8.1 32-bit

We saw what happened in 64-bit mode, but not in 32-bit. We will not detail again the instruction flow as it is exactly the same. The following code snippet corresponds to the 32-bit equivalent of the previous code snippet.

Basic block 4

In pseudocode, the size for the memset() looks like:

EAX <- endpoint number                  If the number of endpoint is zero
EAX <- EAX-1                            EAX <- 0-1 = 0xffffffff
EAX <- (EAX*0x14)+0x38                  EAX->(0xffffffff*0x14)+0x38 = 0x24
memset(@dest, 0x0, EAX)                 memset(@dest, 0x0, 0x24)

Here, the size calculation is different because the pointer size in the structures is different. Because EAX is only 32 bits long, the result 0x1400000024 does not fit into it, hence 0x00000024 is stored. The size of an _URB_SELECT_CONFIGURATION in 0x38 bytes, so 20 bytes of the allocated structure are not initialized. It could have been exploitable under specific conditions if the allocated space was not correctly filled with a memcpy() just after.

Bug report to Microsoft

The bugs have been reported to Microsoft. They do not consider this local DoS as a security issue since it requires a physical access.

Acknowledgments

Thanks to the QuarksLab team for their help on reverse engineering and their reviews!

Comments