USB HID Down the rabbit hole: Reverse engineering the Logitech CU0019 USB receiver
Tags: usb, reverseengineering, hardware, reversing
Published: 2024-05-30 09:04:12

Recently, I have an obsession with USB HID devices. So let's go through this rabbit hole. These devices send a descriptor which specifies the format of the "packets" they send or receive. Most fields are defined in the standard, so your hid driver can interact with the device out of the box. Some devices implement curious fields, like my headphones can send phone "events" like calls.
Most of them implement custom vendor specific fields. This is where it gets interesting.
 
Let's introduce my laptop mouse (below is Jaime playing with it), it's a Logitech M185. Almost all the Logitech hid devices implement their proprietary standard hid++, which is well-known. Mine is the exception.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mitc2mfw3u1noax1vsiq.png)



It uses the [CU0019 dongle](https://support.logi.com/hc/en-us/articles/7565042135959-USB-Wireless-Receiver-Pairing-and-Troubleshooting#h_01G8P2H7084PDDFCXE3QN9GM15) (aka PID C542), [here are some internal photos thanks to the FCC](https://fccid.io/JNZCU0019/Internal-Photos/Internal-photos-4169246). 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1v2zei33ow6vf5t9vqgl.png)

{% embed https://github.com/pwr-Solaar/Solaar/issues/1835 %}

It uses the [Telink TLSR8366](https://wiki.telink-semi.cn/wiki/chip-series/TLSR836x-Series/). While Telink openly publishes a lot of tools, documents and code on their website, it is an amalgam of things which don't do a good job explaining themselves, so most of the time I was like "huh?". Oh, they also implement their own ISA called TC32, which is undocumented. Fortunately, GitHub users _trust1995_ and _rgov_ have done a fairly decent job of implementing it on Ghidra.

{% embed https://github.com/trust1995/Ghidra_TELink_TC32/ %}

The HID descriptor, apart from the standard mouse report, exposes a vendor report with ID 5. This report has a feature (aka input and output interface) of 7 bytes. Reading from it returns nothing.

```
0x90,              // Output
0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,        // Usage (Mouse)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x09, 0x01,        //   Usage (Pointer)
0xA1, 0x00,        //   Collection (Physical)
0x05, 0x09,        //     Usage Page (Button)
0x19, 0x01,        //     Usage Minimum (0x01)
0x29, 0x05,        //     Usage Maximum (0x05)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x95, 0x05,        //     Report Count (5)
0x75, 0x01,        //     Report Size (1)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01,        //     Report Count (1)
0x75, 0x03,        //     Report Size (3)
0x81, 0x01,        //     Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x16, 0x01, 0x80,  //     Logical Minimum (-32767)
0x26, 0xFF, 0x7F,  //     Logical Maximum (32767)
0x75, 0x10,        //     Report Size (16)
0x95, 0x02,        //     Report Count (2)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x38,        //     Usage (Wheel)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection
0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x00,        // Usage (Undefined)
0xA1, 0x01,        // Collection (Application)
0x85, 0x05,        //   Report ID (5)
0x06, 0x00, 0xFF,  //   Usage Page (Vendor Defined 0xFF00)
0x09, 0x01,        //   Usage (0x01)
0x15, 0x81,        //   Logical Minimum (-127)
0x25, 0x7F,        //   Logical Maximum (127)
0x75, 0x08,        //   Report Size (8)
0x95, 0x07,        //   Report Count (7)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection

// 91 bytes
```


# Getting a firmware dump

I started the house by the roof, instead of trying to fuzz the vendor report, I tried to dump the firmware "the hardware way". These chips have a SWIRE pin, which is a proprietary protocol for debugging and accessing the memory. This interface requires a Telink EVK tool, which isn't cheap, but GitHub user _pvvx_ has written some tools for reading and writing the memory. The 826X tools are compatible with the 836x series.

{% embed https://github.com/pvvx/TlsrComSwireWriter %}

After adapting the tools to use my FTDI ft2332HL board and dumps the memory and soldering cables to the reset and Swire pin (fortunately these pins aren't connected to anything), the dumper kinda worked. It was very unstable, but the data seemed good (it wasn't). Seeing the `KNLT`, `Logitech`, `Wirless Receiver` and `TLSR8366` strings was a good confirmation that it wasn't just garbage.

{% embed https://twitter.com/fw_r3_t/status/1793635090746257531 %}

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qpdbsmtgm8ortfk2rex3.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5s6clzne9076ljqsd59m.png)

I could have wasted countless days trying to work with this dump, hopefully, I decided to do things right and started fuzzing the HID vendor report using a fastly written Python script. The results were, let's say, very verbose. A lot of send commands responded with data. Also, I thought I bricked the device as the mouse stopped working, luckily a reset fixed it. 

On the results instantaneously something catches my attention. When the fuzzer was changing the data of the second byte, the received data was like sliding.

```
i: -064 j: -082 send dat: [5, 192, 174, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 0, 128, 3, 8, 33, 132]
i: -064 j: -081 send dat: [5, 192, 175, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 128, 5, 3, 8, 33, 132]
i: -064 j: -080 send dat: [5, 192, 176, 0, 0, 0, 0, 0]	 recv dat: [0, 128, 5, 0, 3, 8, 33, 132]
i: -064 j: -079 send dat: [5, 192, 177, 0, 0, 0, 0, 0]	 recv dat: [128, 5, 0, 0, 3, 8, 33, 132] 
i: -064 j: -078 send dat: [5, 192, 178, 0, 0, 0, 0, 0]	 recv dat: [5, 0, 0, 0, 3, 8, 33, 132] 
i: -064 j: -077 send dat: [5, 192, 179, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 0, 0, 3, 8, 33, 132] 
i: -064 j: -076 send dat: [5, 192, 180, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 0, 0, 3, 8, 33, 132] 	
i: -064 j: -075 send dat: [5, 192, 181, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 0, 147, 3, 8, 33, 132] 
i: -064 j: -074 send dat: [5, 192, 182, 0, 0, 0, 0, 0]	 recv dat: [0, 0, 147, 0, 3, 8, 33, 132] 	
i: -064 j: -073 send dat: [5, 192, 183, 0, 0, 0, 0, 0]	 recv dat: [0, 147, 0, 0, 3, 8, 33, 132]
i: -064 j: -072 send dat: [5, 192, 184, 0, 0, 0, 0, 0]	 recv dat: [147, 0, 0, 0, 3, 8, 33, 132] 
```

My intuition was that we were reading memory!. I quickly write a dumper and Tachan! We got a more stable, easy and correct dump.

{% embed https://twitter.com/fw_r3_t/status/1794070181775327239 %}

Beyond are the scripts I made for dumping, they are crappy and require the [hid-tools package](https://gitlab.freedesktop.org/libevdev/hid-tools). 

{% embed https://gist.github.com/endes0/31e527c3b4d07fb1a187fdef3e7ccff8 %}

# Analyzing the firmware dump
After renaming the symbols of the startup code according to the names on the boot c startup assembly code of the SDKs, I found that a constant was different from those. After a quick GitHub code search, VOILA here are the possible SDK they used.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/exu4rgzk4khcs1ksxi2l.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u3h64ao0aegtrdm3g8u5.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55y1nq7m6cr99were35m.png)




I'm lazy, I wanted to generate a Fidb for Ghidra to automatically recognize functions from the SDK, especially the ones related to USB, but I didn't want to setup and compile the SDK. Fortunately, [there was a precompiled bin with its symbols on one of the repos](https://github.com/stephanosio/js_km_proj/blob/4d9960bace457d7514554185ee73924662cfabcd/tlsdk/8366_dongle/8366_dongle.lst).

## A great find
After generating a Fidb and doing an analysis, for me, it was clear to me that the firmware was a slightly modified version of the _8366_dongle_ project on that repo(I already had my suspicions at the moment I opened the disassembly of the bin). 

After a little bit of digging, tachan! This seems to be the code that handles the vendor HID Report.


``` Cpp
		case HID_REPORT_CUSTOM:
#if (USB_CUSTOM_HID_REPORT)
		{	//Paring, EMI-TX, EMI-RX
			if (data_request) {
				int i=0;
				usbhw_reset_ctrl_ep_ptr (); //address
				for(i=0;i<8;i++) {
					host_cmd[i] = usbhw_read_ctrl_ep_data();
				}
#if (USB_CUSTOM_HID_REPORT_REG_ACCESS)
				custom_reg_cmd = (host_cmd[1] & 0xf0) == 0xc0;
				if (custom_reg_cmd) {
					host_cmd[0] = 0;
					int adr = *((u16 *)(host_cmd + 2));
					int len = host_cmd[1] & 3;
					if (host_cmd[1] == 0xcc && adr == 0x5af0) { //re-enumerate device
						usb_dp_pullup_en (0);			//disable device
						sleep_us (300000);
						reg_ctrl_ep_irq_mode = 0xff;	//hardware mode
						usb_dp_pullup_en (1);			//enable device
					}
					else {
						adr += 0x800000;
					}

					if ((host_cmd[1] & 0x0c)==0) {	//write core register
						if (len == 0) {
							for (int k=0; k<4; k++) {
								custom_read_dat = (custom_read_dat >> 8) | (read_reg8 (adr++) << 24);
							}
						}
						else if (len == 1) {
							write_reg8 (adr, host_cmd[4]);
						}
						else if (len == 2) {
							write_reg16 (adr, *((u16 *)(host_cmd + 4)));
						}
						else {
							write_reg32 (adr, *((u32 *)(host_cmd + 4)));
						}
					}
					else {	//read core register
						if (len == 0) {
							custom_read_dat = analog_read (host_cmd[2]);
						}
						else {
							analog_write (host_cmd[2], host_cmd[4]);
						}
					}
				}

...

		case HID_REQ_GetReport:
#if(USB_SOMATIC_ENABLE)
			if(usbsomatic_hid_report_type((control_request.wValue & 0xff))){
			}
			else
#elif (USB_CUSTOM_HID_REPORT)
			if( control_request.wValue==0x0305 ) {
				if (USB_CUSTOM_HID_REPORT_REG_ACCESS && custom_reg_cmd) {
					usbhw_write_ctrl_ep_data (custom_read_dat);
					usbhw_write_ctrl_ep_data (custom_read_dat>>8);
					usbhw_write_ctrl_ep_data (custom_read_dat>>16);
					usbhw_write_ctrl_ep_data (custom_read_dat>>24);
					usbhw_write_ctrl_ep_data (0x10);
					usbhw_write_ctrl_ep_data (0x20);
					usbhw_write_ctrl_ep_data (0x40);
					usbhw_write_ctrl_ep_data (0x80);
				}
				else {
					usbhw_write_ctrl_ep_data (0x04);
					usbhw_write_ctrl_ep_data (0x58);
					usbhw_write_ctrl_ep_data (0x00);
					usbhw_write_ctrl_ep_data (host_cmd_paring_ok ? 0xa1 : 0x00);  //For binding OK
					usbhw_write_ctrl_ep_data (0x00);
					usbhw_write_ctrl_ep_data (0x00);
					usbhw_write_ctrl_ep_data (0x08);
					usbhw_write_ctrl_ep_data (0x00);
				}
			}
			else
#endif			
			{	//  donot know what is this
	//			usbhw_write_ctrl_ep_data(0x81);
	//			usbhw_write_ctrl_ep_data(0x02);
	//			usbhw_write_ctrl_ep_data(0x55);
	//			usbhw_write_ctrl_ep_data(0x55);
			}
			break;
```
_/proj/drivers/usb.c_

``` Cpp
void usb_host_cmd_proc(u8 *pkt)
{
	extern u8		host_cmd[8];
	extern u8		host_cmd_paring_ok;

	u8   chn_idx;
	u8   test_mode_sel;
	u8 	 cmd = 0;
	static emi_flg;


	if((host_cmd[0]==0x5) && (host_cmd[2]==0x3) )
	{
		host_cmd[0] = 0;
		dongle_host_cmd1 = host_cmd[1];

		if (dongle_host_cmd1 > 12 && dongle_host_cmd1 < 16){  //soft paring
			host_cmd_paring_ok = 0;
			rf_paring_tick = clock_time();  //update paring time

			if(dongle_host_cmd1 == 13){     //kb and mouse tolgether
				mouse_paring_enable = 1;
				keyboard_paring_enable = 1;
			}
			else if(dongle_host_cmd1 == 14){ //mouse only
				mouse_paring_enable = 1;
			}
			else if(dongle_host_cmd1 == 15){  //keyboard only
				keyboard_paring_enable = 1;
			}
		}
		else if(dongle_host_cmd1 > 0 && dongle_host_cmd1 < 13)  //1-12:����EMI
		{
			emi_flg = 1;
			cmd = 1;

			irq_disable();
			reg_tmr_ctrl &= ~FLD_TMR1_EN;
			//rf_stop_trx ();

			chn_idx = (dongle_host_cmd1-1)/4;
			test_mode_sel = (dongle_host_cmd1-1)%4;
		}
	}

	if(emi_flg){
		emi_process(cmd, chn_idx,test_mode_sel, pkt, dongle_cust_tx_power_emi);
	}
}
```
_/vendor/dongle/dongle_emi.c_


## Mouse Device ID 
I think I also found the memory address where the current paired Mouse is stored(`custom_binding[0]`): `0x809160`. I can't confirm it as I don't have another mouse and the value is a little bit off for me.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nvfbxvm5iuctuqoydix.png)

Potentially, this can be used to send a USB HID read memory and obtain the current mouse ID.

## Ghidra symbols
Here are all the symbols I found, it can be imported with the `ImportSymbolsScript.py`.

{% embed 
 https://gist.github.com/endes0/82fbbfa8f1c777fcca1e144d82103b87 %}

# USB HID custom commands
So, after analyzing the firmware, fuzzer output and doing some test, I found the following USB HID set feature commands.

|    1 |             2 |                        3 |            4 |                       5 |                        6 |                        7 | Description                                      |
|-----:|--------------:|-------------------------:|-------------:|------------------------:|-------------------------:|-------------------------:|--------------------------------------------------|
| 0xD  | 0x3           | -                        | -            | -                       | -                        | -                        | Software pairing: Mouse and keyboard              |
| 0xE  | 0x3           | -                        | -            | -                       | -                        | -                        | Software pairing: Mouse                           |
| 0xF  | 0x3           | -                        | -            | -                       | -                        | -                        | Software pairing: Keyboard                        |
| 0x1  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel low, mode carrier_                   |
| 0x2  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel low, mode cd_                        |
| 0x3  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel low, mode  rx_                       |
| 0x4  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel low, mode tx_                        |
| 0x5  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel medium, mode carrier_                |
| 0x6  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel medium, mode cd_                     |
| 0x7  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel medium, mode  rx_                    |
| 0x8  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel medium, mode tx_                     |
| 0x9  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel high, mode carrier_                  |
| 0xA  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel high, mode cd_                       |
| 0xB  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel high, mode  rx_                      |
| 0xC  | 0x3           | -                        | -            | -                       | -                        | -                        | _EMI: channel high, mode tx_                       |
| 0xC0 | addr&amp;0xff | (addr&gt;&gt;8)&amp;0xff | -            | -                       | -                        | -                        | Memory: read 32 bits from **addr** + 0x800000        |
| 0xC1 | **addr**&amp;0xff | (**addr**&gt;&gt;8)&amp;0xff | **dat**          | -                       | -                        | -                        | _Memory: write 8 bits **dat** to **addr** + 0x800000_      |
| 0xC2 | **addr**&amp;0xff | (**addr**&gt;&gt;8)&amp;0xff | **dat**&amp;0xff | (**dat**&gt;&gt;8)&amp;0xff | -                        | -                        | _Memory: write 16 bits **dat** to **addr** + 0x800000_     |
| 0xC3 | **addr**&amp;0xff | (**addr**&gt;&gt;8)&amp;0xff | **dat**&amp;0xff | (**dat**&gt;&gt;8)&amp;0xff | (**dat**&gt;&gt;16)&amp;0xff | (**dat**&gt;&gt;24)&amp;0xff | _Memory: write 32 bits **dat** to **addr** + 0x800000_     |
| 0xC4 | **addr**          | -                        | -            | -                       | -                        | -                        | Memory: read analog address **addr**                 |
| 0xC5 | **addr**          | -                        | **dat**          | -                       | -                        | -                        | _Memory:  write 8 bits **dat** at analog address **addr**_ |
| 0xCC | 0xF0          | 0x5A                     | -            | -                       | -                        | -                        | Misc: "renumerates USB devices"                  |

Notes: Take the _italics_ entries with a grain of salt, as I didn't test it. Byte 0 is always the report ID, in this case 5.

It seems that software pairing is broken, Keyboard and Mouse pairing command always return success while the other 2 never succeed. Also, all the pairing commands disconnect the mouse, and it won't work until restarting the dongle.



Issuing the "renumerate" command will connect the device as a USB printer "Telink Semiconductor USB DevSys" with VID 248A and PID 5320. Maybe this is the "USB programming mode" for interfacing with [Telink BDT tools](https://wiki.telink-semi.cn/wiki/IDE-and-Tools/Burning-and-Debugging-Tools-for-all-Series/)? Taking a look at the sources of [web BDT tool](https://wiki.telink-semi.cn/debug/readme/README_en.html#3), the PID doesn't seem to match. So I thought it wasn't.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfizqqgrh0p91484dasc.png)

``` Javascript
async function  usb_connect(){
	const myfilters = [
      { 'vendorId': 0x2341, 'productId': 0x8036 },
      { 'vendorId': 0x248A, 'productId': 0x826A }, ]; //'productId': 0x826A 
```

The analog read I think it reads the "3.3V analog registers" referenced in the [datasheet](https://wiki.telink-semi.cn/doc/ds/DS_TLSR8366-E_Datasheet%20for%20Telink%202.4GHz%20RF%20System-On-Chip%20Solution%20TLSR8366.pdf).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9mn1r76fqjb14ljh1u74.png)

### Another "great" dump
Before I said that when the device renumerates as "Telink Semiconductor USB DevSys" maybe it is for the BDT tools, well after launching the desktop tools on Windows, it connects, so it is.

This is great as we can have total access to all the memory spaces and also some debugging functions.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uztsoffw6uqh51ulpiig.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9eigcva32f5dmpod24j4.png)

Let's just say that the memory access tool, well, umh, it's not great. The CORE access it also seems to start at address 0x800000, but if we read 0x808000 it seems to contain errors, or maybe another program.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wnhdbkh4x9fw3f1rgdm6.png)

The analog read works like the USB HID analog read, the flash read always returns 0xFF (In theory this chip doesn't have flash at all) and unfortunately the OTP read doesn't work. This is bad news, as the OTP memory is likely to have something.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jea0qcsvlkzwort6hi2r.png)