Wherein I disassemble my mouse's USB command processing code in order to figure out why it's semi-bricked and how to get it into a usable state again. Long read, but hopefully worth it :p

Previous Posts

I've now got myself a processor module for IDA which I can use to disassemble my mouse's firmware and analyse it with a bit more finesse than just repeatedly typing :%s/something/something else/ into VSCode. It's time to delve into it and see what we can find out!

Answering USB Requests

Back in Part 6, I delved into the USB descriptors which the mouse uses to describe how it communicates to the host's OS. This got us some information that'll be quite useful when it comes to figuring this out.

The most important things we need to know here are which endpoints are involved -- those are the communication channels defined by the USB specification. The Holtek HT68FB560 supports eight endpoints, EP0 to EP7, but the mouse only uses five of them.

EP0 is a bi-directional control channel in every USB device. EP1 is used to send mouse data to the host. EP2 is used to send keyboard data to the host (and some other non-keyboard controls that the mouse can pretend to activate). EP3 is used to send configuration data to the host, and EP4 is used to receive configuration data from the host.

With that laid out, we can start looking at the USB interrupt handler.

ROM:1600 USBInterruptHandler:                    # CODE XREF: ROM:Interrupt_USB↑j
ROM:1600                 mov     usbintSaveAcc, A
ROM:1601                 mov     A, B0_STATUS    # Status Register
ROM:1602                 mov     usbintSaveStatus, A
ROM:1603                 snz     B0_USC.USC:SUSP # USB Control
ROM:1604                 jmp     usbi_process
ROM:1605 # ---------------------------------------------------------------------------
ROM:1605
ROM:1605 usb_suspended:                          # CODE XREF: ROM:1603↑j
ROM:1605                 clr     B0_USR          # USB Endpoint Accessed Detection
ROM:1606                 snz     B0_USC.USC:RESUME # USB Control
ROM:1607                 jmp     usbi_exit
ROM:1608 # ---------------------------------------------------------------------------
ROM:1608
ROM:1608 usb_also_resumed:                       # CODE XREF: ROM:1606↑j
ROM:1608                 set     B0_UCC.UCC:USBCKEN # USB Clock Control
ROM:1609                 set     someUSBFlags.deviceHasBeenResumed
ROM:160A                 jmp     usbi_exit
ROM:160B # ---------------------------------------------------------------------------
ROM:160B
ROM:160B usbi_exit_fullRestore:                  # CODE XREF: ROM:just_fullRestore↓j
ROM:160B                                         # ROM:168D↓j
ROM:160B                 mov     A, usbintSaveBP
ROM:160C                 mov     B0_BP, A        # Bank Pointer
ROM:160D                 mov     A, usbintSaveMP0
ROM:160E                 mov     B0_MP0, A       # Memory Pointer 0
ROM:160F                 mov     A, usbintSaveMP1
ROM:1610                 mov     B0_MP1, A       # Memory Pointer 1
ROM:1611                 mov     A, usbintSaveTBHP
ROM:1612                 mov     B0_TBHP, A      # Table Lookup High Pointer
ROM:1613                 mov     A, usbintSaveTBLP
ROM:1614                 mov     B0_TBLP, A      # Table Lookup Low Pointer
ROM:1615
ROM:1615 usbi_exit:                              # CODE XREF: ROM:1607↑j
ROM:1615                                         # ROM:160A↑j
ROM:1615                 mov     A, usbintSaveStatus
ROM:1616                 mov     B0_STATUS, A    # Status Register
ROM:1617                 mov     A, usbintSaveAcc
ROM:1618                 clr     B0_INTC0.INTC0:EMI # Interrupt Control 0
ROM:1619                 set     B0_INTC0.INTC0:USBE # Interrupt Control 0
ROM:161A                 reti

It starts off by saving the current state of the accumulator and status registers, and then doing some busywork with the USC.SUSP and USP.RESUME registers which control the device's suspend/resume features. If the suspend flag is clear, then it jumps to what I've called usbi_process... that's where the real fun happens

Checking the Endpoints

ROM:161B usbi_process:                           # CODE XREF: ROM:1604↑j
ROM:161B                 set     B0_UCC.UCC:USBCKEN # USB Clock Control
ROM:161C                 clr     B0_UCC.UCC:SUSP2 # USB Clock Control
ROM:161D                 mov     A, B0_MP1       # Memory Pointer 1
ROM:161E                 mov     usbintSaveMP1, A
ROM:161F                 mov     A, B0_MP0       # Memory Pointer 0
ROM:1620                 mov     usbintSaveMP0, A
ROM:1621                 mov     A, B0_BP        # Bank Pointer
ROM:1622                 mov     usbintSaveBP, A
ROM:1623                 mov     A, B0_TBLP      # Table Lookup Low Pointer
ROM:1624                 mov     usbintSaveTBLP, A
ROM:1625                 mov     A, B0_TBHP      # Table Lookup High Pointer
ROM:1626                 mov     usbintSaveTBHP, A
ROM:1627                 clr     B0_INTC0.INTC0:USBE # disable USB interrupts
ROM:1628                 set     B0_INTC0.INTC0:EMI # enable global interrupt control
ROM:1629                 sz      B0_USR.USR:EP0F # USB Endpoint Accessed Detection
ROM:162A                 jmp     endpoint0Accessed
ROM:162B # ---------------------------------------------------------------------------
ROM:162B
ROM:162B loc_162B:                               # CODE XREF: ROM:1629↑j
ROM:162B                 sz      B0_USR.USR:EP4F # USB Endpoint Accessed Detection
ROM:162C                 jmp     endpoint4Accessed_specialToDevice
ROM:162D # ---------------------------------------------------------------------------
ROM:162D
ROM:162D handleEndpointsAfter4:                  # CODE XREF: ROM:loc_162B↑j
ROM:162D                                         # ROM:16AF↓j
ROM:162D                 sz      B0_USR.USR:EP1F # USB Endpoint Accessed Detection
ROM:162E                 jmp     endpoint1Accessed_mouseIn
ROM:162F # ---------------------------------------------------------------------------
ROM:162F
ROM:162F handleEndpointsAfter1:                  # CODE XREF: ROM:handleEndpointsAfter4↑j
ROM:162F                                         # ROM:169D↓j
ROM:162F                 sz      B0_USR.USR:EP2F # USB Endpoint Accessed Detection
ROM:1630                 jmp     endpoint2Accessed_keyboardIn
ROM:1631 # ---------------------------------------------------------------------------
ROM:1631
ROM:1631 handleEndpointsAfter2:                  # CODE XREF: ROM:handleEndpointsAfter1↑j
ROM:1631                                         # ROM:16A5↓j
ROM:1631                 sz      B0_USR.USR:EP3F # USB Endpoint Accessed Detection
ROM:1632                 jmp     endpoint3Accessed_specialToHost
ROM:1633 # ---------------------------------------------------------------------------
ROM:1633
ROM:1633 just_fullRestore:                       # CODE XREF: ROM:handleEndpointsAfter2↑j
ROM:1633                                         # ROM:16AA↓j
ROM:1633                 jmp     usbi_exit_fullRestore

This bit of code starts off by setting USBCKEN (the 'USB clock control bit' register) and clearing SUSP2 (the 'reduce power consumption in suspend mode control bit' register). It then saves a few more of the registers so that they can be restored before returning from the interrupt code. Finally, it checks the various flags in the USR register to determine which endpoints have been accessed by the host.

EP0 (the control endpoint) is checked first, followed by EP4 (config to device), EP1 (mouse data to host), EP2 (keyboard data to host) and EP3 (config to host).

Examining EP1: Mouse Data

Let's go back to our Python REPL tricks, but from a slightly different angle this time. There's a neat little Python binding to hidapi, available in the Arch repos as python-hidapi. It'll be useful if we can send HID commands directly and see what comes out - as that way we can examine what works (and what doesn't), and try and activate individual bits of the mouse's functionality.

[ninji@archvm ~]$ sudo ipython
Python 3.7.0 (default, Jul 15 2018, 10:44:58)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.1.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import hid

In [2]: hid.enumerate()
Out[2]:
[{'path': b'0001:0005:00',
  'vendor_id': 1241,
  'product_id': 41240,
  'serial_number': '',
  'release_number': 257,
  'manufacturer_string': '',
  'product_string': 'USB Gaming Mouse',
  'usage_page': 0,
  'usage': 0,
  'interface_number': 0},
 {'path': b'0001:0005:01',
  'vendor_id': 1241,
  'product_id': 41240,
  'serial_number': '',
  'release_number': 257,
  'manufacturer_string': '',
  'product_string': 'USB Gaming Mouse',
  'usage_page': 0,
  'usage': 0,
  'interface_number': 1},
 {'path': b'0001:0005:02',
  'vendor_id': 1241,
  'product_id': 41240,
  'serial_number': '',
  'release_number': 257,
  'manufacturer_string': '',
  'product_string': 'USB Gaming Mouse',
  'usage_page': 0,
  'usage': 0,
  'interface_number': 2},
 {'path': b'0001:0002:00',
  'vendor_id': 33006,
  'product_id': 33,
  'serial_number': '',
  'release_number': 256,
  'manufacturer_string': 'VirtualBox',
  'product_string': 'USB Tablet',
  'usage_page': 0,
  'usage': 0,
  'interface_number': 0}]

In [3]: d = hid.device()

In [4]: d.open_path(b'0001:0005:00')

In [5]: d.read(8)
Out[5]: [1, 0, 0, 0, 0, 0, 0]

In [6]: d.read(8)
Out[6]: [0, 0, 0, 0, 0, 0, 0]

First, I need to call hid.enumerate() so that I can figure out what the path for my device is. There's three entries in the list, differentiated by interface number. We know that interface 0 exposes the mouse, interface 1 exposes the keyboard, and interface 2 exposes the vendor-specific controls. The mouse seems pretty straightforward, so let's test that by opening its path (b'0001:0005:00' in that log).

We know from the descriptors that the mouse has a maximum packet size of 8, so we can try reading 8 bytes. And... just as expected. d.read(8) hangs until we press a button, at which point it returns a response, containing a single 1. Calling it again returns another response containing all zeroes. This sure looks like a mouse report!

In [24]: d.read(8)
Out[24]: [2, 0, 0, 0, 0, 0, 0]

In [25]: d.read(8)
Out[25]: [0, 0, 0, 0, 0, 0, 0]

In [26]: d.read(8)
Out[26]: [4, 0, 0, 0, 0, 0, 0]

In [27]: d.read(8)
Out[27]: [0, 0, 0, 0, 0, 0, 0]

Clicking the right button gets us a 2. Clicking the wheel gets us a 4. Movement doesn't seem to affect it at all, but that's no surprise, given that in this semi-bricked state, the mouse won't actually detect any movement.

We've seen what it behaves like from the host's end, so let's have a look at the device code for handling Endpoint 1. (I've given a bunch of stuff helpful labels that wasn't originally there, so this will make more sense to you than it initially did to me :p)

ROM:168E endpoint1Accessed_mouseIn:              # CODE XREF: ROM:162E↑j
ROM:168E                 set     unk_40A1.b0A1:6
ROM:168F                 sz      reportRateCounter
ROM:1690                 snz     unk_40F2.b0F2:0
ROM:1691
ROM:1691 loc_1691:                               # CODE XREF: ROM:168F↑j
ROM:1691                 jmp     doMouseReport
ROM:1692 # ---------------------------------------------------------------------------
ROM:1692
ROM:1692 loc_1692:                               # CODE XREF: ROM:1690↑j
ROM:1692                 sdz     reportRateCounter
ROM:1693                 jmp     usbi_exit_endpoint1_mouseIn
ROM:1694 # ---------------------------------------------------------------------------
ROM:1694
ROM:1694 doMouseReport:                          # CODE XREF: ROM:loc_1691↑j
ROM:1694                                         # ROM:loc_1692↑j
ROM:1694                 mov     A, reportRate
ROM:1695                 mov     reportRateCounter, A
ROM:1696                 snz     unk_4090.F_USBAccessDownloading
ROM:1697                 sz      unk_4090.F_USBAccessUploading
ROM:1698
ROM:1698 loc_1698:                               # CODE XREF: ROM:1696↑j
ROM:1698                 jmp     usbi_exit_endpoint1_mouseIn
ROM:1699 # ---------------------------------------------------------------------------
ROM:1699
ROM:1699 loc_1699:                               # CODE XREF: ROM:1697↑j
ROM:1699                 sz      nCmdIndex
ROM:169A                 jmp     usbi_exit_endpoint1_mouseIn
ROM:169B # ---------------------------------------------------------------------------
ROM:169B
ROM:169B loc_169B:                               # CODE XREF: ROM:loc_1699↑j
ROM:169B                 jmp     generateMouseData
ROM:169C # ---------------------------------------------------------------------------
ROM:169C
ROM:169C usbi_exit_endpoint1_mouseIn:            # CODE XREF: ROM:076A↑j
ROM:169C                                         # ROM:0770↑j
ROM:169C                                         # ROM:0772↑j
ROM:169C                                         # ROM:1693↑j
ROM:169C                                         # ROM:loc_1698↑j
ROM:169C                                         # ROM:169A↑j
ROM:169C                 clr     B0_USR.USR:EP1F # USB Endpoint Accessed Detection
ROM:169D                 jmp     handleEndpointsAfter1

