Analysis of Windows USB Descriptor Vulnerability - MS13-081 (CVE-2013-3200)

February 27, 2015

Occasionally we receive requests to develop Core Impact modules for specific vulnerabilities. Here, I'd like to dive into what that process looked like for CVE-2013-3200, Windows USB vulnerability included in MS13-081 bulletin a.k.a. Windows USB Descriptor Vulnerability. The vulnerability allows physically proximate attackers to execute arbitrary code by connecting a crafted USB device. Crafty, eh?

You may be wondering why someone would look to exploit this vulnerability when other options (like the Arduino-based attack) are available. Well, in this case, the target machine doesn't have to be unlocked for the attack to be successful.

Finding the Bug

I decided to look for the bug on the Windows 7 SP1 x86 patches, so I downloaded all of them and began the binary diffing process. I focused on the USB related drivers, and ignored drivers that were patched to fix other 6 kernel related vulnerabilities included in the same bulletin.

I used TurboDiff to diff almost all of the drivers included in the patch. Here are some notes I took about all the modified functions:

Hidparse.sys

Changed:
HidP_AllocateCollections.

Hidclass.sys

Changed:
HidpIdleTimeWorker(x,x)

Suspicious:
HidpCheckIdleState(x,x)
HidpCheckPowerState(x,x)
HidpSetDeviceBusy(x)
HidpStartIdleTimeout(x,x)
HidpCancelIdleNotification(x,x)

Hidusb.sys

No changed functions nor suspicious.

Usbd.sys

Changed:
USBD_CreateConfigurationRequestEx(x,x)

Usbhub.sys

Changed:
USBD_ValidateExtendedConfigurationDescriptor(x,x,x,x,x)

Suspicious:
UsbhHubRunPortChangeQueue(x,x,x,x)
UsbhPostInterrupt(x)
UsbhReferenceListRemove(x,x)
UsbhGetTtDeviceHandle(x,x,x,x)
UsbhGetDeviceHandle(x,x)
UsbhInitialize(x,x)
UsbhFinishStart(x,x)
UsbhAddDevice(x,x)
UsbhFdoWakePoComplete_Action(x,x,x,x,x)
UsbhPeekNextPdoWakeIrp(x,x,x)
UsbhPeekNextPdoIdleIrp(x,x,x)
UsbhReinitialize(x,x)
Usbh_PCE_wChangeERROR_Action(x,x,x,x,x,x,x)
UsbhHubProcessTimeoutObj(x,x,x,x)
UsbhResetPortTimerDpc(x,x,x,x)
Usbh_BusRemove_PdoEvent(x,x)
UsbhSyncResetDeviceInternal(x,x,x)
UsbhPdoPnp_StopDevice(x,x)
UsbhPdoPnp_RemoveDevice(x,x)
UsbhIdleIrp_NoIrp(x,x,x,x,x,x)
UsbhIdleIrp_WaitWorker(x,x,x,x,x,x)
UsbhIdleIrp_CB_Complete(x,x,x,x,x,x)
UsbhIdleIrp_CB_Canceled(x,x,x,x,x,x)
UsbhIdleIrp_CB_Pending(x,x,x,x,x,x)
UsbhIdleExIrp_IdleReady(x,x,x,x,x,x)
UsbhPdoSetContentId(x,x,x)
UsbhCreateConfigurationRequestEx(x,x,x,x,x)

Usbccgp.sys

Changed:
USBD_ValidateExtendedConfigurationDescriptor(x,x,x,x,x)
BuildSelectConfigUrb(x,x,x)
QuerySelectiveSuspendRegistryFlag(x)
CancelFdoIdleIrp(x,x)
ParentIdleNotificationCallback(x)
PdoIdleCancelComplete(x,x)                                     

Suspicious:
CompletionPdoSelectInterface(x,x,x)
DispatchPdoUrb(x,x)
DispatchPdoInternalDeviceControl(x,x)
DispatchPdoSetPower_SetParentD0Completion(x,x,x)
FdoSendSetPowerRequest(x,x,x,x)
DispatchPdoSetPower(x,x)
StopIdleTimer(x)
StartIdleTimer(x)
SetPdoIdle(x,x,x,x,x)
ChangeIdleState(x,x,x)
FreeFunctionPDOResources(x)
InstallExtPropDesc(x)
QueryFunctionPdoID(x,x)
CreateStaticFunctionPDOs(x)

Usbport.sys

