LEdoian's Blog

Ducky keyboard NKRO in Linux

Reading:

  • https://www.usb.org/hid
    • HID class definition (1.11)
    • Usage tables (1.4) (they version wrong, there is v. 1.22, which is older…)
  • Kbd manual

Step 1: get to know what is happening:

https://wiki.wireshark.org/CaptureSetup/USB

usbmon, lsusb, get a dump of the traffic.

$ lsusb ... Bus 002 Device 005: ID 04d9:0356 Holtek Semiconductor, Inc. Ducky Keyboard ...

sudo modprobe usbmon sudo setfacl -m u:ledoian:r /dev/usbmon2

wireshark

locate a packet, in details device → apply as filter: selected => "usb.device_address == 5"

new capture, press the keys, see the pkts.

=> ~/Hardware/Ducky_KBD/nkro.pcapng (pressed asdf,hjkl, dropped)

There is another endpoint being used :-/ Wireshark tells us that the endpoints are ep81 and ep82

$ sudo lsusb -s 2:5 -v

(sudo adds two more lines, IDK which…)

examine the endpoints… - The ep81 is the boot-device-compatible one, with only 6kro (max. 8B packets) - The ep82 contains the rest of the pressed buttons, but in a very different format :-/ (max. 64B packets) Unf, we do not get to read the Report descriptors. IDK why :-/

/sys/devices/pci0000:00/0000:00:1a.0/usb2/2-1/2-1.6/2-1.6:1.2/0003:04D9:0356.0003/report_descriptor Yes!, apart from the fact we cannot make sense of it. (also: usbhid-dump. The same binary data :-/)

sudo usbhid-dump -m 04d9:0356

002:005:002:DESCRIPTOR 1693448552.883746
05 01 09 80 A1 01 85 01 19 81 29 83 15 00 25 01 95 03 75 01 81 02 95 01 75 05 81 01 C0 05 0C 09 01 A1 01 85 02 15 00 25 01 95 12 75 01 0A 83 01 0A 8A 01 0A 92 01 0A 94 01 09 CD 09 B7 09 B6 09 B5 09 E2 09 EA 09 E9 0A 21 02 0A 23 02 0A 24 02 0A 25 02 0A 26 02 0A 27 02 0A 2A 02 81 02 95 01 75 0E 81 01 C0 05 01 09 02 A1 01 09 01 A1 00 85 03 05 09 19 01 29 08 15 00 25 01 75 01 95 08 81 02 05 01 09 30 09 31 16 01 80 26 FF 7F 75 10 95 02 81 06 09 38 15 81 25 7F 75 08 95 01 81 06 05 0C 0A 38 02 95 01 81 06 C0 C0 05 01 09 06 A1 01 85 04 05 07 95 01 75 08 81 03 95 E8 75 01 15 00 25 01 05 07 19 00 29 E7 81 00 C0
002:005:001:DESCRIPTOR 1693448552.884253
06 00 FF 09 01 A1 01 09 02 15 00 26 FF 00 75 08 95 40 81 02 09 03 15 00 26 FF 00 75 08 95 40 91 02 C0
002:005:000:DESCRIPTOR 1693448552.884753
05 01 09 06 A1 01 05 07 19 E0 29 E7 15 00 25 01 75 01 95 08 81 02 95 01 75 08 81 03 95 03 75 01 05 08 19 01 29 03 91 02 95 01 75 05 91 03 95 06 75 08 15 00 26 A4 00 05 07 19 00 29 A4 81 00 C0

DDG: usb hid report descriptor parse

0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x80, // Usage (Sys Control) 0xA1, 0x01, // Collection (Application) 0x85, 0x01, // Report ID (1) 0x19, 0x81, // Usage Minimum (Sys Power Down) 0x29, 0x83, // Usage Maximum (Sys Wake Up) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x03, // Report Count (3) 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, 0x05, // Report Size (5) 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x85, 0x02, // Report ID (2) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x12, // Report Count (18) 0x75, 0x01, // Report Size (1) 0x0A, 0x83, 0x01, // Usage (AL Consumer Control Configuration) 0x0A, 0x8A, 0x01, // Usage (AL Email Reader) 0x0A, 0x92, 0x01, // Usage (AL Calculator) 0x0A, 0x94, 0x01, // Usage (AL Local Machine Browser) 0x09, 0xCD, // Usage (Play/Pause) 0x09, 0xB7, // Usage (Stop) 0x09, 0xB6, // Usage (Scan Previous Track) 0x09, 0xB5, // Usage (Scan Next Track) 0x09, 0xE2, // Usage (Mute) 0x09, 0xEA, // Usage (Volume Decrement) 0x09, 0xE9, // Usage (Volume Increment) 0x0A, 0x21, 0x02, // Usage (AC Search) 0x0A, 0x23, 0x02, // Usage (AC Home) 0x0A, 0x24, 0x02, // Usage (AC Back) 0x0A, 0x25, 0x02, // Usage (AC Forward) 0x0A, 0x26, 0x02, // Usage (AC Stop) 0x0A, 0x27, 0x02, // Usage (AC Refresh) 0x0A, 0x2A, 0x02, // Usage (AC Bookmarks) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x01, // Report Count (1) 0x75, 0x0E, // Report Size (14) 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x85, 0x03, // Report ID (3) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x08, // Usage Maximum (0x08) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data,Var,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) 0x05, 0x0C, // Usage Page (Consumer) 0x0A, 0x38, 0x02, // Usage (AC Pan) 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, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x85, 0x04, // Report ID (4) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0xE8, // Report Count (-24) 0x75, 0x01, // Report Size (1) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0xE7, // Usage Maximum (0xE7) 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection

