Wherein I learn more than I ever wanted to learn about USB descriptors and start using pyusb to send raw USB commands to my mouse, to try and learn even more about the inner workings of it. Partly so I can make sense of the firmware disassembly, and partly so I can maybe have a hope of unbricking it!

It's Been... Uh... The Story So Far?

A long long time ago, in Part 1, I decided I wanted to make my mouse's fancy features work outside of Windows, and figured out the most useless part of the protocol. I extracted the firmware in Part 2, disassembled it in Part 3, and figured out more of the protocol and subsequently bricked the mouse in Part 4. Ouch.

Where do we go from here? Well, ideally we'd like to have this mouse fully working again so that we can play with it - writing a tool for the vendor-specific commands is useless if it won't actually accept any of them...!

The inbuilt firmware updater tool doesn't work, of course, since it just tries to send one of those commands that the mouse is straight up ignoring.

Exploring the Firmware Disassembly

I wanted to get a better understanding of what was going on behind the scenes, so I started delving into the disassembly. Remember how I mentioned in part 3 that Holtek supplied some sample code for a USB Gaming Mouse... with a suspiciously similar feature set?

I started looking through my primitive disassembly and comparing it to the sample code. It was most definitely very suspiciously similar. The microcontroller assigns a reset vector at address 0, and then places interrupt vectors at addresses 4, 8, 0xC, 0x10, and so forth -- the locations that the CPU will jump to when a particular kind of interrupt occurs. Each address maps to one word, or one instruction, so you can actually fit 4 instructions into one interrupt vector.

The sample code's main.asm starts as follows (skipping past a ton of extern declarations to reference code/data defined in other files):

main       .section  AT 00000H 'code'
;-----------------------------------------------------------------------------;
           ;Watchdot Time-out or Reset
           SZ        PDF                    ;power down flag
           SNZ       TO                     ;Watchdot time-out flag
           JMP       main_start             ;reset
           JMP       Watch_Wake             ;Watchdot Time-out
;------------------------------------------------------------------------------
           ORG       0004H                  ;External Interrupt 0
           RETI
           public    DelayXuSec
DelayXuSec:
           sdz       ACC                             
           jmp       DelayXuSec
           ret                                     
;------------------------------------------------------------------------------
           ORG       0008H                  ;External Interrupt 1
           RETI
           public    Delay100uSec
Delay100uSec:
           mov       A,132                         
           jmp       DelayXuSec                      
;------------------------------------------------------------------------------
           ORG       000CH                  ;usb interrut subroutine
           JMP       USB_ISR_BEGIN          ;usb_int.asm
           public    Delay12uSec
Delay12uSec:
           mov       a,14
           jmp       DelayXuSec
;-----------------------------------------------------------------------------;
           ORG       0010H                  ;Multi-function 0 Interrupt subroutine
           JMP       TMR0_ISR
           public    Delay4uSec
Delay4uSec:
           mov       A,05
           jmp       DelayXuSec
;-----------------------------------------------------------------------------;
           ORG       0014H                  ;Multi-function 1 Interrupt subroutine
           RETI
           public    Delay50uSec
Delay50uSec:
           mov       A,66
           jmp       DelayXuSec
;-----------------------------------------------------------------------------;
           ORG       0018H                  ;Multi-function 2 Interrupt subroutine
           RETI
;-----------------------------------------------------------------------------;
           ORG       001CH                  ;Multi-function 3 Interrupt subroutine
           RETI
;-----------------------------------------------------------------------------;
           ORG       0020H                  ;SIM Interrupt subroutine
           RETI
;-----------------------------------------------------------------------------;                                          
           ORG       0024H                  ;SPIA Interrupt subroutine
           RETI
;-----------------------------------------------------------------------------;
           ORG       0028H                  ;LVD Interrupt subroutine
           RETI

The reset vector is handled by a couple of conditions which jump to either main_start or Watch_Wake depending on the state of certain registers. This takes up the full four instructions allotted between 0 (the reset vector) and 4 (the first interrupt vector).