Changed:
USBPORT_RootHub_StandardCommand(x,x,x,x)
USBPORT_InitializeConfigurationHandle(x,x,x)
USBPORT_RegisterUSBPortDriver(x,x,x,x)
USBPORT_PdoDevicePowerIrp(x,x,x)
USBPORT_PdoSystemPowerIrp(x,x,x)

I was somewhat lineal in analyzing the functions. I started with the Hidparse.sys (HID Parsing Library) driver, and stopped once I came to the USBD_CreateConfigurationRequestEx function in the usbd.sys (Universal Serial Bus Driver) driver. This is the change that caught my attention:

usbd_createconfigrequestex_diff

At first glance, we see that a new basic block was added in the picture on the right (patched driver) where the EDX and ESI values are compared, and if the value in EDX (which is arg4) is greater than the value stored in ESI, it returns immediately with 0 in EAX. Otherwise, the execution continues.

Let's dig into the details of this function. According to MSDN, “The USBD_CreateConfigurationRequestEx routine allocates and formats a URB to select a configuration for a USB device.” A URB is a structure used by USB client drivers to describe the requests sent to the USB driver stack.

This function is a replacement for the USBD_CreateConfigurationRequest function and has two parameters:

  1. A pointer to a USB_CONFIGURATION_DESCRIPTOR
  2. A pointer to a USBD_INTERFACE_LIST_ENTRY

In the non-patched driver, at the beginning of the USBD_CreateConfigurationRequestEx function, the first element of the InterfaceList array is taken and stored in EAX:

.text:00010987 push    esi
.text:00010988 mov     esi, [ebp+InterfaceList] ; get USBD_INTERFACE_LIST_ENTRY ptr
.text:0001098B mov     eax, [esi] ; get USB_INTERFACE_DESCRIPTOR ptr
.text:0001098D push    edi
.text:0001098E mov     ecx, esi ; ECX = USBD_INTERFACE_LIST_ENTRY ptr
.text:00010990 mov     [ebp+InterfaceList], 18h
.text:00010997 jmp     short loc_109AF

And then, the InterfaceList variable is used to store a value (0x18).

The first element of the InterfaceList is a pointer to a USB_INTERFACE_DESCRIPTOR structure:

typedef struct _USB_INTERFACE_DESCRIPTOR {
  UCHAR bLength;
  UCHAR bDescriptorType;
  UCHAR bInterfaceNumber;
  UCHAR bAlternateSetting;
  UCHAR bNumEndpoints;
  UCHAR bInterfaceClass;
  UCHAR bInterfaceSubClass;
  UCHAR bInterfaceProtocol;
  UCHAR iInterface;
} USB_INTERFACE_DESCRIPTOR, *PUSB_INTERFACE_DESCRIPTOR;

After a null pointer check at offset 0x109AF, the bNumEndpoints field is taken and some arithmetic operations are performed with it:

.text:00010999 loc_10999:
.text:00010999 movzx   ax, byte ptr [eax+4]
.text:0001099E imul    ax, 14h
.text:000109A2 add     ax, 10h
.text:000109A6 add     word ptr [ebp+InterfaceList], ax
.text:000109AA add     ecx, 8
.text:000109AD mov     eax, [ecx]

We can see that AX has the result of the arithmetic operations and is then added to the InterfaceList variable.

Immediately, ECX is incremented in 8, and then the next interface descriptor is taken.

Then we have a call to ExAllocatePoolWithTag using the previous calculated value stored in the InterfaceList variable as the NumberOfBytes parameter:

.text:000109B3 movzx   ebx, word ptr [ebp+InterfaceList]
.text:000109B7 push    'DBSU'          ; Tag
.text:000109BC push    ebx             ; NumberOfBytes
.text:000109BD push    eax             ; PoolType
.text:000109BE call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:000109C4 mov     edi, eax
.text:000109C6 test    edi, edi
.text:000109C8 jz      short loc_10A46

After that, the buffer (EDI) is initialized using a memset call:

.text:000109CA push    ebx             ; size_t
.text:000109CB push    0               ; int
.text:000109CD push    edi             ; void *
.text:000109CE call    _memset
.text:000109D3 add     esp, 0Ch
.text:000109D6 lea     eax, [edi+18h]
.text:000109D9 jmp     short loc_10A2E

At this point, I went back to see the patch. After the null pointer check and before storing the result of the arithmetic operations, a small check was added:

