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
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!
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
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).
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.
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.
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.
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!
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.
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.
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.
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)
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.
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.
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.
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.
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.
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.
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:
SET_REPORT
requestbSetupFlag
is set, it's parsed by the device as a USB request, which clears bSetupFlag
first thingSET_REPORT
, nCmdIndex
is set to 1, 2 or 3 depending on the kind of reportbSetupFlag
is clear and nCmdIndex
is non-zero, the device jumps to handleCmdIndexStuff
[EFh]
probably gets set in here)GET_REPORT
requestGET_REPORT
, a reply is produced, and the cycle is completedLet'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 logic here goes as follows:
sub_115D
[EFh]
(we were expecting that)jtbl_06AA_case_target_00
command_ID & 0xF
and go handle a command based off thatROM: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?
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.
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.
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.
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?
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.
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.
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.
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.
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
Previous Post: Mouse Adventures #7: Writing an IDA Processor Module
Next Post: Spoonalysis: Mapping UK Chain Pub Prices