// 203 bytes

We see several ReportIDs:

  • 1: Maybe ignore the first application?
    • 3x1 bit of whatever, 1x5 bits ARRAY of sth else. => 8 bits = 1 byte = 2 nibbles
  • 2: consumer control are similar to the hotkeys described on p.43 of the kbd manual
    • 18x1 bit of the controls? (manual says about 15 keys + three mouse clicks)
    • and 1x14 bits ARRAY (mouse movements?) => 32 bits = 4 bytes = 8 nibbles
  • 3: mouse is mouse-by-keys (p.39)
    • 8x1b buttons, 2x16b pos, 1x8b wheel, 1x8?b pan wtf => 7 bytes = 14 nibbles
  • 4: Application: keyboard
    • 1x8 bits whatever, 232?x1bit keys ARRAY (interpret 0xe8 as unsigned, seems like too many.) => 240b = 30B = 60n

We should now be able to understand the reply: 04 00 00 C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 The reply started with prefix 0x04, so it is the Keyboard report. (HID s.8) Then, there is a byte of zeroes (probably) and then an array of bits. By the encoding, the first bit is the LSb of the first byte. Therefore, we see that the bits 14 and 15 are set, representing scancodes for K and L, as expected.

Let's look at the simple endpoint:

0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0xE0, // Usage Minimum (0xE0) 0x29, 0xE7, // Usage Maximum (0xE7) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x03, // Report Count (3) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (Num Lock) 0x29, 0x03, // Usage Maximum (Scroll Lock) 0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 0x95, 0x01, // Report Count (1) 0x75, 0x05, // Report Size (5) 0x91, 0x03, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xA4, 0x00, // Logical Maximum (164) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0xA4, // Usage Maximum (0xA4) 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection

// 64 bytes

This should behave normally. The response should contain: - 8x1b of some data (modifiers, scan codes E0…E7) - 1x8b of const data (in my experience, just zeroes. IDK whether this can change.) Note: now follow OUTPUT fields for LEDs: - 3x1b for LEDs (num lock, caps lock, scroll lock, as per HUT) - 1x5b for const data (maybe zeroes?) - 6x8b for the keys (b/w 0 and 164), encoded as array.

Let's look at the response: 00 00 04 16 07 09 0b 00

Only one report type, so no Report IDs.

This represents pressing 'asdfhj' - acc to Usage Tables for kbd/keypad, this corresponds to scan codes (hex) 04, 16, 07, 09, 0b, 0d

Keep in mind this is bitstream encoded little-endian, read LSb to MSb. Therefore, we read bytes left to right, but bits right to left

  • no modifiers (tested in separate capture)
  • 0x00 (zeroes/reserved.)
  • 6 items in an array: 0x4, 0x16, 0x7, 0x9, 0xb, 0xd, as expected. (Zero field means not occupied, these may be at any place.)

OK, so where is the catch? - The Report Count might be too high (even though HID s.6.2.2.7 explicitly expects this to be an unsigned number) - The data is declared as Array, not as bit-field!! Compare with the modifiers (or s.6.2.2.5 of HID)…

Therefore: the solution might be to just fix the report descriptor.


Tackling the descriptors:

  • Editing it in /sys/bus/usb/devices/…/report_descriptor does not look like an option (444 perms, -EPERM)
  • Kernel quirk it is!

Part 2: kernel driver

By the power of guessing, the correct part of kernel is drivers/hid :-) Let's find some quirk to inspire us (thanks ranger!) - hid-cherry.c is deals with fixing report descriptors - hid-redragon, -semitek, -topre

I went with redragon. Inspired by commit 85455dd906d568d5a42d9365938fbf82e932e2b8, I also add Kconfig stuff.

-> https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html -> https://www.kernel.org/doc/html/latest/kbuild/makefiles.html (skim only)

hid-quirks.c?

Building the module: Screw it. Just build the whole kernel (it did not work for me easily :-/) -> https://wiki.archlinux.org/title/Compile_kernel_module

Also <https://www.cyberciti.biz/tips/compiling-linux-kernel-module.html> and <https://www.cyberciti.biz/tips/build-linux-kernel-module-against-installed-kernel-source-tree.html>??

I tried not rebooting, but only insmoding the new module:

  1. build all
  2. make INSTALL_MOD_PATH=/tmp/modules-extra INSTALL_MOD_DIR=extra M=drivers/hid modules_install
    • ignore warnings maybe it will work
  3. copy the extra directory to /lib/modules/KERNELVERSION/extra (and maybe chown to root:root)
  4. sudo depmod (-vA) in the KERNELVERSION directory
  5. sudo modprobe --force-vermagic hid-ducky (I changed the build number in the process :-/)
  6. It seems to work.

Part 3: sending the patch

  1. create a reasonable commit message.

During development, I only went with ad hoc messages without much explanations. git rebase -i or the correct combination of git reset and git add commands will help here.

  1. rebase onto Linus's tree (or maybe HID-team's tree)