[...]
loc_10998:
movzx   ax, byte ptr [eax+4]
imul    ax, 14h
add     ax, 10h
movzx   eax, ax
movzx   edx, ax
mov     esi, 0FFFFh
sub     esi, edx
movzx   edx, word ptr [ebp+InterfaceList]
cmp     edx, esi
jg      loc_10A6
[...]

If the value in EDX, stored in the InterfaceList variable, is greater than the value calculated from the bNumEndpoints field, then we are out!

Here I thought, “There is no check on the value used as a size for the allocation, and then a memset() call is used to initialize the buffer. I could wrap the bNumEndpoints using the arithmetic operations, perform a small allocation, and then, using the memset call, overwrite arbitrary data with nulls.” But if we look closer, both the ExAllocatePoolWithTag and the memset call receive size parameters in EBX, which came from the InterfaceList variable. There isn't an inconsistency, because even though we can make that value up to 0xFF*0x14+0x10, the same value is used in both function calls. So nothing happened here :(

At this point we can confirm that:

  1. We can't make the InterfaceList variable to wrap.
  2. The sizes used in the ExAllocatePoolWithTag and memset calls came from the same InterfaceList variable, therefore making a small allocation and then a memset call with a greater size than the allocated buffer is not possible.

The next patch I saw was in the usbhub.sys driver. The patched function is USBD_ValidateExtendedConfigurationDescriptor. This one made more sense from the beginning, I should have started here :P

The function has a very descriptive name, it is used to “validate” an “extended configuration descriptor.” But what is this descriptor used for? What's its structure? We'll see, but first thing first. Let's see the patch:

usbd_validatextendedconfigurationdescriptor_diff

The patch is in the middle of a loop and it seems pretty obvious:

loc_2F487:
movzx   edx, byte ptr [eax+2]
add     edx, esi
cmp     edx, ebx
jnb     loc_2F5C9

A field from a structure is accessed (EAX+2), moved to EDX and compared against the value stored in the EBX register. If EDX is not below EBX then we reach this code:

loc_2F5C9:
mov     [ebp+var_4], 0C000000Dh

This could be STATUS_INVALID_PARAMETER or USBD_STATUS_BUFFER_UNDERRUN, but given the context I bet on the second option. The EDX value is then used here:

mov     ebx, [ebp+P]
mov     byte ptr [edx+ebx], 2
movzx   edx, byte ptr [eax+3]
inc     esi
mov     ebx, 100h
cmp     esi, edx
jb      short loc_2F487

EBX is a pointer to a buffer returned by ExAllocatePoolWithTag:

.text:0002F41F push    [ebp+Tag]       ; Tag
.text:0002F422 mov     ebx, 100h
.text:0002F427 push    ebx             ; NumberOfBytes
.text:0002F428 push    edi             ; PoolType
.text:0002F429 call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:0002F42F mov     [ebp+P], eax
.text:0002F432 cmp     eax, edi
.text:0002F434 jnz     short loc_2F442

Therefore, the patch is preventing the write operation from going beyond the limits of the allocated buffer. Now we need to determine what that field is and how to reach this basic block.

If we see the xrefs to this function we get a bit more of information about the descriptor:

xrefs_to_validateextendeddescriptor

We can see that the USBD_ValidateExtendedConfigurationDescriptor is called from the _UsbhGetMsOsExtendedConfigDescriptor. My question was, “WTF is a MsOsExtendedConfigurationDescriptor?” Well, I did a little research and I found this and this. It seems that it's an additional configuration descriptor used by vendors to perform device specific requests.

Let's see how the USBD_ValidateExtendedConfigurationDescriptor works internally.

First, it checks the total length of the configuration descriptor. If it is lower than 0x10 we are out:

.text:0002F3C6 cmp     [ebp+SizeOfConfiguration], 10h
.text:0002F3CA jb      loc_2F58A

Then, it takes the pointer to the first descriptor which is a USB_CONFIGURATION_DESCRIPTOR structure:

.text:0002F3D0 mov     esi, [ebp+ConfigurationDescriptor]
.text:0002F3D3 cmp     esi, edi
.text:0002F3D5 jnz     short loc_2F3E3

In WinDbg:

usbhub!USBD_ValidateExtendedConfigurationDescriptor+0x27:
922343d0 8b7510          mov     esi,dword ptr [ebp+10h]
1: kd> dd ebp+10h
8d8309a4  8695a288 00000027 42554855 87122028
8d8309b4  c0000000 8691f0e8 8691f0e8 00000028
8d8309c4  873ef930 00000028 00040100 00000001
8d8309d4  00000000 1fa77e8a 8d830a1c 92224aaa
8d8309e4  87122028 8691f030 87122028 8712bd88
8d8309f4  00000400 87122028 87122028 92247700
8d830a04  8d830a00 00000000 871220e0 00000503
8d830a14  00000000 008217ab 8d830a5c 922256c9

