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.
It uses the CU0019 dongle (aka PID C542), here are some internal photos thanks to the FCC.
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
- Plug in
- Start solaar
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.
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):
- Import the binary, do not Auto-Analyse
- Run the fix_funcnames.py plugin
- Run the disas_symbols.py plugin
- Run the Auto-Analysis, without call convention identification
- 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)
- 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Top comments (0)