The interrupt vectors at addresses 4, 8, 0x14, 0x18, 0x1C, 0x20, 0x24 and 0x28 are all empty handlers -- you get reti (return from interrupt) and that's it. The two at 0xC and 0x10 are non-empty, but just contain a single jmp instruction to another place where the meat is.

They've taken advantage of this to fill the 3-instruction spaces between the vectors with some stuff. DelayXuSec is a very simple function which spins in a loop until ACC (the other name for the accumulator in this architecture) reaches zero, decrementing ACC each iteration.

Then, there's a few very badly-named variants of this: Delay100uSec loads 132 into the accumulator and jumps to DelayXuSec. Delay12uSec does 14. Delay4uSec does 5. Delay50uSec does 66.

My mouse's firmware contains the exact same function, in the exact same place, and a bunch of similar variants, but with slightly different values (and there's more of them). That's suspicious; did they base it off the sample code? This similarity is not conclusive, and it's not really useful on its own, but it is enough to make me go looking further.

The code at main_start isn't identical, but there's definite similarities - though admittedly there's bound to be some sort of similarity in places like this; after all, they're both going to need to do things like set up USB communications using the registers that control this chip's built-in USB functionality.

USB Descriptors

I decided to have a read through the sample code so I could better familiarise myself with how it was structured. Something that immediately caught my eye was the descriptors, located in the aptly named DESCRIPTOR.ASM in the sample code.

These are standard data structures (defined in the USB specifications) which describe the device and how it communicates. The host machine asks the device things like "what are you?" and "what do you offer?"; the device replies with the appropriate descriptors, and the host parses them to learn how to deal with that device. There's a few different kinds that show up in the sample code, which I'll describe here.

If you're feeling as masochistic as I was, you can find the structure definitions for general USB descriptors on pages 261-274 of the USB 2.0 specification and for HID descriptors on pages 21-47 of the USB HID Device Class Definition.

Device Descriptor

Only one of these exists! Stores general information about the device as a whole. On our Sample Mouse, it contains the following:

  • metadata that every descriptor has: type, size
  • USB version it's designed for (2.0)
  • class/subclass/protocol (zero)
  • maximum packet size for the control endpoint (8 bytes - this will make sense a few paragraphs later!)
  • vendor/product ID (04D9:A067 here)
  • claimed firmware version (1.0)
  • IDs of the string descriptors that store the manufacturer/product/serial number (if any)
  • amount of configurations

Configuration Descriptor

Devices are allowed to have more than one of these, but the Sample Mouse only has one. This allows devices to change their capabilities on-the-fly (what interfaces they expose, how much power they promise to draw).

This one says that there are 3 interfaces, the number of the configuration is 1, the device is bus-powered and supports remote wakeup, and it claims to draw 100mA of power at most.

Interface Descriptor

This is kind of like a 'sub-device'. In the case of the Sample Mouse, there's three!

  • Keyboard: This is interface 0, uses 1 endpoint, is class 3 (HID), subclass 1 (boot subclass), and protocol 1 (keyboard). Its name is given by string descriptor 1.
  • Mouse: This is interface 1, uses 1 endpoint, is class 3 (HID), subclass 1 (boot subclass) and protocol 2 (mouse). It has no name.
  • Mystery Interface!: This is interface 2, uses 2 endpoints, is class 3 (HID), subclass 0 (no subclass) and protocol 0 (no class-specific protocol). It also has no name.

Class 3 means that it's a HID class device, unsurprisingly. The subclass specifies that the keyboard and mouse interfaces are usable as boot devices -- this means that they support a predefined protocol for either keyboard or mice (surprise!), so that a BIOS can use them without having to parse a complex Report specification (more on this shortly).

Endpoint Descriptor

These tell us which endpoints (communication channels, essentially) are used by each interface. Each endpoint has an address defined (number + transfer direction), attributes (specifying what kind of transfer occurs), and a couple of transfer-type-specific parameters. Thankfully, we don't need to know much about these particular details to understand what's going on in our mouse.

The Keyboard's got one: endpoint address 1 (in / to host), interrupt transfer type, maximum packet size 8 bytes.