1: kd> dt Wdf01000!_USB_CONFIGURATION_DESCRIPTOR poi(ebp+10h)
   +0x000 bLength          : 0x9 ''
   +0x001 bDescriptorType  : 0x2 ''
   +0x002 wTotalLength     : 0x27
   +0x004 bNumInterfaces   : 0x1 ''
   +0x005 bConfigurationValue : 0x1 ''
   +0x006 iConfiguration   : 0 ''
   +0x007 bmAttributes     : 0xc0 ''
   +0x008 MaxPower         : 0x30 '0'

We can confirm this information using, for example, USB Device Viewer:

          ===>Configuration Descriptor<===
bLength:                           0x09
bDescriptorType:                   0x02
wTotalLength:                    0x0027  -> Validated
bNumInterfaces:                    0x01
bConfigurationValue:               0x01
iConfiguration:                    0x00
bmAttributes:                      0xC0  -> Self Powered
MaxPower:                          0x30 =  96 mA

Btw, all these descriptors came from a Samsung Galaxy Note GT-N7000.

The, it takes the wTotalLenght field:

.text:0002F3E3
.text:0002F3E3 loc_2F3E3:              ; EAX = USB_CONFIGURATION_DESCRIPTOR.wTotalLength
.text:0002F3E3 movzx   eax, word ptr [esi+2]
.text:0002F3E7 cmp     ax, 9
.text:0002F3EB jb      short loc_2F3D7

usbhub!USBD_ValidateExtendedConfigurationDescriptor+0x3a:
922343e3 0fb74602        movzx   eax,word ptr [esi+2]
1: kd> db esi+2
8695a28a  27 00 01 01 00 c0 30 09-04 00 00 03 06 01 01 05  '.....0.........
8695a29a  07 05 82 02 00 02 00 07-05 01 02 00 02 01 07 05  ................
8695a2aa  83 03 1c 00 06 00 00 00-00 00 00 00 00 00 00 00  ................
8695a2ba  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8695a2ca  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8695a2da  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8695a2ea  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8695a2fa  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Compare it against 9 which is the length of USB_CONFIGURATION_DESCRIPTOR structure, if it is lower than that, we are out again.

Then it compare it against the total length of the configuration descriptor:

.text:0002F3ED movzx   eax, ax
.text:0002F3F0 cmp     eax, [ebp+_SizeOfConfiguration]
.text:0002F3F3 ja      short loc_2F3D7