The control flow here is a bit hard to follow, but let's give it a shot. I've annotated it with some names (F_USBAccessDownloading, F_USBAccessUploading and nCmdIndex) which I discovered by comparing identical code blocks with the Holtek sample code earlier on, and reportRate which I figured out when I looked at the code for command 3 (whose purpose we've already confirmed thanks to my earlier messing around with the TeckNet config tool).

It sets the flag b0A1:6 and then checks two variables. If reportRateCounter is zero or b0F2:0 is clear, it jumps straight to doMouseReport. If neither of these conditions are true, then it decrements reportRateCounter. If the result is zero, it goes ahead to doMouseReport; otherwise, it goes to usbi_exit_endpoint1_mouseIn, where the endpoint status flag is cleared and it jumps back to the main bit that checks all the endpoint flags.

What happens if it gets to doMouseReport? It copies reportRate into reportRateCounter and then checks the F_USBAccessDownloading and F_USBAccessUploading flags. If either of them is true, we bail.

Finally, it checks nCmdIndex. If that variable is zero, it jumps to generateMouseData; otherwise, it bails.

We can tease out a bit of logic here with some thinking. It looks like reportRateCounter serves as a rate-limiting mechanism: the mouse will only consider giving up a report if it reaches zero (i.e. this code path has triggered reportRate times since the last report was generated), or if that b0F2:0 flag is clear.

Even if it gets past that stage, for a report to be generated, F_USBAccessDownloading and F_USBAccessUploading must be clear, and nCmdIndex must be zero.

So, what's inside that generateMouseData label? I've kind of spoiled it already by giving it that name, haven't I? 🤔

ROM:0769 generateMouseData:                      # CODE XREF: ROM:loc_169B↓j
ROM:0769                 snz     updateFlagsMaybe.haveMouseReport
ROM:076A                 jmp     usbi_exit_endpoint1_mouseIn
ROM:076B # ---------------------------------------------------------------------------
ROM:076B
ROM:076B loc_76B:                                # CODE XREF: ROM:generateMouseData↑j
ROM:076B                 call    beginWriteToEP1_mouseIn
ROM:076C                 snz     B0_MISC.MISC:READY # Misc Register
ROM:076D                 jmp     loc_771
ROM:076E # ---------------------------------------------------------------------------
ROM:076E
ROM:076E loc_76E:                                # CODE XREF: ROM:076C↑j
ROM:076E                 call    buildMouseReport
ROM:076F                 call    writeBank0FifoBlockIntoEP1_MouseIn
ROM:0770                 jmp     usbi_exit_endpoint1_mouseIn
ROM:0771 # ---------------------------------------------------------------------------
ROM:0771
ROM:0771 loc_771:                                # CODE XREF: ROM:076D↑j
ROM:0771                 clr     B0_MISC.MISC:REQUEST # Misc Register
ROM:0772                 jmp     usbi_exit_endpoint1_mouseIn

There's a particular variable with a bunch of bits in it that I suspect stores flags about what data has been updated since the mouse last sent a report, as it shows up in both the mouse and keyboard read handlers. I've called it updateFlagsMaybe there.

If the haveMouseReport flag is zero, it bails out. Otherwise, it calls beginWriteToEP1, verifies that the READY register is set, and then calls buildMouseReport and writeBank0FifoBlockIntoEP1.

I'm not going to go into heavy detail on these because I want to focus more on the other endpoints. beginWriteToEP1 does some boring USB register setup to prepare for writing data to EP1. buildMouseReport uses a bunch of configuration variables and global variables to build the report, and places it into an 8-byte array located in Bank 0. writeBank0FifoBlockIntoEP1 completes the write process, using that 8-byte array.

EP2: Keyboard Data

Code-wise, this is mostly more of the same. The report rate guff is missing, and there's multiple different kinds of reports it can generate, but there's nothing particularly exciting at this point. It'll be worth a look later if we want to figure out what capabilities the firmware has with regards to "doing interesting things on your machine", but right now we just want to find a way to unbrick the mouse.

We can try to read from EP2 using hidapi (keeping in mind that it uses Interface 1 and not 0)...

In [96]: d.open_path(b'0001:0005:01')

In [97]: d.read(16)

... but nothing comes out. It just blocks. Presumably, if I had a mouse button mapped to a keyboard key or some other special control, it would show up here, but I've not actually got one. With the mouse in this semi-bricked state, I can't change the settings to add one either. Oops.

EP3: Config Data (to Host)

The very first thing this function does is check the F_USBAccessUploading flag, and if it's zero, it just ignores the request entirely. Note the contrast: while EP1 and EP2 will ignore requests when this flag is set, EP3 is the opposite! Presumably this flag means that the mouse is currently in the process of sending (uploading) some data to the host.

If it's set, then it calls beginWriteToEP3 to set up the USB registers for writing data, and then does some stuff that involves reading data from the flash memory (using an address and size pulled from global variables) into a buffer in Bank 2 and then writing that buffer over the USB FIFO register.

Once it reaches the end, it clears the F_USBAccessUploading flag. Makes sense.

Presumably, what happens is that when you send one of the "I want some data!" commands like 0x8C (get config block), the mouse stores the address/size of the data to read into those globals and sets the F_USBAccessUploading flag. The next time you read from EP3, that data is returned. We'll hopefully confirm this later when we look into the control commands.

EP4: Config Data (to Device)

This one's kind of similar to the logic for EP3. It starts a read operation, and pulls 0x40 bytes down (the maximum packet size that the mouse's descriptors say you can send to it over EP4 - no surprise there) into the buffer in Bank 2.

If F_USBAccessDownloading is zero, it proceeds to entirely ignore that data. Otherwise, it checks a couple of variables to figure out where in the flash memory to write that data.

What's curious is that at first glance, there appears to be five different kinds of data that can be sent over this pipe. We currently only know of three commands that activate a bulk write - 0xC (set config), 0xD (set button mappings) and 0xF (set macro). This is worth further investigation at some point!

EP0: The Big One

There's nothing interesting we can do with the other four endpoints at this point. We've got a reasonable overview on how they work, but to get further, we'll need to delve into EP0 - the control endpoint. This one's the most complex, but understandably so, because so many things go through it.

The first bit of the handler for EP0 is a bit difficult to read as it's full of conditions and register checks, but with heavy commenting we can get an idea of what's going on.

ROM:1634 endpoint0Accessed:                      # CODE XREF: ROM:162A↑j
ROM:1634                 set     B0_USC.USC:URD  # URD = 1: "USB reset signal will reset MCU"
ROM:1635                 clr     B0_USR.USR:EP0F # clear the "host wants to poke EP0" notification
ROM:1636                 sz      someUSBFlags.bWait_Setup # this flag name comes from the sample code
ROM:1637                 jmp     waitSetupIsTrue_or_packetIsZeroLen
ROM:1638 # ---------------------------------------------------------------------------
ROM:1638
ROM:1638 waitSetupIsFalse:                       # CODE XREF: ROM:1636↑j
ROM:1638                 snz     B0_MISC.MISC:LEN0 # did the host send a zero-sized packet? (spooky)
ROM:1639                 jmp     packetIsNotEmpty
ROM:163A # ---------------------------------------------------------------------------
ROM:163A
ROM:163A waitSetupIsTrue_or_packetIsZeroLen:     # CODE XREF: ROM:1637↑j
ROM:163A                                         # ROM:waitSetupIsFalse↑j
ROM:163A                 snz     B0_MISC.MISC:SETCMD # is the data in the FIFO a setup command?
ROM:163B                 jmp     usbi_exit_endpoint0 # if not, then exit
ROM:163C # ---------------------------------------------------------------------------
ROM:163C
ROM:163C loc_163C:                               # CODE XREF: ROM:waitSetupIsTrue_or_packetIsZeroLen↑j
ROM:163C                 jmp     handleSetupCommand
ROM:163D # ---------------------------------------------------------------------------
ROM:163D
ROM:163D packetIsNotEmpty:                       # CODE XREF: ROM:1639↑j
ROM:163D                 snz     B0_MISC.MISC:SETCMD # is the data in the FIFO a setup command?
ROM:163E                 jmp     handleNormalPacket # if not, then go ahead and handle it
ROM:163F # ---------------------------------------------------------------------------
ROM:163F
ROM:163F handleSetupCommand:                     # CODE XREF: ROM:loc_163C↑j
ROM:163F                                         # ROM:packetIsNotEmpty↑j
ROM:163F                 clr     B0_MISC.MISC:SETCMD # clear the setup flag as per datasheet (pg170)
ROM:1640                 clr     B0_MISC.MISC:LEN0 # clear the zero-len packet flag
ROM:1641                 clr     someUSBFlags.bWait_Setup
ROM:1642                 set     someUSBFlags.bSetup_Flag
ROM:1643                 jmp     usbi_exit_endpoint0

The device starts out with bWait_Setup set to true, by one of the early initialisation functions. This appears to stop it from processing anything until it receives a 'setup command' (signified by MISC.SETCMD being on), at which point it clears bWait_Setup and enables bSetup_Flag.

If bWait_Setup is off, the packet is not empty and the packet is not a setup command, then it goes ahead to handleNormalPacket.

ROM:1644 handleNormalPacket:                     # CODE XREF: ROM:163E↑j
ROM:1644                 call    beginReadFromEP0_control # prepare to read data
ROM:1645                 sz      B0_MISC.MISC:READY # Misc Register
ROM:1646                 jmp     ep0IsReady
ROM:1647 # ---------------------------------------------------------------------------
ROM:1647
ROM:1647 ep0IsNotReady:                          # CODE XREF: ROM:1645↑j
ROM:1647                 clr     B0_MISC.MISC:REQUEST # Misc Register
ROM:1648                 jmp     readFromEP0Failed
ROM:1649 # ---------------------------------------------------------------------------
ROM:1649
ROM:1649 ep0IsReady:                             # CODE XREF: ROM:1646↑j
ROM:1649                 call    read8BytesFromEP0 # get ourselves some data
ROM:1649                                         # puts the amount of bytes read into fifoTransferSize
ROM:164A                 sz      someUSBFlags.bSetup_Flag # is bSetup_Flag zero?
ROM:164B                 jmp     parseCommandFromEP0 # if it's set, then do this
ROM:164C # ---------------------------------------------------------------------------
ROM:164C
ROM:164C loc_164C:                               # CODE XREF: ROM:164A↑j
ROM:164C                 sz      nCmdIndex       # is nCmdIndex zero?
ROM:164D                 jmp     handleCmdIndexStuff # if not, then do this
ROM:164E # ---------------------------------------------------------------------------
ROM:164E
ROM:164E loc_164E:                               # CODE XREF: ROM:loc_164C↑j
ROM:164E                 jmp     usbi_exit_endpoint0 # if nCmdIndex was zero and bSetup_Flag was zero, then nothing happens

Things are beginning to pick up: we're now at the point where we receive some data, and hopefully start to consider parsing it! It calls beginReadFromEP0 to set the registers up for reading and then does the usual check on the READY register. If it's true, then it calls read8BytesFromEP0 to fetch 8 bytes into the 8-byte buffer in Bank 0.

Finally, we reach a fork in the road. if bSetup_Flag is set, we jump to parseCommandFromEP0 (there I go spoiling things with my useful labels again). Otherwise, if nCmdIndex is non-zero, we jump to handleCmdIndexStuff. If both of those fail, then nothing happens, and the command is just ignored.

Parsing USB Control Requests

ROM:164F parseCommandFromEP0:                    # CODE XREF: ROM:164B↑j
ROM:164F                 clr     someUSBFlags.bSetup_Flag
ROM:1650                 clr     whatToStall.stall_EP0
ROM:1651                 mov     A, whatToStall
ROM:1652                 mov     B0_STLI, A      # don't stall EP0
ROM:1653                 clr     someUSBFlags.b0F1:5
ROM:1654                 clr     someUSBFlags.b0F1:4
ROM:1655                 clr     nCmdIndex
ROM:1656                 mov     A, 8
ROM:1657                 xor     A, fifoTransferSize # is fifoTransferSize == 8?
ROM:1658                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:1659                 jmp     enableStallForEP0_and_exit # no, so bail out