The Mouse's got one: endpoint address 2 (in / to host), interrupt transfer type, maximum packet size 8 bytes.

The Mystery Interface's got two: one has endpoint address 3 (in / to host), the second has endpoint address 4 (out / to device). Both have a maximum packet size of 32 bytes.

There's also Endpoint 0, which always exists. This is used for core USB control commands (including the ones that fetch the descriptors - so that's necessary to bootstrap the whole system) and can be implicitly used by any interface without this interface having to create an endpoint descriptor that goes "hey folks, I wanna use endpoint 0, thank".

String Descriptor

These allow the device to serve up text; mostly just used for the purpose of showing a name in Device Manager and similar. The sample mouse includes three of these: the first one just tells the host what languages it supports (code 0x0409: English), the second one is "Holtek", and the third is "USB Gaming Mouse".

The Device, Configuration and Interface Descriptors include fields that can refer to strings by using either 0 for "no string", or a value higher than 0 to specify the index of a particular string descriptor.

HID Descriptors

Last but not least come the HID-specific descriptors. All the ones we've described up to now are part of the main USB specification; every USB device uses them. These, on the other hand, are exclusively for HID class devices.

(It's easy to say "HID device", but when you say that, what you're really saying is "Human Interface Device device". That's nasty. The HID specification says "HID class device", so I'll defer to them and use that. Still feels kinda ugly to me, but they know more about HID dev-- HID class devices than I do!)

The HID Descriptor is basically a little bit of metadata that gets attached to each interface (not device!), with fields relating to the HID spec. It describes what version of the HID spec the device abides by (1.10 for the Sample Mouse), what country code the device is localised for (optional, but useful for e.g keyboard layouts) and which extra HID-specific descriptors are included.

The Report Descriptor is a complex, variable-size beast. It's not really a structure, it's more like a domain-specific language. Each HID Descriptor contains one Report Descriptor. These essentially tell the host device how to parse the data coming from the device, or in some cases, how to format data being sent to the device. This is why you can have mice with different amounts of buttons, or game controllers with different amounts of controls.

The report descriptor contains stuff like "I'm gonna send you the on/off state of 8 buttons, followed by a 16-bit X position in the range -32768 to 32767, followed by an 8-bit Y position in the range 0 to 255" (but in a machine-readable, formally specified way), and it's the host's job to make sense out of that. More on this shortly, where I investigate my beloved TeckNet mouse's reports.

The Physical Descriptor is optional, and includes information on how the reports map to physical properties of the device. The sample mouse code doesn't use this.

Comparing these to my mouse

I'm going to admit I made a mistake here when I was working on this. I decided to try and parse the descriptor data by hand, straight from my disassembly. In retrospect it would have been much easier to just grab it straight from the device and feed it into a parser, but hindsight is 20/20... For your sake, I'm going to go for the latter method here.

First, I hooked up my mouse to an Arch Linux VM and installed pyusb and IPython. I feel silly for not trying this earlier, but this library combined with the REPL makes a wonderful little tool for poking at USB devices ad-hoc.

(Note: You may need to run ipython as superuser to be able to write to USB devices directly.)

In [1]: import usb.core, usb.control, usb.util, binascii

In [2]: dev = usb.core.find(idVendor=0x4D9, idProduct=0xA118)

In [3]: dev
Out[3]: <DEVICE ID 04d9:a118 on Bus 001 Address 004>

In [4]: binascii.hexlify(usb.control.get_descriptor(dev, 0x400, usb.util.DESC_TYPE_DEVICE, 0))
Out[4]: b'1201000200000008d90418a1010100020001'

In [5]: binascii.hexlify(usb.control.get_descriptor(dev, 0x400, usb.util.DESC_TYPE_CONFIG, 0))
Out[5]: b'09025b00030100a03209040000010301020009211001000122470007058103080001090401000103000100092110010001228c00070582031000040904020002030000000921100100012220000705830340000407050403400004'