usbhub!USBD_ValidateExtendedConfigurationDescriptor+0x47:
922343f0 3b4514          cmp     eax,dword ptr [ebp+14h]
1: kd> db ebp+14h
8d8309a8  27 00 00 00 55 48 55 42-28 20 12 87 00 00 00 c0  '...UHUB( ......
8d8309b8  e8 f0 91 86 e8 f0 91 86-28 00 00 00 30 f9 3e 87  ........(...0.>.
8d8309c8  28 00 00 00 00 01 04 00-01 00 00 00 00 00 00 00  (...............
8d8309d8  8a 7e a7 1f 1c 0a 83 8d-aa 4a 22 92 28 20 12 87  .~.......J".( ..
8d8309e8  30 f0 91 86 28 20 12 87-88 bd 12 87 00 04 00 00  0...( ..........
8d8309f8  28 20 12 87 28 20 12 87-00 77 24 92 00 0a 83 8d  ( ..( ...w$.....
8d830a08  00 00 00 00 e0 20 12 87-03 05 00 00 00 00 00 00  ..... ..........
8d830a18  ab 17 82 00 5c 0a 83 8d-c9 56 22 92 00 00 00 00  ....\....V".....

If it is above that value, we are out again.

If everything went fine, it allocates a pool buffer with size 0x100:

.text:0002F3F5 push    [ebp+Tag]       ; Tag
.text:0002F3F8 push    100h            ; NumberOfBytes
.text:0002F3FD push    edi             ; PoolType
.text:0002F3FE call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:0002F404 mov     edi, eax
.text:0002F406 test    edi, edi
.text:0002F408 jnz     short loc_2F416

And initialize it with NULL:

.text:0002F416
.text:0002F416 loc_2F416:              ; size_t
.text:0002F416 push    100h
.text:0002F41B push    0               ; int
.text:0002F41D push    edi             ; void *
.text:0002F41E call    _memset

This time, the buffer starts in 0x8507fc40:

2: kd> db edi edi+0x100
8507fc40  00 00 00 00 80 cb 98 82-00 00 00 00 00 00 00 00  ................
8507fc50  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fc60  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fc70  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fc80  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fc90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fca0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fcb0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fcc0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fcd0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fce0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fcf0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fd00  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fd10  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fd20  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fd30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
8507fd40  21                                               !

Now, we have the following code:

.text:0002F423 movzx   ecx, word ptr [esi+2] ; ECX = USB_CONFIGURATION_DESCRIPTOR.wTotalLength
.text:0002F427 add     ecx, esi ; ECX points to the end of all the configuration data
.text:0002F429 add     esp, 0Ch
.text:0002F42C and     [ebp+ConfigurationDescriptor], 0
.text:0002F430 lea     eax, [esi+9] ; EAX points to the beginning of the first Interface Descriptor
.text:0002F433 movzx   esi, byte ptr [esi+4] ; ESI = USB_CONFIGURATION_DESCRIPTOR.bNumInterfaces
.text:0002F437 jmp     short loc_2F49A

Essentially, it calculates a pointer to the end of the CONFIGURATION_DESCRIPTOR structure by copying the value of the wTotalLength member of that structure to ECX and adding it to ESI (which, let's remember points to the beginning of the that structure).

Then EAX ends, pointing to the first Interface Descriptor:

92234430 8d4609          lea     eax,[esi+9]
2: kd> dt Wdf01000!_USB_INTERFACE_DESCRIPTOR esi+9
   +0x000 bLength          : 0x9 ''
   +0x001 bDescriptorType  : 0x4 ''
   +0x002 bInterfaceNumber : 0 ''
   +0x003 bAlternateSetting : 0 ''
   +0x004 bNumEndpoints    : 0x3 ''
   +0x005 bInterfaceClass  : 0xff ''
   +0x006 bInterfaceSubClass : 0xff ''
   +0x007 bInterfaceProtocol : 0 ''
   +0x008 iInterface       : 0x5 ''

Finally, the bNumInterfaces field is moved to ESI.

.text:0002F49A
.text:0002F49A loc_2F49A:
.text:0002F49A cmp     eax, ecx
.text:0002F49C jb      short loc

Now, it checks if EAX (which points to the beginning of the first Interface Descriptor) is below ECX (which points to the end of all the configuration data). If the condition is True we reach the following code:

.text:0002F439
.text:0002F439 loc_2F439: ; EDX = USB_INTERFACE_DESCRIPTOR.bLength
.text:0002F439 movzx   edx, byte ptr [eax]
.text:0002F43C add     edx, eax ; EDX = USB_INTERFACE_DESCRIPTOR.bLength + address of the first Interface Descriptor
.text:0002F43E cmp     edx, ecx
.text:0002F440 ja      loc_2F58A

Another check, EDX receives the bLength field of the USB_INTERFACE_DESCRIPTOR and EAX is added to it make it point to the first Endpoint Descriptor:

2: kd> dt Wdf01000!_USB_ENDPOINT_DESCRIPTOR @edx
   +0x000 bLength          : 0x7 ''
   +0x001 bDescriptorType  : 0x5 ''
   +0x002 bEndpointAddress : 0x82 ''
   +0x003 bmAttributes     : 0x2 ''
   +0x004 wMaxPacketSize   : 0x200
   +0x006 bInterval        : 0 ''

Then, it gets compared against ECX (end of the configuration data). If EDX is above that value, we are out once more.

We are near the importat part, but we have another check to bypass:

.text:0002F446 cmp     byte ptr [eax+1], 0Bh ; EAX = _USB_INTERFACE_DESCRIPTOR.bDescriptorType
.text:0002F44A jnz     short loc_2F47B

EAX+1 is the bDescriptorType from the USB_INTERFACE_DESCRIPTOR structure. Every USB descriptor has its own number assigned, for example:

00048 /* USB_COMMON_DESCRIPTOR.bDescriptorType constants */
00049 #define USB_DEVICE_DESCRIPTOR_TYPE        0x01
00050 #define USB_CONFIGURATION_DESCRIPTOR_TYPE 0x02
00051 #define USB_STRING_DESCRIPTOR_TYPE        0x03
00052 #define USB_INTERFACE_DESCRIPTOR_TYPE     0x04
00053 #define USB_ENDPOINT_DESCRIPTOR_TYPE      0x05
00054 #define USB_RESERVED_DESCRIPTOR_TYPE        0x06
00055 #define USB_CONFIG_POWER_DESCRIPTOR_TYPE    0x07
00056 #define USB_INTERFACE_POWER_DESCRIPTOR_TYPE 0x08

But where is the 0x0B descriptor type? Well, I found this and it seems the bDescriptorType = 0x0B corresponds to a Interface Association Descriptor. Now the question is: WTF is an Interface Association Descriptor?

Quoting Microsoft definition:

USB interface association descriptor (IAD) allows the device to group interfaces that belong to a function.

Whatever you say, dude!

The important takeaway here is that we'll need to forge a USB Composite Device in order to get and bypass this check.

Let's assume for a moment that we have built a USB Composite Device with the corresponding Interface Association Descriptor and we reach this check. We are going to bypass this comparission because the bDescriptorType of the IAD is 0x0B. Then, we should arrive here:

.text:0002F44C movzx   edx, byte ptr [eax+2]
.text:0002F450 mov     byte ptr [edx+edi], 1
.text:0002F454 xor     edx, edx
.text:0002F456 inc     edx
.text:0002F457 cmp     [eax+3], dl
.text:0002F45A jbe     short loc_2F472

EAX should be pointing the IAD and EAX+2 is going to be the bFirstInterface field. A 0x01 is written to the previous allocated buffer (EDI) using EDX as an offset. Here, it is not possible to write beyond the limits of the allocated buffer because the bFirstInterface is defined as a byte, so the maximum value is 0xFF and our buffer is 0x100.

Then EDX is XOR'ed, incremented by 1 and DL compared against EAX+3, which is bInterfaceCount. If the comparission it is not True (which means bInterfaceCount is at least 0x02) we finally reach the vulnerable basic block:

.text:0002F45C
.text:0002F45C loc_2F45C:
.text:0002F45C movzx   ebx, byte ptr [eax+2]
.text:0002F460 add     ebx, edx
.text:0002F462 mov     byte ptr [ebx+edi], 2
.text:0002F466 movzx   ebx, byte ptr [eax+3]
.text:0002F46A inc     edx
.text:0002F46B cmp     edx, ebx
.text:0002F46D jb      short loc_2F45C

Again, bFirstInterface is taken and moved to EBX. EDX, which was previously XOR'ed and incremented by 1, is added to EBX, which is then used as an offset in the next write operation at 0x2F462. This time, we can write beyond the limit of EDI because even though bFirstInterface is defined as a byte, the ADD operation can increase the value of EBX. If that happens, we are going to overwrite the header of the next pool chunk. Then, when at 0x2F598 the call to ExFreePoolWithTag is reached and the header of the next pool chunk (the one we have overwritten) is checked, we should get a BAD_POOL_HEADER error.

So we need two things in order to trigger the bug:

1. A composite device with a MS OS Extended Configuration Descriptor, in order to get USBD_ValidateExtendedConfigurationDescriptor function.

2. A Interface Association Descriptor with the following structure:

    BYTE  bLength      0x08
    BYTE  bDescriptorType    0x0B
    BYTE  bFirstInterface    0xff
    BYTE  bInterfaceCount    0x02
    BYTE  bFunctionClass    0x0E
    BYTE  bFunctionSubClass   0x03
    BYTE  bFunctionProtocol   0x00
    BYTE  iFunction      0x04

The important fields are marked in red. With the bDescriptorType set to 0x0B we bypass the check at USBD_ValidateExtendedConfigurationDescriptor+0x9D:

.text:0002F446 cmp     byte ptr [eax+1], 0Bh
.text:0002F44A jnz     short loc_2F47B

With the bInterfaceCount set to 0x02 we bypass the check at USBD_ValidateExtendedConfigurationDescriptor+0xAE:

.text:0002F457 cmp     [eax+3], dl
.text:0002F45A jbe     short loc_2F472

With the bFirstInterface field set to 0xFF we increase the value in EBX using the ADD operation and that opens the possibility to write beyond the limits of the allocated buffer at USBD_ValidateExtendedConfigurationDescriptor+0x55.

Sadly, researching how to actually program a composite device to provide the proper frames fell outside of the time we could invest in this vulnerability, but it would be great if a reader gets interested in this bug and could develop a proof of concept for it.

  • Latest from CoreLabs

Ready for a Demo?

Eliminate identity-related breaches with SecureAuth!