DEV Community

Cover image for USB HID Down the rabbit hole: Reverse engineering the Logitech CU0019 USB receiver
endes0
endes0

Posted on

USB HID Down the rabbit hole: Reverse engineering the Logitech CU0019 USB receiver

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

It uses the CU0019 dongle (aka PID C542), here are some internal photos thanks to the FCC.

Image description

Nano Receiver with USB ID C542 does not use HID++ #1835

Dmesg log

[21837.663373] usb 2-2: new full-speed USB device number 9 using xhci_hcd
[21837.815411] usb 2-2: New USB device found, idVendor=046d, idProduct=c542, bcdDevice= 3.02
[21837.815419] usb 2-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[21837.815422] usb 2-2: Product: Wireless Receiver
[21837.815424] usb 2-2: Manufacturer: Logitech
[21837.819645] input: Logitech Wireless Receiver Mouse as /devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/0003:046D:C542.000D/input/input38
[21837.819801] hid-generic 0003:046D:C542.000D: input,hidraw0: USB HID v1.11 Mouse [Logitech Wireless Receiver] on usb-0000:00:14.0-2/input0

Describe the bug Adapter CU0019 not recognised (same on windows Logitech utilities). Tried multiple of these adapters.

To Reproduce

  1. Plug in
  2. Start solaar

Screenshots Screenshot_2022-11-10_13-49-26

It uses the Telink TLSR8366. 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.

GitHub logo trust1995 / Ghidra_TELink_TC32

Ghidra processor specification for the Telink TC32

Telink TC32 Processor Specification for Ghidra

This repository contains a fairly complete processor specification for the Telink TC32 architecture, used by all of Telink's System-On-Chips. The work herein is based on Ryan Govostes' work and extended with various fix-ups and actual P-code implementation.

Right now decompilation is working well with several tested TC32 ELFs.

Usage

Copy the Telink_TC32 repository to Ghidra/Processors. Restart Ghidra Afterwards, when importing a TC32 binary, when prompted for the binary's "Language", select the "Telink_TC32" processor.

For analysing Telink ELFs, I use the following process (using some plugins from my GhidraPlugins repo):

  1. Import the binary, do not Auto-Analyse
  2. Run the fix_funcnames.py plugin
  3. Run the disas_symbols.py plugin
  4. Run the Auto-Analysis, without call convention identification
  5. Parse the register header file into the Data Type Manager (Grab the Telink SDK, redefine the REG_ADDR%X macros in register_82XX.h as integers instead of as pointers)
  6. Export the register/defines values just imported from…

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
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo pvvx / TlsrComSwireWriter

TLSR826x/825x COM port Swire Writer

TlsrComSwireWriter

TLSR826x/825x COM port Swire Writer Utility

Telink SWIRE simulation on a COM port.

Using only the COM port, downloads and runs the program in SRAM for TLSR826x or TLSR825x chips.

SCH

COM-RTS connect to Chip RST or Vcc.

usage: ComSwireWriter [-h] [--port PORT] [--tact TACT] [--file FILE] [--baud BAUD]

TLSR826x ComSwireWriter Utility version 21.02.20

optional arguments:
    -h, --help            show this help message and exit
    --port PORT, -p PORT  Serial port device (default: COM1)
    --tact TACT, -t TACT  Time Activation ms (0-off, default: 600 ms)
    --file FILE, -f FILE  Filename to load (default: floader.bin)
    --baud BAUD, -b BAUD  UART Baud Rate (default: 230400)

Added TLSR825xComFlasher:

usage: TLSR825xComFlasher.py [-h] [-p PORT] [-t TACT] [-c CLK] [-b BAUD] [-r]
                             [-d]
                             {rf,wf,es,ea}
TLSR825x Flasher version 00.00.02

positional arguments:
  {rf,wf,es,ea}         TLSR825xComFlasher {command} -h for additional help
    rf                  Read Flash to binary file
    wf                  Write file to Flash with sectors erases
    es                  Erase Region (sectors)

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.

Image description

Image description

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] 
Enter fullscreen mode Exit fullscreen mode

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

Beyond are the scripts I made for dumping, they are crappy and require the hid-tools package.

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

Image description

Image description

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.

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.

        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;
Enter fullscreen mode Exit fullscreen mode

/proj/drivers/usb.c

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

/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

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.

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&0xff (addr>>8)&0xff - - - - Memory: read 32 bits from addr + 0x800000
0xC1 addr&0xff (addr>>8)&0xff dat - - - Memory: write 8 bits dat to addr + 0x800000
0xC2 addr&0xff (addr>>8)&0xff dat&0xff (dat>>8)&0xff - - Memory: write 16 bits dat to addr + 0x800000
0xC3 addr&0xff (addr>>8)&0xff dat&0xff (dat>>8)&0xff (dat>>16)&0xff (dat>>24)&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? Taking a look at the sources of web BDT tool, the PID doesn't seem to match. So I thought it wasn't.

Image description

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

The analog read I think it reads the "3.3V analog registers" referenced in the datasheet.

Image description

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

Image description

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

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

Top comments (0)