This library exposes a nice API for dealing with configurations and endpoints, but I'm going to mostly ignore it in the sake of poking at the raw data, because I want to see how it works. I load the device, verify it's been connected to, then use pyusb's helper functions to send two Get_Descriptor requests. One gets me the Device Descriptor, and the second gets me the Configuration Descriptor as well as the bits it contains (Interface, Endpoint and HID Descriptors).

There's an extremely handy parser I found online here: USB Descriptor and Request Parser - you can just paste the hex blobs and click 'USB Standard Descriptor' and it'll output what they contain. It's excellent. Here's the output I've gotten from my mouse for those:

0x12,        // bLength
0x01,        // bDescriptorType (Device)
0x00, 0x02,  // bcdUSB 2.00
0x00,        // bDeviceClass (Use class information in the Interface Descriptors)
0x00,        // bDeviceSubClass 
0x00,        // bDeviceProtocol 
0x08,        // bMaxPacketSize0 8
0xD9, 0x04,  // idVendor 0x04D9
0x18, 0xA1,  // idProduct 0xA118
0x01, 0x01,  // bcdDevice 2.01
0x00,        // iManufacturer (String Index)
0x02,        // iProduct (String Index)
0x00,        // iSerialNumber (String Index)
0x01,        // bNumConfigurations 1

0x09,        // bLength
0x02,        // bDescriptorType (Configuration)
0x5B, 0x00,  // wTotalLength 91
0x03,        // bNumInterfaces 3
0x01,        // bConfigurationValue
0x00,        // iConfiguration (String Index)
0xA0,        // bmAttributes Remote Wakeup
0x32,        // bMaxPower 100mA

0x09,        // bLength
0x04,        // bDescriptorType (Interface)
0x00,        // bInterfaceNumber 0
0x00,        // bAlternateSetting
0x01,        // bNumEndpoints 1
0x03,        // bInterfaceClass
0x01,        // bInterfaceSubClass
0x02,        // bInterfaceProtocol
0x00,        // iInterface (String Index)

0x09,        // bLength
0x21,        // bDescriptorType (HID)
0x10, 0x01,  // bcdHID 1.10
0x00,        // bCountryCode
0x01,        // bNumDescriptors
0x22,        // bDescriptorType[0] (HID)
0x47, 0x00,  // wDescriptorLength[0] 71

0x07,        // bLength
0x05,        // bDescriptorType (Endpoint)
0x81,        // bEndpointAddress (IN/D2H)
0x03,        // bmAttributes (Interrupt)
0x08, 0x00,  // wMaxPacketSize 8
0x01,        // bInterval 1 (unit depends on device speed)

0x09,        // bLength
0x04,        // bDescriptorType (Interface)
0x01,        // bInterfaceNumber 1
0x00,        // bAlternateSetting
0x01,        // bNumEndpoints 1
0x03,        // bInterfaceClass
0x00,        // bInterfaceSubClass
0x01,        // bInterfaceProtocol
0x00,        // iInterface (String Index)

0x09,        // bLength
0x21,        // bDescriptorType (HID)
0x10, 0x01,  // bcdHID 1.10
0x00,        // bCountryCode
0x01,        // bNumDescriptors
0x22,        // bDescriptorType[0] (HID)
0x8C, 0x00,  // wDescriptorLength[0] 140

0x07,        // bLength
0x05,        // bDescriptorType (Endpoint)
0x82,        // bEndpointAddress (IN/D2H)
0x03,        // bmAttributes (Interrupt)
0x10, 0x00,  // wMaxPacketSize 16
0x04,        // bInterval 4 (unit depends on device speed)

0x09,        // bLength
0x04,        // bDescriptorType (Interface)
0x02,        // bInterfaceNumber 2
0x00,        // bAlternateSetting
0x02,        // bNumEndpoints 2
0x03,        // bInterfaceClass
0x00,        // bInterfaceSubClass
0x00,        // bInterfaceProtocol
0x00,        // iInterface (String Index)

0x09,        // bLength
0x21,        // bDescriptorType (HID)
0x10, 0x01,  // bcdHID 1.10
0x00,        // bCountryCode
0x01,        // bNumDescriptors
0x22,        // bDescriptorType[0] (HID)
0x20, 0x00,  // wDescriptorLength[0] 32