This section starts out in a pretty straightforward fashion. bSetup_Flag is cleared (hey, that's how we got to this point in the first place!) as are some other flags and the mysterious nCmdIndex. There's a quick check to make sure that we read precisely 8 bytes in the previous call to read8BytesFromEP0 (that function puts the amount of bytes read into global variable fifoTransferSize). If all that passes, we get to the real fun stuff: parsing USB commands. You know what that means?

It's time to dig out the USB 2.0 specification again!

Page 248 (Section 9.3), as pictured above, describes the format of the structure. This will be crucial to figuring out what on earth is going on inside the parsing code (or at least, it'll save us a lot of time and questions).

ROM:165A loc_165A:                               # CODE XREF: ROM:1658↑j
ROM:165A                 mov     A, FIFO_out1    # Read bmRequestType
ROM:165B                 and     A, 1Fh          # Extract the Recipient field
ROM:165C                 sub     A, 3
ROM:165D                 sz      B0_STATUS.STATUS:C # Status Register
ROM:165E                 jmp     enableStallForEP0_and_exit # If Recipient >= 3, bail
ROM:165F # ---------------------------------------------------------------------------
ROM:165F
ROM:165F loc_165F:                               # CODE XREF: ROM:165D↑j
ROM:165F                 sz      FIFO_out1.6     # FIFO_Type
ROM:1660                 jmp     enableStallForEP0_and_exit # If (bmRequestType & 6) != 0, bail
ROM:1660                                         # (In practice, this means that the Type
ROM:1660                                         #  is either 2: Vendor or 3: Reserved)
ROM:1661 # ---------------------------------------------------------------------------
ROM:1661
ROM:1661 loc_1661:                               # CODE XREF: ROM:loc_165F↑j
ROM:1661                 snz     FIFO_out1.5     # If (bmRequestType & 5) != 0, skip
ROM:1662                 jmp     handleStandardRequest # ...This is called if Type == 0 (Standard)
ROM:1663 # ---------------------------------------------------------------------------
ROM:1663
ROM:1663 loc_1663:                               # CODE XREF: ROM:loc_1661↑j
ROM:1663                 jmp     handleClassRequest # ... This is called if Type == 1 (Class)

The first bit of code checks the fields in bmRequestType. If the Recipient specified is within the Reserved range (4...31), the request fails. If the Type specified is Vendor or Reserved, the request fails. Otherwise, we jump to either handleStandardRequest or handleClassRequest depending on the type.

Standard Requests

ROM:16B0 handleStandardRequest:                  # CODE XREF: ROM:1662↑j
ROM:16B0                 mov     A, 5
ROM:16B1                 sz      someUSBFlags.pendingAddressChange
ROM:16B2                 mov     A, 1
ROM:16B3
ROM:16B3 loc_16B3:                               # CODE XREF: ROM:16B1↑j
ROM:16B3                 mov     unk_40DE, A
ROM:16B4                 mov     A, FIFO_out2    # bRequest
ROM:16B5                 sub     A, 13
ROM:16B6                 sz      B0_STATUS.STATUS:C # if bRequest >= 13, fail
ROM:16B7                 jmp     enableStallForEP0_and_exit
ROM:16B8 # ---------------------------------------------------------------------------
ROM:16B8
ROM:16B8 loc_16B8:                               # CODE XREF: ROM:16B6↑j
ROM:16B8                 mov     A, FIFO_out2    # FIFO_Request
ROM:16B9                 sz      B0_ACC.0        # if (bRequest & 1) == 0, skip next instruction
ROM:16BA                 jmp     jumptable_for_standard_req # odd-numbered requests always get through!
ROM:16BB # ---------------------------------------------------------------------------
ROM:16BB
ROM:16BB loc_16BB:                               # CODE XREF: ROM:16B9↑j
ROM:16BB                 snz     FIFO_out1.7     # if ((bRequestType & 0x80) == 1), skip
ROM:16BC                 jmp     enableStallForEP0_and_exit # fail out if top bit is zero (request is host-to-device)
ROM:16BD # ---------------------------------------------------------------------------
ROM:16BD
ROM:16BD jumptable_for_standard_req:             # CODE XREF: ROM:16BA↑j
ROM:16BD                                         # ROM:loc_16BB↑j
ROM:16BD                 addm    A, B0_PCL       # so::
ROM:16BD                                         # we get here if either of two conditions is true:
ROM:16BD                                         # - bRequest is an odd number
ROM:16BD                                         # - bRequestType's top bit is set, indicating that
ROM:16BD                                         #   this is a device-to-host transfer
ROM:16BE # ---------------------------------------------------------------------------
ROM:16BE
ROM:16BE jtbl_16BD_case_00_GET_STATUS:           # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16BE                 jmp     handle_getStatus
ROM:16BF # ---------------------------------------------------------------------------
ROM:16BF
ROM:16BF jtbl_16BD_case_01_CLEAR_FEATURE:        # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16BF                 jmp     handle_clearFeature_and_setFeature
ROM:16C0 # ---------------------------------------------------------------------------
ROM:16C0
ROM:16C0 jtbl_16BD_case_02_Reserved:             # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C0                 jmp     enableStallForEP0_and_exit # reserved 2
ROM:16C1 # ---------------------------------------------------------------------------
ROM:16C1
ROM:16C1 jtbl_16BD_case_03_SET_FEATURE:          # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C1                 jmp     handle_clearFeature_and_setFeature
ROM:16C2 # ---------------------------------------------------------------------------
ROM:16C2
ROM:16C2 jtbl_16BD_case_04_Reserved:             # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C2                 jmp     enableStallForEP0_and_exit # reserved 4
ROM:16C3 # ---------------------------------------------------------------------------
ROM:16C3
ROM:16C3 jtbl_16BD_case_05_SET_ADDRESS:          # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C3                 jmp     handle_setAddress
ROM:16C4 # ---------------------------------------------------------------------------
ROM:16C4
ROM:16C4 jtbl_16BD_case_06_GET_DESCRIPTOR:       # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C4                 jmp     handle_getDescriptor
ROM:16C5 # ---------------------------------------------------------------------------
ROM:16C5
ROM:16C5 jtbl_16BD_case_07_SET_DESCRIPTOR:       # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C5                 jmp     enableStallForEP0_and_exit
ROM:16C6 # ---------------------------------------------------------------------------
ROM:16C6
ROM:16C6 jtbl_16BD_case_08_GET_CONFIGURATION:    # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C6                 jmp     handle_getConfiguration
ROM:16C7 # ---------------------------------------------------------------------------
ROM:16C7
ROM:16C7 jtbl_16BD_case_09_SET_CONFIGURATION:    # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C7                 jmp     handle_setConfiguration
ROM:16C8 # ---------------------------------------------------------------------------
ROM:16C8
ROM:16C8 jtbl_16BD_case_0A_GET_INTERFACE:        # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C8                 jmp     handle_getInterface
ROM:16C9 # ---------------------------------------------------------------------------
ROM:16C9
ROM:16C9 jtbl_16BD_case_0B_SET_INTERFACE:        # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16C9                 jmp     handle_setInterface
ROM:16CA # ---------------------------------------------------------------------------
ROM:16CA
ROM:16CA jtbl_16BD_case_0C_SYNCH_FRAME:          # CODE XREF: ROM:jumptable_for_standard_req↑j
ROM:16CA                 jmp     enableStallForEP0_and_exit

The validation here is a bit odd, but whatever. It checks precisely what request was sent - Table 9-4 (on Page 251) in the USB spec lists the request codes.

Even-numbered requests (GET_STATUS, GET_DESCRIPTOR, GET_CONFIGURATION, GET_INTERFACE, SYNCH_FRAME) are only accepted if sent as device-to-host transfers (the top bit of bRequestType is set to 1).

There's handling for a bunch of interesting-looking USB requests, and thanks to the jump table detection code that I wrote, it was easy to figure out which one was for which. I renamed each label to add the names of the request(s) they handled.

  • GET_STATUS: Can be sent to a device, interface or endpoint. For devices, it tells the host if 'remote wakeup' is enabled. For interfaces, it returns two zero bytes, as required by the spec (exciting!). For endpoints, it tells the host whether the endpoint is currently halted (stalled and not handling data).
  • SET_FEATURE, CLEAR_FEATURE: Lets the host enable/disable remote wakeup (for a device) or enable/disable halt (for an endpoint). There's other things these requests can do as per the USB spec (like enable test modes), but they're optional and this mouse doesn't implement any of them.
  • SET_ADDRESS: Lets the host change the device address used to access the device. Wild?!
  • GET_DESCRIPTOR: Returns the requested descriptor. We already dumped these back in Part 5, so there's nothing new to learn here.
  • GET_CONFIGURATION, SET_CONFIGURATION: On multi-configuration devices this would let the host get and change the active configuration. This isn't one, so all it does is some USB state voodoo.
  • GET_INTERFACE, SET_INTERFACE: Lets the host change the 'alternate setting' for a particular interface. I'm not convinced that this actually gets used at all.

All fairly standard stuff, but nothing mind-blowing. Let's go have a look at the class requests. That's where the more interesting bits will be, as it'll include HID class device-specific stuff and not just plain old USB bookkeeping. (The price you pay for having simple plug-and-play USB devices is that there's a lot of faff going on behind-the-scenes to try and make everything seem seamless. Remember IRQs? :p)

HID Class-Specific Requests

First off, we'll need to pull up section 7.2 (pages 50-51) in the Device Class Definition for HID specification. This tells us what the valid request types are.

ROM:182F handleClassRequest:                     # CODE XREF: ROM:loc_1663↑j
ROM:182F                 mov     A, FIFO_out1    # FIFO_Type
ROM:1830                 and     A, 1Fh          # get bmRequestType recipient
ROM:1831                 addm    A, B0_PCL       # Program Counter Low
ROM:1832 # ---------------------------------------------------------------------------
ROM:1832
ROM:1832 jtbl_1831_case_00:                      # CODE XREF: ROM:1831↑j
ROM:1832                 jmp     enableStallForEP0_and_exit # to Device, not used
ROM:1833 # ---------------------------------------------------------------------------
ROM:1833
ROM:1833 jtbl_1831_case_01:                      # CODE XREF: ROM:1831↑j
ROM:1833                 jmp     handleClass_toInterface
ROM:1834 # ---------------------------------------------------------------------------
ROM:1834
ROM:1834 jtbl_1831_case_02:                      # CODE XREF: ROM:1831↑j
ROM:1834                 jmp     handleClass_toEndpoint
ROM:1835 # ---------------------------------------------------------------------------
ROM:1835
ROM:1835 handleClass_toEndpoint:                 # CODE XREF: ROM:jtbl_1831_case_02↑j
ROM:1835                 sz      FIFO_out5.7     # clear out the high bit of wIndex's low byte
ROM:1836                 clr     FIFO_out5.7     # (an entirely pointless condition, because
ROM:1836                                         #  if FIFO_out5.7 is zero, then clr FIFO_out5
ROM:1836                                         #  is a no-op...)
ROM:1837
ROM:1837 handleClass_toInterface:                # CODE XREF: ROM:jtbl_1831_case_01↑j
ROM:1837                                         # ROM:handleClass_toEndpoint↑j
ROM:1837                 sz      FIFO_out6       # require that the high byte of
ROM:1837                                         # wIndex (Interface number) is zero
ROM:1838                 jmp     enableStallForEP0_and_exit
ROM:1839 # ---------------------------------------------------------------------------
ROM:1839
ROM:1839 loc_1839:                               # CODE XREF: ROM:handleClass_toInterface↑j
ROM:1839                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:183A                 sub     A, 3
ROM:183B                 sz      B0_STATUS.STATUS:C # require that wIndex (interface number) is < 3
ROM:183C                 jmp     enableStallForEP0_and_exit
ROM:183D # ---------------------------------------------------------------------------
ROM:183D
ROM:183D loc_183D:                               # CODE XREF: ROM:183B↑j
ROM:183D                 mov     A, FIFO_out2    # FIFO_Request
ROM:183E                 sub     A, 0Ch
ROM:183F                 sz      B0_STATUS.STATUS:C # require that bRequest is < 0xC
ROM:1840                 jmp     enableStallForEP0_and_exit
ROM:1841 # ---------------------------------------------------------------------------
ROM:1841
ROM:1841 loc_1841:                               # CODE XREF: ROM:183F↑j
ROM:1841                 mov     A, FIFO_out2    # FIFO_Request
ROM:1842                 addm    A, B0_PCL       # Program Counter Low
ROM:1843 # ---------------------------------------------------------------------------
ROM:1843
ROM:1843 jtbl_1842_case_00:                      # CODE XREF: ROM:1842↑j
ROM:1843                 jmp     enableStallForEP0_and_exit
ROM:1844 # ---------------------------------------------------------------------------
ROM:1844
ROM:1844 jtbl_1842_case_01:                      # CODE XREF: ROM:1842↑j
ROM:1844                 jmp     handleClass_getReport
ROM:1845 # ---------------------------------------------------------------------------
ROM:1845
ROM:1845 jtbl_1842_case_02:                      # CODE XREF: ROM:1842↑j
ROM:1845                 jmp     handleClass_getIdle
ROM:1846 # ---------------------------------------------------------------------------
ROM:1846
ROM:1846 jtbl_1842_case_03:                      # CODE XREF: ROM:1842↑j
ROM:1846                 jmp     handleClass_getProtocol
ROM:1847 # ---------------------------------------------------------------------------
ROM:1847
ROM:1847 jtbl_1842_case_04:                      # CODE XREF: ROM:1842↑j
ROM:1847                 jmp     enableStallForEP0_and_exit
ROM:1848 # ---------------------------------------------------------------------------
ROM:1848
ROM:1848 jtbl_1842_case_05:                      # CODE XREF: ROM:1842↑j
ROM:1848                 jmp     enableStallForEP0_and_exit
ROM:1849 # ---------------------------------------------------------------------------
ROM:1849
ROM:1849 jtbl_1842_case_06:                      # CODE XREF: ROM:1842↑j
ROM:1849                 jmp     enableStallForEP0_and_exit
ROM:184A # ---------------------------------------------------------------------------
ROM:184A
ROM:184A jtbl_1842_case_07:                      # CODE XREF: ROM:1842↑j
ROM:184A                 jmp     enableStallForEP0_and_exit
ROM:184B # ---------------------------------------------------------------------------
ROM:184B
ROM:184B jtbl_1842_case_08:                      # CODE XREF: ROM:1842↑j
ROM:184B                 jmp     enableStallForEP0_and_exit
ROM:184C # ---------------------------------------------------------------------------
ROM:184C
ROM:184C jtbl_1842_case_09:                      # CODE XREF: ROM:1842↑j
ROM:184C                 jmp     handleClass_setReport
ROM:184D # ---------------------------------------------------------------------------
ROM:184D
ROM:184D jtbl_1842_case_0A:                      # CODE XREF: ROM:1842↑j
ROM:184D                 jmp     handleClass_setIdle
ROM:184E # ---------------------------------------------------------------------------
ROM:184E
ROM:184E jtbl_1842_case_0B:                      # CODE XREF: ROM:1842↑j
ROM:184E                 jmp     handleClass_setProtocol

Here's the annotated dispatch code. First off, there's a tiny jump table deciding where to jump depending on the recipient specified in bmRequestType. Sending a HID Class request to a Device is not supported, so it stalls EP0 and exits. If sent to an Interface, it jumps to handleClass_toInterface and starts validating the request parameters.

Finally, if sent to an Endpoint, it ... checks to see if the top bit of the wIndex low byte is set, and only if it's set, it clears that bit. This is strange on multiple levels. First off, the check is entirely pointless, since clr would just be a no-op if that bit was clear. Second, according to the HID Class Device specs, interfaces are the only valid recipients for these requests.

I had a look at the Holtek sample mouse source code to see if the same strangeness was present there. Sure enough, it is, and without even an explanation!

;----- Class-Specific request -----------------------------------------------
ClassRequest:
;bmRequestType : Type = Class(1), Recipient =  Interface(1)
           MOV       A,FIFO_Type
           AND       A,Setup_Rev
ClassRequest_PCL4:                     ;!!!!! Need in same page
           ADDM      A,PCL
           JMP       ToStallPipe0      ;Recipient=Device(¤£¥i¯à)
           JMP       InterfaceRev      ;Recipient=Interface
           JMP       EndpointRev       ;Recipient=Endpoint
           PUBLIC    ClassRequest_PCL4
;-----------------------------------------------------------------------------;
EndpointRev:
           SZ        FIFO_wIndexL.7    
           CLR       FIFO_wIndexL.7    
;-----------------------------------------------------------------------------;
;bRequest : specific request.
;wIndexL  : Interface Number
;wIndexH  : 0
InterfaceRev:
           SZ        FIFO_wIndexH       ;High byte for endpoint == 0 ?
           JMP       ToStallPipe0       ;No

           MOV       A,FIFO_wIndexL
           SUB       A,INTERFACE_NO
           SZ        C
           JMP       ToStallPipe0
           ;Check bRequest
           MOV       A,FIFO_Request     ;bRequest
           SUB       A,CLASS_REQUEST_NO ;Over 12 item ?
           SZ        C
           JMP       ToStallPipe0       ;Yes
           MOV       A,FIFO_Request
InterfaceRev_PCL13:                    ;!!!!! Need in same page
           ADDM      A,PCL
           JMP       ToStallPipe0       ;0
           JMP       GetReport          ;1
           JMP       ToStallPipe0       ;2
           JMP       GetProtocol        ;3
           JMP       ToStallPipe0       ;4
           JMP       ToStallPipe0       ;5
           JMP       ToStallPipe0       ;6
           JMP       ToStallPipe0       ;7
           JMP       ToStallPipe0       ;8
           JMP       SetReport          ;9
           JMP       ToStallPipe0       ;A
           JMP       SetProtocol        ;B

There's different names for things, and the TM155 implements GetIdle and SetIdle whereas the sample code doesn't, but otherwise it's practically the same. Weeeeird.

Enough of that though, let's have a look at these requests and see exactly what's going on.

GET_IDLE, SET_IDLE

As per pages 52 and 53 in the HID spec, this pair of requests "is used to limit the reporting frequency of an interrupt in endpoint". Essentially, if you send SET_IDLE, you can ask an interface to go slower. GET_IDLE retrieves the current state of that setting for a particular interface.

ROM:184F handleClass_getIdle:                    # CODE XREF: ROM:jtbl_1842_case_02↑j
ROM:184F                 deca    FIFO_out7       # an optimised way to check that wLength's low byte is 1
ROM:1850                 or      A, FIFO_out8    # and that wLength's high byte is 0
ROM:1851                 or      A, FIFO_out4    # and that wValue's high byte is 0
ROM:1852                 sz      FIFO_out1.7     # and that bmRequestType's data transfer direction is 1 (device to host)
ROM:1853                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:1854
ROM:1854 loc_1854:                               # CODE XREF: ROM:1852↑j
ROM:1854                 jmp     enableStallForEP0_and_exit
ROM:1855 # ---------------------------------------------------------------------------
ROM:1855
ROM:1855 loc_1855:                               # CODE XREF: ROM:1853↑j
ROM:1855                 sz      FIFO_out5       # now, check wIndex to figure out which interface
ROM:1856                 jmp     getIdle_interface_is_not_0
ROM:1857 # ---------------------------------------------------------------------------
ROM:1857
ROM:1857 getIdle_interface_is_0:                 # CODE XREF: ROM:loc_1855↑j
ROM:1857                 mov     A, 0CEh         # is interface 0? HTRAM:2CEh
ROM:1858                 call    ReadFromBank2
ROM:1859                 jmp     loc_185C
ROM:185A # ---------------------------------------------------------------------------
ROM:185A
ROM:185A getIdle_interface_is_not_0:             # CODE XREF: ROM:1856↑j
ROM:185A                 mov     A, 0CFh         # is interface 1? HTRAM:2CFh
ROM:185B                 call    ReadFromBank2
ROM:185C
ROM:185C loc_185C:                               # CODE XREF: ROM:1859↑j
ROM:185C                 jmp     returnSingleByteFifo
ROM:185D # ---------------------------------------------------------------------------
ROM:185D
ROM:185D handleClass_setIdle:                    # CODE XREF: ROM:jtbl_1842_case_0A↑j
ROM:185D                 sz      FIFO_out7       # require length is zero (low byte)
ROM:185E                 jmp     enableStallForEP0_and_exit
ROM:185F # ---------------------------------------------------------------------------
ROM:185F
ROM:185F loc_185F:                               # CODE XREF: ROM:handleClass_setIdle↑j
ROM:185F                 sz      FIFO_out8       # require length is zero (high byte)
ROM:1860                 jmp     enableStallForEP0_and_exit
ROM:1861 # ---------------------------------------------------------------------------
ROM:1861
ROM:1861 loc_1861:                               # CODE XREF: ROM:loc_185F↑j
ROM:1861                 sz      FIFO_out5       # is this interface 0 (mouse) or 1 (keyboard)?
ROM:1862                 jmp     setIdle_interface_is_not_0
ROM:1863 # ---------------------------------------------------------------------------
ROM:1863
ROM:1863 setIdle_interface_is_0:                 # CODE XREF: ROM:loc_1861↑j
ROM:1863                 mov     A, FIFO_out4    # FIFO_wValueH
ROM:1864                 mov     byteToWriteToBank, A
ROM:1865                 mov     A, 0CEh         # ref: HTRAM:02CEh
ROM:1866                 call    WriteToBank2
ROM:1867                 jmp     writeEmptyPacketToControl_maybe
ROM:1868 # ---------------------------------------------------------------------------
ROM:1868
ROM:1868 setIdle_interface_is_not_0:             # CODE XREF: ROM:1862↑j
ROM:1868                 mov     A, FIFO_out4    # FIFO_wValueH
ROM:1869                 mov     byteToWriteToBank, A
ROM:186A                 mov     A, 0CFh         # ref: HTRAM:02CFh
ROM:186B                 call    WriteToBank2
ROM:186C                 jmp     writeEmptyPacketToControl_maybe

Nothing all too fancy going on here. The question is: Can we actually send these commands in our current 'semi-bricked' state? Let's try that and see what happens.

A Brief Interlude For PyUSB (yes, again)

There's a lot of state in here. We can't debug the MCU or examine its memory, so all we can really do is poke at it from the outside and see how it reacts to attempt to gauge what's going on. We know that the mouse is ignoring our vendor-specific commands (which are handled through GET_REPORT and SET_REPORT), but that's all we've figured out so far.

If we can get these commands to do something, then that means every condition that we've reached up to now is out of the picture, and whatever is causing the mouse to ignore commands is occurring inside the report request handlers.

Let's first try sending a GET_IDLE request. As per the HID spec, we need to send a bmRequestType of 10100001 (0xA1), bRequest of GET_IDLE (2), wValue matching the Report ID, wIndex matching an interface number, and wLength of 1. We can see from the disassembly that the Report ID (wValue) doesn't actually get used, so we'll set that to 0.

In [1]: import usb.core, usb.control, usb.util, binascii
In [2]: dev = usb.core.find(idVendor=0x4D9, idProduct=0xA118)
In [3]: dev.ctrl_transfer(0xA1, 2, 0, 0, 1)
Out[3]: array('B', [0])

Hey, we've got a reply! Let's try SET_IDLE next. That's pretty similar, but we set the bmRequestType to 00100001 (0x21), bRequest to SET_IDLE (0xA), wValue to the Duration (high byte) and Report ID (low byte), and we set wLength to zero as we're not expecting a reply.

In [7]: dev.ctrl_transfer(0x21, 0xA, 0x500, 0, 0)
Out[7]: 0
In [8]: dev.ctrl_transfer(0xA1, 2, 0, 0, 1)
Out[8]: array('B', [5])

It works - we've successfully changed the idle parameter for Interface 0, and it will happily return it back to us. So that's one thing we know to work. Our issue must lie inside the handlers for the get/set report requests.

GET_PROTOCOL, SET_PROTOCOL

You might recall how I mentioned in Part 5, when describing the mouse's descriptors, that the mouse and keyboard interfaces declared themselves as being part of the "Boot subclass" and defined a protocol. These two commands allow the host to enable/disable that special protocol and to query the status of that.

There's a reason for this - if you want to read the full details, it's described in Appendix B (pages 59 to 61) and Appendix F (pages 73-77) of the HID class specification. In a nutshell: These protocols allow keyboards and/or mice to communicate using a restricted, pre-defined report format. The intended use case is so that systems like a PC BIOS (haha, remember those) can use a USB keyboard/mouse without having to write a ridiculous amount of code to parse Report descriptors and extract the necessary info from reports.

There's no need to include the Report descriptors for these protocols (unless they are the only kind of report the device supports) - they just sort of exist in the perfect world of USB. The spec's Appendix B tells you what they are, though.

ROM:1813 handleClass_getProtocol:                # CODE XREF: ROM:jtbl_1842_case_03↓j
ROM:1813                 deca    FIFO_out7       # FIFO_wLengthL
ROM:1814                 or      A, FIFO_out3    # FIFO_wValueL
ROM:1815                 or      A, FIFO_out4    # FIFO_wValueH
ROM:1816                 or      A, FIFO_out8    # FIFO_wLengthH
ROM:1817                 sz      FIFO_out1.b0E1:7 # FIFO_Type
ROM:1818                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:1819
ROM:1819 loc_1819:                               # CODE XREF: ROM:1817↑j
ROM:1819                 jmp     enableStallForEP0_and_exit
ROM:181A # ---------------------------------------------------------------------------
ROM:181A
ROM:181A loc_181A:                               # CODE XREF: ROM:1818↑j
ROM:181A                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:181B                 call    sub_867
ROM:181C                 and     A, unk_40F2
ROM:181D                 clr     B0_ACC          # Accumulator
ROM:181E                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:181F                 mov     A, 1
ROM:1820
ROM:1820 loc_1820:                               # CODE XREF: ROM:181E↑j
ROM:1820                 jmp     returnSingleByteFifo
ROM:1821 # ---------------------------------------------------------------------------
ROM:1821
ROM:1821 handleClass_setProtocol:                # CODE XREF: ROM:jtbl_1842_case_0B↓j
ROM:1821                 mov     A, FIFO_out7    # FIFO_wLengthL
ROM:1822                 or      A, FIFO_out8    # FIFO_wLengthH
ROM:1823                 or      A, FIFO_out4    # FIFO_wValueH
ROM:1824                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:1825                 jmp     enableStallForEP0_and_exit
ROM:1826 # ---------------------------------------------------------------------------
ROM:1826
ROM:1826 loc_1826:                               # CODE XREF: ROM:1824↑j
ROM:1826                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:1827                 call    sub_867
ROM:1828                 sz      FIFO_out3       # FIFO_wValueL
ROM:1829                 jmp     loc_182D
ROM:182A # ---------------------------------------------------------------------------
ROM:182A
ROM:182A loc_182A:                               # CODE XREF: ROM:1828↑j
ROM:182A                 cpl     B0_ACC          # Accumulator
ROM:182B                 andm    A, unk_40F2
ROM:182C                 jmp     writeEmptyPacketToControl_maybe
ROM:182D # ---------------------------------------------------------------------------
ROM:182D
ROM:182D loc_182D:                               # CODE XREF: ROM:1829↑j
ROM:182D                 orm     A, unk_40F2
ROM:182E                 jmp     writeEmptyPacketToControl_maybe

The code's not massively different from that for GET_IDLE and SET_IDLE. There's some validation performed on the parameters and then either a value is returned or an update is made in RAM. This does at least give us a meaning for unk_40F2 (memory location [F2h]: it stores which protocol is being used for which interfaces.

GET_REPORT, SET_REPORT

This is where it gets interesting. With any luck, we'll be able to solve the mystery of why this mouse is ignoring commands once and for all.

For a start, let's have a look at GET_REPORT. Page 51 in the spec tells us the format, and it's quite simple - I'll lay it out here so we know what to expect.

bmRequestType must be 10100001 (0xA1), bRequest must be GET_REPORT (1), wValue must have a Report Type in its high byte and a Report ID (or zero) in its low byte, wIndex must be the interface number, and wLength must be the report length. The Report Type is 01 for an Input report, 02 for an Output report and 03 for a Feature report.

Armed with that, we can have a look at the code, and hopefully gain some insight we can use to poke at the device from pyusb.

ROM:186D handleClass_getReport:                  # CODE XREF: ROM:jtbl_1842_case_01↑j
ROM:186D                 mov     A, FIFO_out4    # FIFO_wValueH
ROM:186E                 sub     A, 4
ROM:186F                 sz      FIFO_out1.7     # bmRequestType must have top bit set
ROM:186F                                         # (transfer is device-to-host)
ROM:1870                 sz      B0_STATUS.STATUS:C # wValueH (Report Type) must be <= 3
ROM:1871
ROM:1871 loc_1871:                               # CODE XREF: ROM:186F↑j
ROM:1871                 jmp     enableStallForEP0_and_exit
ROM:1872 # ---------------------------------------------------------------------------
ROM:1872
ROM:1872 loc_1872:                               # CODE XREF: ROM:1870↑j
ROM:1872                 sz      FIFO_out8       # high byte of wLength must be zero
ROM:1873                 jmp     enableStallForEP0_and_exit
ROM:1874 # ---------------------------------------------------------------------------
ROM:1874
ROM:1874 loc_1874:                               # CODE XREF: ROM:loc_1872↑j
ROM:1874                 mov     A, FIFO_out4    # jump based on wValueH (Report Type)
ROM:1875                 addm    A, B0_PCL       # Program Counter Low
ROM:1876 # ---------------------------------------------------------------------------
ROM:1876
ROM:1876 jtbl_1875_case_00:                      # CODE XREF: ROM:1875↑j
ROM:1876                 jmp     enableStallForEP0_and_exit
ROM:1877 # ---------------------------------------------------------------------------
ROM:1877
ROM:1877 jtbl_1875_case_01:                      # CODE XREF: ROM:1875↑j
ROM:1877                 jmp     getReportInput
ROM:1878 # ---------------------------------------------------------------------------
ROM:1878
ROM:1878 jtbl_1875_case_02:                      # CODE XREF: ROM:1875↑j
ROM:1878                 jmp     getReportOutput
ROM:1879 # ---------------------------------------------------------------------------
ROM:1879
ROM:1879 jtbl_1875_case_03:                      # CODE XREF: ROM:1875↑j
ROM:1879                 jmp     getReportFeature

Nothing interesting, just validation. Let's look at the handlers for the different report types.

ROM:062D getReportInput:                         # CODE XREF: ROM:jtbl_1875_case_01↓j
ROM:062D                 jmp     enableStallForEP0_and_exit
ROM:062E # ---------------------------------------------------------------------------
ROM:062E
ROM:062E getReportOutput:                        # CODE XREF: ROM:jtbl_1875_case_02↓j
ROM:062E                 jmp     enableStallForEP0_and_exit

Incredibly useful handlers here - nothing. Cool, that means less code I have to read (and write about)! So it's off to the feature reports.

Getting Feature Reports

getReportFeature starts off by checking to make sure that the low byte of wIndex (the accessed interface) is 2, and if not, it bails out. Makes sense: 2 is the interface that exposes the vendor-specific commands as feature reports, after all.

Then, it checks to see if the top bit in memory location [EFh] is set. If it's set, then it prepares a reply using some global variables (including [EFh]), and then jumps to a bit of code I've called dispatch_5_byte_reply. If not, then it jumps to a bit I've called do_interesting_stuff.

ROM:05F4 getReportFeature:                       # CODE XREF: ROM:jtbl_1875_case_03↓j
ROM:05F4                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:05F5                 xor     A, 2            # interface must be 2 (vendor interface)
ROM:05F6                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:05F7                 jmp     enableStallForEP0_and_exit
ROM:05F8 # ---------------------------------------------------------------------------
ROM:05F8
ROM:05F8 loc_5F8:                                # CODE XREF: ROM:05F6↑j
ROM:05F8                 sz      unk_40EF.b0EF:7
ROM:05F9                 jmp     do_interesting_stuff
ROM:05FA # ---------------------------------------------------------------------------
ROM:05FA
ROM:05FA loc_5FA:                                # CODE XREF: ROM:loc_5F8↑j
ROM:05FA                 mov     A, unk_40EF
ROM:05FB                 mov     FIFO_out1, A    # FIFO_Type
ROM:05FC                 mov     A, unk_408C
ROM:05FD                 mov     FIFO_out2, A    # FIFO_Request
ROM:05FE                 mov     A, ep3BulkAmountRemaining
ROM:05FF                 mov     FIFO_out3, A    # FIFO_wValueL
ROM:0600                 mov     A, blockSrcLow
ROM:0601                 mov     FIFO_out4, A    # FIFO_wValueH
ROM:0602                 mov     A, blockSrcHigh
ROM:0603                 mov     FIFO_out5, A    # FIFO_wIndexL
ROM:0604                 jmp     dispatch_5_byte_reply

If it gets to dispatch_5_byte_reply, that block of code clears the last three bytes of the FIFO buffer, clears [EFh] (if its top bit is set - which it is, in this case), calls an odd function if a particular bit in another variable is set, and then jumps to another block of code which simply sends some bytes through EP0.

ROM:0639 dispatch_2_byte_reply:                  # CODE XREF: ROM:0647↓j
ROM:0639                                         # ROM:064A↓j
ROM:0639                                         # ROM:0655↓j
ROM:0639                                         # ROM:065C↓j
ROM:0639                                         # ROM:0665↓j
ROM:0639                 clr     FIFO_out3       # FIFO_wValueL
ROM:063A
ROM:063A dispatch_3_byte_reply:                  # CODE XREF: ROM:0638↑j
ROM:063A                                         # ROM:064F↓j
ROM:063A                                         # ROM:0678↓j
ROM:063A                 clr     FIFO_out4       # FIFO_wValueH
ROM:063B                 clr     FIFO_out5       # FIFO_wIndexL
ROM:063C
ROM:063C dispatch_5_byte_reply:                  # CODE XREF: ROM:0604↑j
ROM:063C                 clr     FIFO_out6       # FIFO_wIndexH
ROM:063D                 clr     FIFO_out7       # FIFO_wLengthL
ROM:063E                 clr     FIFO_out8       # FIFO_wLengthH
ROM:063F                 sz      unk_40EF.b0EF:7
ROM:0640                 clr     unk_40EF
ROM:0641
ROM:0641 loc_641:                                # CODE XREF: ROM:063F↑j
ROM:0641                 sz      unk_4091.b091:6
ROM:0642                 call    sub_1144
ROM:0643
ROM:0643 loc_643:                                # CODE XREF: ROM:loc_641↑j
ROM:0643                 mov     A, 8
ROM:0644                 jmp     returnAccBytesThroughEP0

So, there's something being generated there. Let's look at the other code path, the one I called do_interesting_stuff.

It checks the USB Access flags (just like the code we looked at earlier for other endpoints) to make sure they're off, and then checks that odd [EFh] variable again. For anything to happen, ([EFh] & 0x7F) must be less than 0x14. Then it writes some values into the first three bytes of the output buffer, and goes somewhere using a jump table based off [EFh] & 0xF.

As it turns out, these are all handlers for the vendor-specific read commands. The ones at 0xC, 0xD and 0xF all lead into functions that prepare a bulk transfer from flash memory using the variables I saw earlier, and set F_USBAccessUploading. That's a pretty dead giveaway there.

ROM:0605 do_interesting_stuff:                   # CODE XREF: ROM:05F9↑j
ROM:0605                 snz     unk_4090.F_USBAccessDownloading
ROM:0606                 sz      unk_4090.F_USBAccessUploading # check that neither of the USB access
ROM:0606                                         # flags are currently set
ROM:0607
ROM:0607 loc_607:                                # CODE XREF: ROM:do_interesting_stuff↑j
ROM:0607                 jmp     enableStallForEP0_and_exit
ROM:0608 # ---------------------------------------------------------------------------
ROM:0608
ROM:0608 loc_608:                                # CODE XREF: ROM:0606↑j
ROM:0608                 mov     A, unk_40EF
ROM:0609                 clr     B0_ACC.7        # Accumulator
ROM:060A                 sub     A, 14h
ROM:060B                 sz      B0_STATUS.STATUS:C # if ([EFh] & 0x7F) >= 0x14, bail
ROM:060C                 jmp     enableStallForEP0_and_exit
ROM:060D # ---------------------------------------------------------------------------
ROM:060D
ROM:060D loc_60D:                                # CODE XREF: ROM:060B↑j
ROM:060D                 mov     A, unk_40EF
ROM:060E                 mov     FIFO_out1, A    # FIFO_Type
ROM:060F                 mov     A, unk_408C
ROM:0610                 mov     FIFO_out2, A    # FIFO_Request
ROM:0611                 clr     FIFO_out3       # FIFO_wValueL
ROM:0612                 mov     A, unk_40EF
ROM:0613                 and     A, 0Fh
ROM:0614                 addm    A, B0_PCL       # Program Counter Low
ROM:0615 # ---------------------------------------------------------------------------
ROM:0615
ROM:0615 jtbl_0614_case_00:                      # CODE XREF: ROM:0614↑j
ROM:0615                 jmp     jtbl_0614_case_target_00
ROM:0616 # ---------------------------------------------------------------------------
ROM:0616
ROM:0616 jtbl_0614_case_01:                      # CODE XREF: ROM:0614↑j
ROM:0616                 jmp     jtbl_0614_case_target_01
ROM:0617 # ---------------------------------------------------------------------------
ROM:0617
ROM:0617 jtbl_0614_case_02:                      # CODE XREF: ROM:0614↑j
ROM:0617                 jmp     jtbl_0614_case_target_02
ROM:0618 # ---------------------------------------------------------------------------
ROM:0618
ROM:0618 jtbl_0614_case_03:                      # CODE XREF: ROM:0614↑j
ROM:0618                 jmp     jtbl_0614_case_target_03_RequestReportRate
ROM:0619 # ---------------------------------------------------------------------------
ROM:0619
ROM:0619 jtbl_0614_case_04:                      # CODE XREF: ROM:0614↑j
ROM:0619                 jmp     jtbl_0614_case_target_04
ROM:061A # ---------------------------------------------------------------------------
ROM:061A
ROM:061A jtbl_0614_case_05:                      # CODE XREF: ROM:0614↑j
ROM:061A                 jmp     jtbl_0614_case_target_05
ROM:061B # ---------------------------------------------------------------------------
ROM:061B
ROM:061B jtbl_0614_case_06:                      # CODE XREF: ROM:0614↑j
ROM:061B                 jmp     enableStallForEP0_and_exit
ROM:061C # ---------------------------------------------------------------------------
ROM:061C
ROM:061C jtbl_0614_case_07:                      # CODE XREF: ROM:0614↑j
ROM:061C                 jmp     enableStallForEP0_and_exit
ROM:061D # ---------------------------------------------------------------------------
ROM:061D
ROM:061D jtbl_0614_case_08:                      # CODE XREF: ROM:0614↑j
ROM:061D                 jmp     enableStallForEP0_and_exit
ROM:061E # ---------------------------------------------------------------------------
ROM:061E
ROM:061E jtbl_0614_case_09:                      # CODE XREF: ROM:0614↑j
ROM:061E                 jmp     enableStallForEP0_and_exit
ROM:061F # ---------------------------------------------------------------------------
ROM:061F
ROM:061F jtbl_0614_case_0A:                      # CODE XREF: ROM:0614↑j
ROM:061F                 jmp     jtbl_0614_case_target_0A
ROM:0620 # ---------------------------------------------------------------------------
ROM:0620
ROM:0620 jtbl_0614_case_0B:                      # CODE XREF: ROM:0614↑j
ROM:0620                 jmp     jtbl_0614_case_target_0B
ROM:0621 # ---------------------------------------------------------------------------
ROM:0621
ROM:0621 jtbl_0614_case_0C:                      # CODE XREF: ROM:0614↑j
ROM:0621                 jmp     jtbl_0614_case_target_0C_RequestConfig
ROM:0622 # ---------------------------------------------------------------------------
ROM:0622
ROM:0622 jtbl_0614_case_0D:                      # CODE XREF: ROM:0614↑j
ROM:0622                 jmp     jtbl_0614_case_target_0D_RequestButtonMappings
ROM:0623 # ---------------------------------------------------------------------------
ROM:0623
ROM:0623 jtbl_0614_case_0E:                      # CODE XREF: ROM:0614↑j
ROM:0623                 jmp     jtbl_0614_case_target_0E
ROM:0624 # ---------------------------------------------------------------------------
ROM:0624
ROM:0624 jtbl_0614_case_0F:                      # CODE XREF: ROM:0614↑j
ROM:0624                 jmp     jtbl_0614_case_target_0F_RequestMacro

This means [EFh] has to contain the command ID we're currently processing. Remember the code for sending commands to the mouse? It first sends a feature report (containing the command), and then gets a feature report (where the reply is obtained). We're looking at the code that handles the second part of that equation. Presumably, the code for handling SET_REPORT will, somewhere, set [EFh] to the command that was sent in.

What's curious about this is that there's no chance for it to fail. If no command was sent in, then it returns a 5-byte reply containing the values of some memory locations.

Rather than poking at it with hidapi, let's try sending the mouse some raw USB commands for this stuff and seeing what happens.

Recall the definition for a valid GET_REPORT command: bmRequestType must be 0xA1, bRequest must be 1, wValue must contain Report Type 3 and Report ID 0, wIndex must be the interface number (2), and wLength must be the report length (8). Let's try that.

In [3]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[3]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

Look at that, we've got something out of the mouse. Presumably, since we didn't send it a command, it's going down the code path at loc_5FA.

However... dispatch_5_byte_reply is setting the last three bytes to zero, and they're not zero here. What's going on?

In [4]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[4]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [5]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[5]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [6]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[6]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [7]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[7]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [8]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[8]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [9]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[9]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [10]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[10]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

In [11]: dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8)
Out[11]: array('B', [88, 213, 253, 150, 161, 164, 142, 206])

I always get the same reply. Even if I unplug the mouse and plug it back in, I still get the same reply. That's both good and bad news: I'm getting some data, which is a good sign, but I have no clue what it means. Let's go read some more code.

Setting Feature Reports

We've had a look at every HID class command so far except for SET_REPORT. The spec informs me on page 52 that the format is as follows: bmRequestType must be 00100001 (0x21), bRequest must be SET_REPORT (9), wValue must be a Report Type and Report ID combined, wIndex must be the interface number, and wLength must be the report length. There's also some data supplied, containing the report itself.

The initial dispatch code is straightforward. It validates that the bmRequestType top bit is clear (i.e. that it's a host-to-device transfer), that the report type is within range and that the data length's high byte is zero. It then jumps elsewhere based on the report type.

ROM:187A handleClass_setReport:                  # CODE XREF: ROM:jtbl_1842_case_09↑j
ROM:187A                 mov     A, FIFO_out4    # read the report type (wValue high byte)
ROM:187B                 sub     A, 4
ROM:187C                 snz     FIFO_out1.7     # if bmRequestType's top bit is set (device-to-host transfer),
ROM:187C                                         # then just stall, no matter what the report type is
ROM:187D                 sz      B0_STATUS.STATUS:C # otherwise this ensures that the report type is <= 3
ROM:187E
ROM:187E loc_187E:                               # CODE XREF: ROM:187C↑j
ROM:187E                 jmp     enableStallForEP0_and_exit
ROM:187F # ---------------------------------------------------------------------------
ROM:187F
ROM:187F loc_187F:                               # CODE XREF: ROM:187D↑j
ROM:187F                 sz      FIFO_out8       # ensure that the wLength high byte is 0
ROM:1880                 jmp     enableStallForEP0_and_exit
ROM:1881 # ---------------------------------------------------------------------------
ROM:1881
ROM:1881 loc_1881:                               # CODE XREF: ROM:loc_187F↑j
ROM:1881                 mov     A, FIFO_out4    # dispatch based off the report type (wValue high byte)
ROM:1882                 addm    A, B0_PCL       # Program Counter Low
ROM:1883 # ---------------------------------------------------------------------------
ROM:1883
ROM:1883 jtbl_1882_case_00:                      # CODE XREF: ROM:1882↑j
ROM:1883                 jmp     enableStallForEP0_and_exit
ROM:1884 # ---------------------------------------------------------------------------
ROM:1884
ROM:1884 jtbl_1882_case_01:                      # CODE XREF: ROM:1882↑j
ROM:1884                 jmp     enableStallForEP0_and_exit
ROM:1885 # ---------------------------------------------------------------------------
ROM:1885
ROM:1885 jtbl_1882_case_02:                      # CODE XREF: ROM:1882↑j
ROM:1885                 jmp     setReportOutput
ROM:1886 # ---------------------------------------------------------------------------
ROM:1886
ROM:1886 jtbl_1882_case_03:                      # CODE XREF: ROM:1882↑j
ROM:1886                 jmp     setReportFeature

There's nothing for Input reports, for good reason.

For Output reports, depending on wIndex (the Interface that the report has been sent to), nCmdIndex is set to 1 or 2, and then we exit. Straightforward.

ROM:0679 setReportOutput:                        # CODE XREF: ROM:jtbl_1882_case_02↓j
ROM:0679                 mov     A, 1
ROM:067A                 sz      FIFO_out5       # FIFO_wIndexL
ROM:067B                 mov     A, 2
ROM:067C
ROM:067C loc_67C:                                # CODE XREF: ROM:067A↑j
ROM:067C                 mov     nCmdIndex, A    # sets cmdIndex = 1 or 2 depending on index
ROM:067D                 jmp     usbi_exit_endpoint0

For Feature reports, we require that wIndex is 2 (the vendor-specific stuff interface), and if it is, then we set nCmdIndex to 3 and exit. Also pretty straightforward.

ROM:067E setReportFeature:                       # CODE XREF: ROM:jtbl_1882_case_03↓j
ROM:067E                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:067F                 xor     A, 2            # wIndex (Interface) must be 2 (Vendor)?
ROM:0680                 snz     B0_STATUS.STATUS:Z # Status Register
ROM:0681                 jmp     enableStallForEP0_and_exit
ROM:0682 # ---------------------------------------------------------------------------
ROM:0682
ROM:0682 loc_682:                                # CODE XREF: ROM:0680↑j
ROM:0682                 mov     A, 3
ROM:0683                 mov     nCmdIndex, A
ROM:0684                 jmp     usbi_exit_endpoint0

Where have we seen nCmdIndex used before...? Back at the beginning of the code for handling data arriving over EP0!

The pieces of the puzzle are fitting together and we can now put together a rough outline of what happens when you send the device a vendor command:

  • Host sends an 8-byte SET_REPORT request
  • Since bSetupFlag is set, it's parsed by the device as a USB request, which clears bSetupFlag first thing
  • Inside the processing for SET_REPORT, nCmdIndex is set to 1, 2 or 3 depending on the kind of report
  • Host sends an 8-byte payload for that command
  • Since bSetupFlag is clear and nCmdIndex is non-zero, the device jumps to handleCmdIndexStuff
  • (Some stuff we haven't figured out yet, [EFh] probably gets set in here)
  • Host sends an 8-byte GET_REPORT request
  • Inside the processing for GET_REPORT, a reply is produced, and the cycle is completed

Handling the SET_REPORT Payload

Let's look at handleCmdIndexStuff. It starts off in a typical way, checking nCmdIndex and jumping somewhere depending on what it is. I've helpfully named each label here.

ROM:0685 handleCmdIndexStuff:                    # CODE XREF: ROM:164D↓j
ROM:0685                 mov     A, nCmdIndex
ROM:0686                 sub     A, 4
ROM:0687                 sz      B0_STATUS.STATUS:C # checks that nCmdIndex <= 3
ROM:0688                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:0689 # ---------------------------------------------------------------------------
ROM:0689
ROM:0689 loc_689:                                # CODE XREF: ROM:0687↑j
ROM:0689                 mov     A, nCmdIndex
ROM:068A                 addm    A, B0_PCL       # Program Counter Low
ROM:068B # ---------------------------------------------------------------------------
ROM:068B
ROM:068B jtbl_068A_case_00:                      # CODE XREF: ROM:068A↑j
ROM:068B                 jmp     usbi_exit_endpoint0
ROM:068C # ---------------------------------------------------------------------------
ROM:068C
ROM:068C jtbl_068A_case_01:                      # CODE XREF: ROM:068A↑j
ROM:068C                 jmp     handlesSetOutputReportToMouse
ROM:068D # ---------------------------------------------------------------------------
ROM:068D
ROM:068D jtbl_068A_case_02:                      # CODE XREF: ROM:068A↑j
ROM:068D                 jmp     handlesSetOutputReportToKB
ROM:068E # ---------------------------------------------------------------------------
ROM:068E
ROM:068E jtbl_068A_case_03:                      # CODE XREF: ROM:068A↑j
ROM:068E                 jmp     handlesSetFeatureReport

handlesSetOutputReportToMouse takes a single value from the Bank 0 FIFO buffer and writes it to a particular address in Bank 2. This probably won't help us...

handlesSetOutputReportToKB just immediately clears nCmdIndex and sends an empty acknowledgement packet over EP0.

That finally takes us to handlesSetFeatureReport, where command processing happens. Will this finally help us solve the mystery as to why this mouse won't accept any commands?? We've not seen any obvious roadblocks up to now, everything seems logical...

The Vendor-Specific Command Parser

The logic here goes as follows:

  • if a particular global flag is set, call sub_115D
  • calculate the sum of all the bytes in the buffer
  • if the result is not 0xFF, bail out (hey, this looks like the rudimentary checksum we saw the config tool adding into commands!)
  • if either of the USB Access flags is set, bail out (so we can't interrupt transfers)
  • store the command ID into [EFh] (we were expecting that)
  • if the command ID has the top bit set (host expects a reply), jump to jtbl_06AA_case_target_00
  • otherwise, calculate command_ID & 0xF and go handle a command based off that
ROM:0698 handlesSetFeatureReport:                # CODE XREF: ROM:jtbl_068A_case_03↑j
ROM:0698                 sz      unk_4091.b091:6
ROM:0699                 call    sub_115D
ROM:069A
ROM:069A loc_69A:                                # CODE XREF: ROM:handlesSetFeatureReport↑j
ROM:069A                 call    sumAllBytesInFifoBuffer
ROM:069B                 siz     B0_ACC          # increment A, skip if result is zero
ROM:069B                                         # this appears to implement the 'checksum' mechanism
ROM:069C                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:069D # ---------------------------------------------------------------------------
ROM:069D
ROM:069D loc_69D:                                # CODE XREF: ROM:069B↑j
ROM:069D                 snz     unk_4090.F_USBAccessDownloading
ROM:069E                 sz      unk_4090.F_USBAccessUploading # USBAccess flags must be off
ROM:069F
ROM:069F loc_69F:                                # CODE XREF: ROM:loc_69D↑j
ROM:069F                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:06A0 # ---------------------------------------------------------------------------
ROM:06A0
ROM:06A0 loc_6A0:                                # CODE XREF: ROM:069E↑j
ROM:06A0                 mov     A, FIFO_out1    # load command ID
ROM:06A1                 mov     unk_40EF, A     # store the command ID for later consumption
ROM:06A1                                         # (i.e. by the GET_REPORT handler)
ROM:06A2                 sz      unk_40EF.7      # if the top bit is clear (i.e. this command has no reply), skip
ROM:06A3                 jmp     jtbl_06AA_case_target_00 # if the command has a reply, jump here
ROM:06A4 # ---------------------------------------------------------------------------
ROM:06A4
ROM:06A4 cmd_has_no_reply:                       # CODE XREF: ROM:06A2↑j
ROM:06A4                 mov     A, unk_40EF
ROM:06A5                 sub     A, 14h
ROM:06A6                 sz      B0_STATUS.STATUS:C # command ID must be <= 0x13
ROM:06A7                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:06A8 # ---------------------------------------------------------------------------
ROM:06A8
ROM:06A8 loc_6A8:                                # CODE XREF: ROM:06A6↑j
ROM:06A8                 mov     A, unk_40EF
ROM:06A9                 and     A, 0Fh          # calculate (command ID & 0xF)
ROM:06AA                 addm    A, B0_PCL       # Program Counter Low
ROM:06AB # ---------------------------------------------------------------------------
ROM:06AB
ROM:06AB jtbl_06AA_case_00:                      # CODE XREF: ROM:06AA↑j
ROM:06AB                 jmp     jtbl_06AA_case_target_00
ROM:06AC # ---------------------------------------------------------------------------
ROM:06AC
ROM:06AC jtbl_06AA_case_01:                      # CODE XREF: ROM:06AA↑j
ROM:06AC                 jmp     jtbl_06AA_case_target_01
ROM:06AD # ---------------------------------------------------------------------------
ROM:06AD
ROM:06AD jtbl_06AA_case_02:                      # CODE XREF: ROM:06AA↑j
ROM:06AD                 jmp     jtbl_06AA_case_target_02
ROM:06AE # ---------------------------------------------------------------------------
ROM:06AE
ROM:06AE jtbl_06AA_case_03:                      # CODE XREF: ROM:06AA↑j
ROM:06AE                 jmp     jtbl_06AA_case_target_03_setReportRate
ROM:06AF # ---------------------------------------------------------------------------
ROM:06AF
ROM:06AF jtbl_06AA_case_04:                      # CODE XREF: ROM:06AA↑j
ROM:06AF                 jmp     jtbl_06AA_case_target_04
ROM:06B0 # ---------------------------------------------------------------------------
ROM:06B0
ROM:06B0 jtbl_06AA_case_05:                      # CODE XREF: ROM:06AA↑j
ROM:06B0                 jmp     jtbl_06AA_case_target_05
ROM:06B1 # ---------------------------------------------------------------------------
ROM:06B1
ROM:06B1 jtbl_06AA_case_06:                      # CODE XREF: ROM:06AA↑j
ROM:06B1                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:06B2 # ---------------------------------------------------------------------------
ROM:06B2
ROM:06B2 jtbl_06AA_case_07:                      # CODE XREF: ROM:06AA↑j
ROM:06B2                 jmp     clearCmdIndexAndWriteEmptyPacket
ROM:06B3 # ---------------------------------------------------------------------------
ROM:06B3
ROM:06B3 jtbl_06AA_case_08:                      # CODE XREF: ROM:06AA↑j
ROM:06B3                 jmp     jtbl_06AA_case_target_08
ROM:06B4 # ---------------------------------------------------------------------------
ROM:06B4
ROM:06B4 jtbl_06AA_case_09:                      # CODE XREF: ROM:06AA↑j
ROM:06B4                 jmp     jtbl_06AA_case_target_09
ROM:06B5 # ---------------------------------------------------------------------------
ROM:06B5
ROM:06B5 jtbl_06AA_case_0A:                      # CODE XREF: ROM:06AA↑j
ROM:06B5                 jmp     jtbl_06AA_case_target_0A
ROM:06B6 # ---------------------------------------------------------------------------
ROM:06B6
ROM:06B6 jtbl_06AA_case_0B:                      # CODE XREF: ROM:06AA↑j
ROM:06B6                 jmp     jtbl_06AA_case_target_0B
ROM:06B7 # ---------------------------------------------------------------------------
ROM:06B7
ROM:06B7 jtbl_06AA_case_0C:                      # CODE XREF: ROM:06AA↑j
ROM:06B7                 jmp     jtbl_06AA_case_target_0C_writeConfig
ROM:06B8 # ---------------------------------------------------------------------------
ROM:06B8
ROM:06B8 jtbl_06AA_case_0D:                      # CODE XREF: ROM:06AA↑j
ROM:06B8                 jmp     jtbl_06AA_case_target_0D_writeButtonMappings
ROM:06B9 # ---------------------------------------------------------------------------
ROM:06B9
ROM:06B9 jtbl_06AA_case_0E:                      # CODE XREF: ROM:06AA↑j
ROM:06B9                 jmp     jtbl_06AA_case_target_0E
ROM:06BA # ---------------------------------------------------------------------------
ROM:06BA
ROM:06BA jtbl_06AA_case_0F:                      # CODE XREF: ROM:06AA↑j
ROM:06BA                 jmp     jtbl_06AA_case_target_0F_writeMacros

All of that seems solid. That optional function call at the start is strange, though. What's in it?

The Case of sub_115D

ROM:115D sub_115D:                               # CODE XREF: ROM:0699↑p
ROM:115D                 mov     A, 0E1h
ROM:115E                 mov     B0_MP0, A       # Memory Pointer 0
ROM:115F                 mov     A, 45h
ROM:1160                 call    sub_117A
ROM:1161                 mov     A, 2Dh
ROM:1162                 call    sub_117A
ROM:1163                 mov     A, 53h
ROM:1164                 call    sub_117A
ROM:1165                 mov     A, 69h
ROM:1166                 call    sub_117A
ROM:1167                 mov     A, 67h
ROM:1168                 call    sub_117A
ROM:1169                 mov     A, 4Eh
ROM:116A                 call    sub_117A
ROM:116B                 mov     A, 61h
ROM:116C                 call    sub_117A
ROM:116D                 mov     A, 6Ch
ROM:116E                 call    sub_117A
ROM:116F                 call    sub_11B7
ROM:1170                 call    sub_11B7
ROM:1171                 call    sub_1197
ROM:1172                 call    sub_1181
ROM:1173                 call    sub_11B7
ROM:1174                 call    sub_11B7
ROM:1175                 ret
ROM:1175 # End of function sub_115D

First the memory pointer register is set to 0E1h, which is the address in data memory of the Bank 0 buffer - that's where the command's payload is currently located. There's eight calls to the same function with different parameters that look suspiciously like ASCII. They decode to the string "E-SigNal".

What's happening inside those?

ROM:117A sub_117A:                               # CODE XREF: sub_115D+3↑p
ROM:117A                                         # sub_115D+5↑p
ROM:117A                                         # sub_115D+7↑p
ROM:117A                                         # sub_115D+9↑p
ROM:117A                                         # sub_115D+B↑p
ROM:117A                                         # sub_115D+D↑p
ROM:117A                                         # sub_115D+F↑p
ROM:117A                                         # sub_115D+11↑p
ROM:117A                 swap    B0_ACC          # Accumulator
ROM:117B                 sub     A, B0_IAR0      # Indirect Addressing Register 0
ROM:117C                 cpl     B0_ACC          # Accumulator
ROM:117D                 inc     B0_ACC          # Accumulator
ROM:117E                 mov     B0_IAR0, A      # Indirect Addressing Register 0
ROM:117F                 inc     B0_MP0          # Memory Pointer 0
ROM:1180                 ret

Not used anywhere else. Let's translate that into pseudo-C just to make it a bit clearer.

void sub_117A(uint8_t v) {
    v = (v >> 4) | (v << 4); // swap the nibbles in this byte
    v -= *MP0;
    v = ~v;
    v++;
    *MP0 = v;
    MP0++;
}

See the context for how this function is being used. MP0 is set to point to the Bank 0 buffer. Each call to sub_117A manipulates a single byte in the buffer, using the supplied parameter in A, and increments MP0. Eight calls, eight bytes in the buffer. This looks like a really rudimentary encryption scheme!

What if the nonsense we got from our attempts at sending GET_REPORT requests ([88, 213, 253, 150, 161, 164, 142, 206]) was actually just a completely sensible response, encrypted using this scheme? Let's try implementing this in Python.

In [18]: def decrypt_byte(data_byte, key_byte):
    ...:     key_byte = ((key_byte >> 4) | (key_byte << 4)) & 0xFF
    ...:     result = (key_byte - data_byte) & 0xFF
    ...:     result = (result ^ 0xFF) + 1
    ...:     return (result & 0xFF)
    ...:

In [19]: def decrypt_packet(packet):
    ...:     return bytearray([decrypt_byte(d, k) for (d, k) in zip(packet, b'E-SigNal')])
    ...:

In [20]: decrypt_packet(dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8))
Out[20]: bytearray(b'\x04\x03\xc8\x00+\xc0x\x08')

That's looking more like a reasonable response. Not there yet - we expect the last three bytes to be zero - but it's progress! There's a few more bits and pieces we need to figure out, though. The decryption function calls sub_11B7 twice, calls sub_1197 once, calls sub_1181 once, and then calls sub_11B7 twice again.

sub_11B7

ROM:11B7 sub_11B7:                               # CODE XREF: sub_115D+12↑p
ROM:11B7                                         # sub_115D+13↑p
ROM:11B7                                         # sub_115D+16↑p
ROM:11B7                                         # sub_115D+17↑p
ROM:11B7                 clr     B0_STATUS.STATUS:C # Status Register
ROM:11B8                 rrc     FIFO_out1       # FIFO_Type
ROM:11B9                 rrc     FIFO_out2       # FIFO_Request
ROM:11BA                 rrc     FIFO_out3       # FIFO_wValueL
ROM:11BB                 rrc     FIFO_out4       # FIFO_wValueH
ROM:11BC                 rrc     FIFO_out5       # FIFO_wIndexL
ROM:11BD                 rrc     FIFO_out6       # FIFO_wIndexH
ROM:11BE                 rrc     FIFO_out7       # FIFO_wLengthL
ROM:11BF                 rrc     FIFO_out8       # FIFO_wLengthH
ROM:11C0                 sz      B0_STATUS.STATUS:C # Status Register
ROM:11C1                 set     FIFO_out1.b0E1:7 # FIFO_Type
ROM:11C2
ROM:11C2 loc_11C2:                               # CODE XREF: sub_11B7+9↑j
ROM:11C2                 ret
ROM:11C2 # End of function sub_11B7

This function rotates the entire array to the right by one bit. The bottom bit of the last element becomes the top bit of the first element. Fairly standard stuff, though it's gonna be a bit clunky to implement this in Python. We'll make it work.

sub_1181

ROM:1181 sub_1181:                               # CODE XREF: sub_1144+2↑p
ROM:1181                                         # sub_115D+15↑p
ROM:1181                 mov     A, FIFO_out7    # FIFO_wLengthL
ROM:1182                 xorm    A, FIFO_out1    # FIFO_Type
ROM:1183                 xor     A, FIFO_out1    # FIFO_Type
ROM:1184                 mov     FIFO_out7, A    # FIFO_wLengthL
ROM:1185                 xorm    A, FIFO_out1    # FIFO_Type
ROM:1186                 mov     A, FIFO_out5    # FIFO_wIndexL
ROM:1187                 xorm    A, FIFO_out2    # FIFO_Request
ROM:1188                 xor     A, FIFO_out2    # FIFO_Request
ROM:1189                 mov     FIFO_out5, A    # FIFO_wIndexL
ROM:118A                 xorm    A, FIFO_out2    # FIFO_Request
ROM:118B                 mov     A, FIFO_out8    # FIFO_wLengthH
ROM:118C                 xorm    A, FIFO_out3    # FIFO_wValueL
ROM:118D                 xor     A, FIFO_out3    # FIFO_wValueL
ROM:118E                 mov     FIFO_out8, A    # FIFO_wLengthH
ROM:118F                 xorm    A, FIFO_out3    # FIFO_wValueL
ROM:1190                 mov     A, FIFO_out6    # FIFO_wIndexH
ROM:1191                 xorm    A, FIFO_out4    # FIFO_wValueH
ROM:1192                 xor     A, FIFO_out4    # FIFO_wValueH
ROM:1193                 mov     FIFO_out6, A    # FIFO_wIndexH
ROM:1194                 xorm    A, FIFO_out4    # FIFO_wValueH
ROM:1195                 ret
ROM:1195 # End of function sub_1181

This ugly mess is actually a variant of an old party trick for swapping two values without any temporary storage through judicious usage of XOR -- applied four times in a row! The first five instructions can be directly translated to pseudo-C as:

A = buffer[6];
buffer[0] ^= A;
A ^= buffer[0];
buffer[6] = A;
buffer[0] ^= A;

Essentially this first swaps (zero-based) indices 0 and 6, then 1 and 4, then 2 and 7, then 3 and 5.

sub_1197

Last but not least is this awkward function which XORs the whole thing against some data from flash memory.

ROM:1197 sub_1197:                               # CODE XREF: sub_1144+3↑p
ROM:1197                                         # sub_115D+14↑p
ROM:1197                 mov     A, 20h
ROM:1198                 mov     B0_TBHP, A      # Table Lookup High Pointer
ROM:1199                 mov     A, 4
ROM:119A                 mov     B0_TBLP, A      # load ROM:2004h into table pointer
ROM:119B                 mov     A, 0E1h
ROM:119C                 mov     B0_MP0, A       # load HTRAM:0E1h (start of Bank 0 FIFO buffer) into MP0
ROM:119D                 mov     A, 4
ROM:119E                 mov     amountToPutIntoFifo, A # iteration count: 4
ROM:119F                 clr     B0_INTC0.INTC0:EMI # disable interrupts
ROM:11A0
ROM:11A0 doItAgain:                              # CODE XREF: sub_1197+11↓j
ROM:11A0                 tabrd   B0_ACC          # read a word
ROM:11A1                 xorm    A, B0_IAR0      # *MP0 ^= the low byte of that word
ROM:11A2                 inc     B0_MP0          # MP0++
ROM:11A3                 mov     A, B0_TBLH      # Table Lookup High Result
ROM:11A4                 xorm    A, B0_IAR0      # *MP0 ^= the high byte of that word
ROM:11A5                 inc     B0_MP0          # MP0++
ROM:11A6                 inc     B0_TBLP         # table pointer ++
ROM:11A7                 sdz     amountToPutIntoFifo # loop four times
ROM:11A8                 jmp     doItAgain
ROM:11A9 # ---------------------------------------------------------------------------
ROM:11A9
ROM:11A9 loc_11A9:                               # CODE XREF: sub_1197+10↑j
ROM:11A9                 set     B0_INTC0.INTC0:EMI # re-enable interrupts
ROM:11AA                 ret
ROM:11AA # End of function sub_1197

The data at address 0x2004 is all zeroes, so we can probably get away with leaving this out of our reimplementation for now. Strange. Anyway, let's implement the other functions and see if that gets us closer to something readable!

In [21]: def apply_xor_key(arr):
    ...:     return arr   # for now
    ...:

In [22]: def rotate_array_right(arr):
    ...:     carry = arr[-1] & 1
    ...:     for i in range(len(arr)):
    ...:         new_carry = arr[i] & 1
    ...:         arr[i] = (arr[i] >> 1) | (carry << 7)
    ...:         carry = new_carry
    ...:     return arr
    ...:

In [23]: def swap_indices(arr):
    ...:     return bytearray([arr[i] for i in (6, 4, 7, 5, 1, 3, 0, 2)])
    ...:

In [24]: def decrypt_packet(packet):
    ...:     arr = bytearray([decrypt_byte(d, k) for (d, k) in zip(packet, b'E-SigNal')])
    ...:     arr = rotate_array_right(rotate_array_right(arr))
    ...:     arr = apply_xor_key(arr)
    ...:     arr = swap_indices(arr)
    ...:     return rotate_array_right(rotate_array_right(arr))
    ...:
    ...:

In [25]: decrypt_packet(dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8))
Out[25]: bytearray(b'\x87\x82\x80\xbc\x00\x00\x00|')

That looks more sensible, but we're still not quite there. The missing piece must be the XOR key. It starts off as all zeroes, but is there something that modifies it? We know it comes from offset 0x2004. What's around that area?

Finding the Encryption Key

ROM:2000                 DW    8Dh
ROM:2001                 DW    3Fh # ?
ROM:2002                 DW 0AA33h # 3
ROM:2003                 DW  0BFEh
ROM:2004 encryptionKey:  DW 0, 0, 0, 0           # 0
ROM:2008                 DW   408h
ROM:2009                 DW   102h
ROM:200A                 DW   603h
ROM:200B                 DW   70Ch
ROM:200C                 DW   0FFh
ROM:200D                 DW      0
ROM:200E                 DW   0FFh
ROM:200F                 DW      0
ROM:2010                 DW 0FFFFh
ROM:2011                 DW 0FF00h
ROM:2012                 DW 0FF00h
ROM:2013                 DW 0FFFFh
ROM:2014                 DW 0FF00h
ROM:2015                 DW 0A0A0h
ROM:2016                 DW 0FFA0h
ROM:2017                 DW 0FFFFh
ROM:2018                 DW  0A2Eh # .
ROM:2019                 DW    42h # B
ROM:201A                 DW      0
ROM:201B                 DW      0
ROM:201C                 DW      0
ROM:201D                 DW      0
ROM:201E                 DW      0
ROM:201F                 DW      0

Hmm. That looks suspiciously like a default version of the configuration block. Back in Part 4, I dumped mine using a little C++ tool I wrote using hidapi, and my dump began as follows: 8d,00,01,00,33,aa,fe,0b,00,00,00,00,00,00,00,00

Taking into account the fact that the disassembly shows it as words (decoded as little-endian) and not as bytes, we see 8d,00 becoming DW 8Dh, 01,00 becoming DW 3Fh (probably a setting I changed from the default), 33,aa becoming DW 0AA33h and fe,0b becoming DW 0BFEh. Then, there's eight zero bytes, precisely where we would expect the encryption key to be.

When I semi-bricked the mouse (also in Part 4) by playing with the config tool too much, I noticed that sometimes it would write block D (the button mapping block) over the top of block C (the configuration block). I've got a dump of my block D, and as far as I know, it hasn't changed. It begins as follows: 01,00,f0,00,01,00,f1,00,01,00,f2,00,0a,f0,1e,02

If I assume that this is the same kind of failure, and block D was written to the flash at address 0x2000, then my encryption key should be 01,00,f2,00,0a,f0,1e,02. Let's give that a shot.

In [28]: XOR_KEY = b'\x01\x00\xf2\x00\x0a\xf0\x1e\x02'

In [29]: def apply_xor_key(arr):
    ...:     return bytearray([d ^ k for (d, k) in zip(arr, XOR_KEY)])
    ...:

In [30]: decrypt_packet(dev.ctrl_transfer(0xA1, 1, 0x300, 2, 8))
Out[30]: bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00')

All zeroes. That's got to be more than just a coincidence - I've probably found the correct key. All that remains to do is to write some encryption code, and I'll be able to see if I can send commands to the mouse again. Luckily, there is such a counterpart in the firmware: it's sub_1144, as called by the code that prepares a reply.

ROM:1144 sub_1144:                               # CODE XREF: ROM:0642↑p
ROM:1144                 call    sub_11AB
ROM:1145                 call    sub_11AB
ROM:1146                 call    swap_indices
ROM:1147                 call    apply_xor_key
ROM:1148                 call    sub_11AB
ROM:1149                 call    sub_11AB
ROM:114A                 mov     A, 0E1h         # Offset to the FIFO buffer
ROM:114B                 mov     B0_MP0, A       # Memory Pointer 0
ROM:114C                 mov     A, 'E'
ROM:114D                 call    sub_1176
ROM:114E                 mov     A, '-'
ROM:114F                 call    sub_1176
ROM:1150                 mov     A, 'S'
ROM:1151                 call    sub_1176
ROM:1152                 mov     A, 'i'
ROM:1153                 call    sub_1176
ROM:1154                 mov     A, 'g'
ROM:1155                 call    sub_1176
ROM:1156                 mov     A, 'N'
ROM:1157                 call    sub_1176
ROM:1158                 mov     A, 'a'
ROM:1159                 call    sub_1176
ROM:115A                 mov     A, 'l'
ROM:115B                 call    sub_1176
ROM:115C                 ret
ROM:115C # End of function sub_1144

There's the two functions we already found, and two new ones. sub_11AB just rotates everything left, so I won't bother going into it. sub_1176 nibble-swaps the parameter and adds it to the value in the buffer. Given this knowledge, it's pretty easy to write an encryption function based on what we already did to decrypt.

In [37]: def rotate_array_left(arr):
    ...:     carry = (arr[0] & 0x80) >> 7
    ...:     for i in reversed(range(len(arr))):
    ...:         new_carry = (arr[i] & 0x80) >> 7
    ...:         arr[i] = ((arr[i] & 0x7F) << 1) | carry
    ...:         carry = new_carry
    ...:     return arr
    ...:

In [38]: def encrypt_byte(data_byte, key_byte):
    ...:     key_byte = ((key_byte >> 4) | (key_byte << 4)) & 0xFF
    ...:     return ((key_byte + data_byte) & 0xFF)
    ...:

In [39]: def encrypt_packet(packet):
    ...:     arr = rotate_array_left(rotate_array_left(bytearray(packet)))
    ...:     arr = swap_indices(arr)
    ...:     arr = apply_xor_key(arr)
    ...:     arr = rotate_array_left(rotate_array_left(arr))
    ...:     return bytearray([encrypt_byte(d, k) for (d, k) in zip(arr, b'E-SigNal')])
    ...:

In [40]: encrypt_packet(decrypt_packet(b'testtest'))
Out[40]: bytearray(b'testtest')

Survives a round-trip, so this will hopefully work.

Challenge 1: Can we send a simple command?

Let's try a little experiment. When I was poking about with it before I bricked it, I found out that command 5 would either turn on or off the LED on the side buttons, depending on what parameter I passed. Can I encrypt that command and get it to execute?

In [41]: cmd5_0 = encrypt_packet(bytearray([5, 0, 0, 0, 0, 0, 0, 0xFF-5]))

In [42]: cmd5_1 = encrypt_packet(bytearray([5, 1, 0, 0, 0, 0, 0, 0xFF-5-1]))

In [43]: cmd5_0
Out[43]: bytearray(b'\\\xd2\x9d\x96\xa1\xa4>\xce')

In [44]: cmd5_1
Out[44]: bytearray(b'\\\xd2\x8d\x96\xb1\xa4>\xce')

In [45]: dev.ctrl_transfer(0x21, 9, 0x300, 2, cmd5_0)
Out[45]: 8

In [46]: dev.ctrl_transfer(0x21, 9, 0x300, 2, cmd5_1)
Out[46]: 8

The answer is yes. It happily accepts the command. Sending cmd5_0 turns off the LED, and sending cmd5_1 turns it back on.

Challenge 2: Can we do a bulk read transfer?

I'm going to switch back to python-hidapi, because it's easier for this. I first need to tell pyusb to release its hold on the device, then I can poke at it with hidapi all I want.

I'd like to try and dump block C. To do this, I need to send a command 0x8C with no parameters except for the obligatory checksum byte using send_feature_report. Then I need to receive a reply via get_feature_report, and last but not least I need to read some data.

In [55]: usb.util.dispose_resources(dev)

In [56]: import hid

In [57]: d = hid.device()

In [58]: d.open_path(b'0001:0007:02')

In [59]: cmd8C = encrypt_packet(bytearray([0x8C,0,0,0,0,0,0,0xFF-0x8C]))

In [60]: d.send_feature_report(cmd8C)
Out[60]: 8

In [61]: d.get_feature_report(0, 9)
Out[61]: [0, 88, 213, 245, 150, 153, 164, 206, 206]

In [62]: binascii.hexlify(decrypt_packet(_61[1:]))
Out[62]: b'8c00800000000000'

Hey, guess what? That's a perfectly valid reply, telling us that we're receiving 0x80 bytes worth of result data.

In [63]: result = d.read(0x40)

In [64]: result += d.read(0x40)

In [65]: binascii.hexlify(bytearray(result))
Out[65]: b'0100f0000100f1000100f2000af01e02070001000100f4000100f300000000000000000000000000000000000000000000000000000000000100f7000100f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'

And there we have it: our device's dump of block C. As expected, it's a copy of the button mapping block. All this hassle just because timings got messed up and the tool wrote block D over block C.

Challenge 3: Can we restore a valid configuration?

Let's try this. I wrote my known-good configuration into a file called knowngood.bin. I'll need to send a command 0xC to tell the mouse that I want to flash 0x80 bytes worth of data, and then send the configuration in two blocks of 0x40 bytes each.

In [81]: good_config = open('knowngood.bin', 'rb').read()

In [82]: part1 = good_config[:0x40]

In [83]: part2 = good_config[0x40:]

In [84]: cmdC = encrypt_packet(bytearray([0xC,0,0x80,0,0,0,0,0xFF-0xC-0x80]))

In [85]: cmdC
Out[85]: bytearray(b'T\xd2-\x96\x99\xa4\xce\xce')

In [86]: d.send_feature_report(cmdC)
Out[86]: 8

In [87]: d.write(part1)
Out[87]: 64

In [88]: d.write(part2)
Out[88]: 64

As soon as I send the final part of the configuration, the LEDs on the mouse turn back on. Normal operation is restored. EVERYTHING IS FINE.

Conclusion

Hardware is difficult. You can also do ridiculous things if you set your mind to it... like accidentally bricking your mouse and then shaving several levels of yaks just so you can figure out how to unbrick it through the medium of a Python REPL.

What next? I haven't actually decided; finishing this post brings me up-to-date with everything I've discovered so far. (I actually semi-bricked the mouse again, on purpose, just for this post. Thankfully my confidence in being able to get it working again was fully founded).

I can keep on reversing the firmware to try and find out what the other mystery commands do, and write a nice little tool for it. I could try and reverse the bootloader - I suspect it's stored in a region of the MTP file that my extractor doesn't give me because it relies on Holtek's library which hides it from prying eyes. I could try to write some custom firmware for it.

Or I could move on entirely - I've got a DREVO Tyrfing V2 mechanical keyboard with fully-customisable RGB lighting, macros and key assignments. The config tool for it is actually written in Python with PyQt5, and seems to be of substantially higher code quality. That could make for another fun reversing project 🤔

Congratulations on making it this far, and thanks for being interested enough in my mouse's story to finish it :p

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