0x07,        // bLength
0x05,        // bDescriptorType (Endpoint)
0x83,        // bEndpointAddress (IN/D2H)
0x03,        // bmAttributes (Interrupt)
0x40, 0x00,  // wMaxPacketSize 64
0x04,        // bInterval 4 (unit depends on device speed)

0x07,        // bLength
0x05,        // bDescriptorType (Endpoint)
0x04,        // bEndpointAddress (OUT/H2D)
0x03,        // bmAttributes (Interrupt)
0x40, 0x00,  // wMaxPacketSize 64
0x04,        // bInterval 4 (unit depends on device speed)

// 109 bytes

Lots to take in there, but it's all handily annotated. There's one configuration, as per the Device Descriptor. It's got three interfaces, just like our sample code.

Where things get different is in the interfaces themselves. Interface 0 has Protocol 2 (means it can pretend to be a generic mouse) and uses Endpoint 1. Interface 1 has Protocol 1 (so it's a keyboard) and uses Endpoint 2. This is the opposite order to the sample code, where interface 0 was a keyboard and interface 1 was a mouse. My mouse's keyboard interface (that just sounds wrong) has a maximum packet size of 16 bytes, in contrast to the sample code's keyboard interface which supplies a measly 8 bytes at most.

The third interface is bidirectional, like the sample code's third interface, using Endpoint 3 as input (device-to-host) and Endpoint 4 as output (host-to-device). It's got a max packet size of 64 bytes. Hey, doesn't that seem suspiciously identical to the bulk data transfers used by the mouse to read/write its configuration? 🤔

(spoiler: yes, that's precisely how they're transmitted)

Report Descriptors

Finally, let's dig into the report descriptors. We can dump the ones from our mouse using pyusb. Sadly, we can't use the helper methods for that because they only let us call Get_Descriptor on a device, whereas we need to send that command in an interface context. That's OK, we can build our own command!

Page 49 of the HID specification tells us precisely how.. or you can just look at that convenient screenshot of the page. Whatever works.

⚠️ Important note: Your mileage may vary, but on my test system (an Arch Linux VM), I got 'Pipe error' when I tried to send a control message directly to the HID interfaces. To get this to work, I had to first sudo rmmod usbhid. I could do this safely because I was working in a VM and was just SSHed into it from the host, but you probably don't want to unload usbhid on a regular system, lest you lose your input devices...!

pyusb provides the following method: ctrl_transfer(bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None)

Going by that document, we need to set bmRequestType to 0x81, bRequest to 6, wValue's high byte to 0x22, wValue's low byte to 0, wIndex to 0 and wLength to the amount of data we want to read.

We've got three interfaces, so we'll need to do it three times. Doing this, I finally gain my prize: the binary descriptor data!

In [20]: for i in range(3):
    ...:     print(binascii.hexlify(dev.ctrl_transfer(0x81, 6, 0x2200, i, 0x400)))
    ...:
b'05010902a1010901a1000509150025011901290575019505810295038101050116018026ff7f093009317510950281061581257f0938750895018106050c0a380295018106c0c0'
b'05010906a1018501050719e029e71500250175019508810219002aff00150026ff007508950e8100c005010980a1018502198129831500250175019503810295058101c0050c0901a101850319002aff02150026ff02751095018100c005010902a1018504150026ff7f09300931751095028102c00601ff0901a1018506150026ff00092f750895038100c0'
b'0600ff0a00ffa101150026ff0009207508954081020921910209229508b102c0'

Once again, I can feed this into Frank Zhao's handy USB Descriptor and Request Parser for some insight.

Mouse Report

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,        // Usage (Mouse)
0xA1, 0x01,        // Collection (Application)
0x09, 0x01,        //   Usage (Pointer)
0xA1, 0x00,        //   Collection (Physical)
0x05, 0x09,        //     Usage Page (Button)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x19, 0x01,        //     Usage Minimum (0x01)
0x29, 0x05,        //     Usage Maximum (0x05)
0x75, 0x01,        //     Report Size (1)
0x95, 0x05,        //     Report Count (5)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x03,        //     Report Count (3)
0x81, 0x01,        //     Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
0x16, 0x01, 0x80,  //     Logical Minimum (-32767)
0x26, 0xFF, 0x7F,  //     Logical Maximum (32767)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x75, 0x10,        //     Report Size (16)
0x95, 0x02,        //     Report Count (2)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x09, 0x38,        //     Usage (Wheel)
0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x0C,        //     Usage Page (Consumer)
0x0A, 0x38, 0x02,  //     Usage (AC Pan)
0x95, 0x01,        //     Report Count (1)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection

// 71 bytes

This one's pretty straightforward. It defines five binary inputs for mouse buttons, three constant padding bits (all that gets packed into one byte), followed by two signed 16-bit values for relative X and Y movement, followed by one signed 8-bit value for relative wheel movement, followed by one signed 8-bit value for relative "AC Pan" (horizontal scrolling).

Keyboard Report

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        //   Usage Minimum (0xE0)
0x29, 0xE7,        //   Usage Maximum (0xE7)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x19, 0x00,        //   Usage Minimum (0x00)
0x2A, 0xFF, 0x00,  //   Usage Maximum (0xFF)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x75, 0x08,        //   Report Size (8)
0x95, 0x0E,        //   Report Count (14)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x80,        // Usage (Sys Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x02,        //   Report ID (2)
0x19, 0x81,        //   Usage Minimum (Sys Power Down)
0x29, 0x83,        //   Usage Maximum (Sys Wake Up)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x03,        //   Report Count (3)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x05,        //   Report Count (5)
0x81, 0x01,        //   Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x03,        //   Report ID (3)
0x19, 0x00,        //   Usage Minimum (Unassigned)
0x2A, 0xFF, 0x02,  //   Usage Maximum (0x02FF)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x02,  //   Logical Maximum (767)
0x75, 0x10,        //   Report Size (16)
0x95, 0x01,        //   Report Count (1)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,        // Usage (Mouse)
0xA1, 0x01,        // Collection (Application)
0x85, 0x04,        //   Report ID (4)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x7F,  //   Logical Maximum (32767)
0x09, 0x30,        //   Usage (X)
0x09, 0x31,        //   Usage (Y)
0x75, 0x10,        //   Report Size (16)
0x95, 0x02,        //   Report Count (2)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x06, 0x01, 0xFF,  // Usage Page (Vendor Defined 0xFF01)
0x09, 0x01,        // Usage (0x01)
0xA1, 0x01,        // Collection (Application)
0x85, 0x06,        //   Report ID (6)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x09, 0x2F,        //   Usage (0x2F)
0x75, 0x08,        //   Report Size (8)
0x95, 0x03,        //   Report Count (3)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection

// 140 bytes

A lot is going on here! This one combines multiple 'Collections' to pull from multiple different kinds of input types, and also defines multiple HID Report IDs, allowing the device to tell the OS that it's offering a particular kind of report.

  • Report ID 1 provides 8 bits representing keyboard modifiers, and then 14 8-bit values representing pressed keys.
  • Report ID 2 provides three bits representing system controls, followed by five constant padding bits.
  • Report ID 3 provides one 16-bit value representing an input from the 'Consumer Control: Application' category.
  • Report ID 4 provides an absolute mouse position as two unsigned 16-bit values (because apparently one form of mouse movement wasn't enough for this device).
  • Report ID 6 provides data in the vendor-defined category FF01:0001: three 8-bit values.

Note that Report ID 6 is what we saw used back in Part 1 for the mouse to supply notifications to the config tool!

Vendor-Specific Interface Report

The last interface has a super-simple report descriptor with some interesting features... and it'll be immediately obvious why.

0x06, 0x00, 0xFF,  // Usage Page (Vendor Defined 0xFF00)
0x0A, 0x00, 0xFF,  // Usage (0xFF00)
0xA1, 0x01,        // Collection (Application)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x09, 0x20,        //   Usage (0x20)
0x75, 0x08,        //   Report Size (8)
0x95, 0x40,        //   Report Count (64)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x21,        //   Usage (0x21)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x22,        //   Usage (0x22)
0x95, 0x08,        //   Report Count (8)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection

// 32 bytes

Whereas the other reports were exclusively Input reports (data being sent from the device to the host), this one provides multiple kinds. It's got vendor-defined usage FF00:FF00, which you may remember as being used by the control channel for this mouse's configuration commands.

The specifications here corroborate what we already figured out and implemented using hidapi. Feature reports form 8 bytes; these are our main commands and their replies. Input and output reports form 64 bytes; these are the individual blocks of our bulk transfers.

There's a lot to go through here, but now we're in a much better place to figure out how this mouse handles commands. We know, for instance, that Endpoints 3 and 4 are used to transmit the bulk data.

Investigating the Sample Mouse's Commands

Inside the USBReport.asm file, we can find the code that deals with reports. There's a jump table near the start inside a block called GetFeatureReport which appears to handle the equivalent of the 'read data' commands. There's also a block called FeatureCommand which seems like an equivalent to my mouse's 'write data'/'set something' commands. Here's the tables for both...

GetFeatureReport_PCL21:
           ADDM      A,PCL                           
           JMP       GetFirmwareVersion          ;00H
           JMP       GetReportRate               ;01H
           JMP       GetProfileValue             ;02H
           JMP       GetSensorOption             ;03H
           JMP       ToStallPipe0                ;04H
           JMP       ToStallPipe0                ;05H
           JMP       ToStallPipe0                ;06H
           JMP       ToStallPipe0                ;07H
           JMP       ToStallPipe0                ;08H
           JMP       ToStallPipe0                ;09H
           JMP       ToStallPipe0                ;0AH
           JMP       GetDPIStage                 ;0BH
           JMP       GetLEDColor                 ;0CH
           JMP       GetLEDEffect                ;0DH
           JMP       GetXYSensitivity            ;0EH
           JMP       ToStallPipe0                ;0FH
           JMP       ToStallPipe0                ;10H
           JMP       GetDPIStageValue            ;11H
           JMP       GetKeyMatrixValue           ;12H
           JMP       GetMacroKeyValue            ;13H

FeatureCommand_PCL21:
           ADDM      A,PCL                           
           JMP       SetUSBAccessIndex           ;00H
           JMP       SetReportRate               ;01H
           JMP       SetProfileValue             ;02H
           JMP       SetSensorOption             ;03H
           JMP       ToStallPipe0                ;04H
           JMP       ToStallPipe0                ;05H
           JMP       ToStallPipe0                ;06H
           JMP       ToStallPipe0                ;07H
           JMP       SetForceMacroStop           ;08H
           jmp       ToStallPipe0                ;09H
           JMP       ToStallPipe0                ;0AH
           JMP       SetDPIStage                 ;0BH
           JMP       SetLEDColor                 ;0CH
           JMP       SetLEDEffect                ;0DH
           JMP       SetXYSensitivity            ;0EH
           JMP       ToStallPipe0                ;0FH
           JMP       SetLEDColorStageValue       ;10H
           JMP       SetDPIStageValue            ;11H
           JMP       SetKeyMatrixValue           ;12H
           JMP       SetMacroKeyValue            ;13H

It's similar to what my mouse has, but definitely not identical. This sample code uses command 0x13 to set macros, but that's command 0xF on mine. This sample code says that command 0xC sets the LED colours, but I already know that command 0xC writes the configuration on mine. So, the sample code won't really help us here. Time to go look at something else...?

The Next Step

I didn't expect to write this much about USB descriptors, but it just kind of happened...

In part 6, I'm going to be trying to reflash the mouse in an attempt to get it back to fully working order, and writing an IDA processor module for the microcontroller. Stay tuned :3

If you've really enjoyed this series of posts and want to help fund my hot chocolate habit, feel free to send me a couple of pounds: Ko-fi | Monzo.me (UK) | PayPal.me