Logitech Unifying for Linux: Reverse Engineering and unpairing tool

Published on by

Introduction

Logitech’s Unifying technology is awesome. Plug the USB receiver in the computer, power on your keyboard or mouse and it is ready for use! Well, the devices themselves do work, but software to attach new devices to a single receiver (or detach bound devices) is only available for Mac OS X and Windows. This article shows a way to pair and unpair devices in Linux and describe the tools that have been used to accomplish that goal. If you are not interested in the technical details, but just want to know how to use the pairing tool, see ltunify: Logitech Unifying tool for Linux.

For Linux, Logitech engineer Benjamin Tissoires has published an unofficial utility that is able to pair new devices to a receiver. It does so by writing a magic byte sequence to the USB receiver. While the tool seems to work, I did not like writing random values to a device without knowing what the side-effects are. Besides, I also wanted to be able to bind the wireless device back to the original receiver. For these reasons, I started exploring possibilities for reverse engineering the protocol used to (un)pair devices.

At first, I looked at the kernel source code, drivers/hid/hid-logitech-dj.c and its related header file drivers/hid/hid-logitech-dj.h. This gave me an idea of how the USB data packet had to be interpreted. While I was actively reverse engineering the protocol, I spotted the Logitech protocol specification by coincidence. These documents can be found in my files directory. Especially the Logitech HID++ 1.0 specification for Unifying Receivers document was very insightful.

My notes and the source code of the resulting pairing program and reverse engineer utilities can be found on https://git.lekensteyn.nl/ltunify/.

Prepare a QEMU VM for USB tapping

Inspired by Avery’s post about reverse engineering the Logitech Unifying USB protocol, I used a virtual machine running Windows XP on a Linux host. To inspect the USB traffic, the usbmon kernel module was used on Linux. Instead of VirtualBox, I chose for QEMU because it is easier to setup for running over a LAN network. The remote desktop PC had plenty of RAM to store the virtual machine disk image, copies of that disk image and any iso files that were needed for the installation of the Windows XP guest system and drivers. See Wikibooks QEMU/Windows XP article for instructions on creating and installing a Windows XP virtual machine with QEMU.

Since my VM was running on a remote LAN machine, I had to use VNC for viewing the virtual machine interface. A virtual network adapter is also added to the VM for downloading Logitech programs. Before you can use the virtual network adapter, you need to install Windows VirtIO Drivers (this is optional as networking is not really necessary for tapping USB).

After creating the virtual machine, unload some kernel modules on the host OS to avoid interference with the Windows guest. Then determine the USB device that you want to pass to the Windows VM and grant yourself read/write permissions to this USB device.

remote$ sudo modprobe -vr usbhid hid-logitech-dj
rmmod /lib/modules/3.5.0-25-generic/kernel/drivers/hid/hid-logitech-dj.ko
rmmod /lib/modules/3.5.0-25-generic/kernel/drivers/hid/usbhid/usbhid.ko
remote$ lsusb
...
Bus 005 Device 004: ID 046d:c52b Logitech, Inc. Unifying Receiver
...
remote$ sudo chgrp $USER /dev/bus/usb/005/004
remote$ qemu-system-i386 -enable-kvm -m 1G -vga std -monitor stdio -vnc :0 -S \
-device usb-host,hostbus=5,vendorid=0x046d,productid=0xc52b \
-netdev user,id=qnet0 -device virtio-net,netdev=qnet0 \
-hda winxp.img
QEMU 1.4.0 monitor - type 'help' for more information
(qemu) 

The qemu-system-i386 command starts a 32-bit VM. Since the -S option was passed, it will not immediately boot the guest OS. This allows you to attach a VNC client and watch the boot process. On your local machine, connect to the VNC server (192.168.4.2 in this example):

$ gvncviewer 192.168.4.2:0
Connected to server
Continue the virtual machine by issuing the c command in the remote qemu shell and your virtual machine should be booted up in a few seconds. You are now ready to monitor USB traffic.

Monitoring USB with usbmon

Linux makes it easy to monitor USB traffic with the usbmon kernel module. Avery showed how to utilise /sys/kernel/debug/usb/usbmon/5u to watch the USB communication. In order to aid me in analysis of its output, I wrote a small AWK script (usbmon.awk) that labels bytes and colors the messages based on their direction (received or sent). An example session is shown below where 5 is the Bus ID from lsusb.

$ sudo modprobe usbmon
$ sudo mount -t debugfs none /sys/kernel/debug
$ sudo cat /sys/kernel/debug/usb/usbmon/5u | usbmon.awk
(lots of long lines here with a lot of noise)

The above method provides me the needed information, but it is too verbose and outputs lots of useless messages. It turns out that the usbmon module also provides a more flexible device interface that makes it easier to process USB messages. Therefore, I wrote a second tool that uses this interface to produce more relevant messages, read-dev-usbmon.c. Another advantage of this tool is that root privileges become unnecessary after setting the appropriate file permissions.

$ sudo modprobe usbmon
$ sudo chmod g+r /dev/usbmon5
$ sudo chgrp $USER /dev/usbmon5
$ make read-dev-usbmon
$ ./read-dev-usbmon /dev/usbmon5

In order to understand the contents of USB messages, I will first provide an overview of the protocol before investigating how the Logitech Unifying software works.

Quick overview of protocol

The messages that are passed in the USB protocol are pretty simple. Some messages are so-called notifications. To enable these notifications, a special device register must be written. Other messages written to the USB receiver will yield a message that describe the result of the requested action. Basically there is a three bytes header followed by a payload that has to interpreted according to the message type:

Messages can be sent from software to the receiver, or received from the receiver to the software. The meaning of these messages do not have to be the same. The four most important message types are listed below. These messages can query or change a device register.

Sending a message to the receiver always generates another message. If the command failed (for example, if the message type or register was invalid), an error message will be delivered containing the message type, the register address and an error code followed by 0x00.

The exact parameters vary for device registers, refer to the registers.txt file in the git repo for an overview of these registers. As an example, consider the Enable HID++ Notifications register. Reading this short register will show all enabled notifications, writing this register will control which notifications will be received.

The short message, directed at the receiver should query a register with few parameters, namely the Enable HID++ Notifications register. No parameters are necessary (00 00 00) and the message looks like: 10 FF 81 00 00 00 00

After writing this message to the USB receiver, you will receive a short message from the USB receiver describing the response to the query for the Enable HID++ Notifications register. The message could look like: 10 FF 81 00 00 01 00 Note the 01 value above, it means that the software will receive notifications whenever a device arrives or disappears. (See Section 4.1 of the Logitech HID++ 1.0 specification (pdf) for other possible values.)

Another example, the magic sequence provided by Benjamin Tissoires's pairing program: 10 FF 80 B2 01 00 00 It is a short message that sets the value of the USB receiver register Device Connection and Disconnection (Pairing) to 01 00 00. These parameters open the receiver for new devices (01) for a default period of 30 seconds (the last 00). See Section 4.3 of the Logitech HID++ 1.0 specification for a detailed description of this message type.

What does Logitech’s Unifying software do?

Now that you know the basics of the Logitech HID++ protocol and with the Big Brother environment being ready, we can proceed with looking at the behavior of Logitech Unifying software in the virtual machine using the read-dev-usbmon tool that was mentioned in a previous section about usbmon.

When Logitech’s software is started, it immediately exchanges some messages with the receiver. These messages first ask for receiver details (serial, firmware versions, etc.) and then allow software to receive device notifications from the receiver. Next, similar details are retrieved for all paired devices. (See registers.txt for detailed steps and an annotated dump.)

Pairing a device is easy and has been explained before. How does Logitech do it? Let’s see what the read-dev-usbmon tool shows when no devices are paired and the Next button is clicked in the Logitech® Unifying Software:

Software asks receiver to pair a new device within a timeout of 0x3C (60) seconds.
Send     report_id=10 short device=FF RECV type=80 SET_REG
    reg=B2 DEVICE_PAIRING  params=01 53 3C
(notification that lock is now open:)
Recv     report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED
    params=01 00 00 00
(feedback that the register was succesfully set:)
Recv     report_id=10 short device=FF RECV type=80 SET_REG
    reg=B2 DEVICE_PAIRING  params=00 00 00

Keyboard is switched on, A Unifying keyboard device with product ID 2010 is deteced.
Recv     report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED
    params=04 61 10 20

Software requests name of device:
Send     report_id=10 short device=FF RECV type=83 GET_LONG_REG
    reg=B5 PAIRING_INFO  params=40 00 00 
(oh, receiver also wants to tell us that receiver is closed for new pairings:)
Recv     report_id=10 short device=FF RECV type=4A NOTIF_RECV_LOCK_CHANGED
    params=00 00 00 00 
Device responds with name of length 4, namely the UTF-8 string K800:
Recv     report_id=11 long  device=FF RECV type=83 GET_LONG_REG
    reg=B5 PAIRING_INFO  params=40 04 4B 38  30 30 00 00  00 00 00 00  00 00 00 00 

Software asks for “extended pairing info”:
Send     report_id=10 short device=FF RECV type=83 GET_LONG_REG
    reg=B5 PAIRING_INFO  params=30 00 00 
Device responds with serial number and the location of the power switch:
Recv     report_id=11 long  device=FF RECV type=83 GET_LONG_REG
    reg=B5 PAIRING_INFO  params=30 FB 84 1B  86 1A 40 00  00 07 00 00  00 00 00 00 
Receiver notifies that pairing has completed, a link had link has been established:
Recv     report_id=10 short device=01 DEV1 type=41 NOTIF_DEVICE_PAIRED
    params=04 A1 10 20 

(more messages that read device firmware/bootloader versions have been omitted)

The key action is making the receiver accept new pairings (just like Benjamins pairing tool). The other commands are used to provide feedback, whether a device was succesfully paired or not. This was not too difficult to guess. The next, more interesting action is unpairing a device. This can be done by pressing the Advanced button from the home screen. After pressing that button, it can be observed that the software polls repeatedly for status updates (that will be ignored in the following annotation). The below messages are communicated when pressing the Unpair button:

Disconnect device number 1 (the one in device=…):
    Send     report_id=10 short device=FF RECV type=80 SET_REG
    reg=B2 DEVICE_PAIRING  params=03 01 00 

Device 1 is now disconnected
Recv     report_id=10 short device=01 DEV1 type=40 NOTIF_DEVICE_UNPAIRED
    params=02 00 00 00 

(register successfully set:)
Recv     report_id=10 short device=FF RECV type=80 SET_REG
    reg=B2 DEVICE_PAIRING  params=00 00 00 

Benjamin was right that unpairing could not be discovered as easy as pairing, but nevertheless it is not impossible, especially if a specification is available.

Logitech has more software for keyboards and mice, namely SetPoint. By tapping the USB traffic, it is possible to discover additional functionalities. For example, flipping the functionality of the F button (like F9 / Play) generated just four messages which can easily be guessed:

(Check the “Swap F key functions” checkbox in SetPoint settings)
Software asks for current status of F key functions
Send      report_id=10 short device=01 DEV1 type=81 GET_REG
    reg=09 FN_KEY_SWAP?  params=00 00 00
(device responds: F key is not swapped)
Recv      report_id=10 short device=01 DEV1 type=81 GET_REG
    reg=09 FN_KEY_SWAP?  params=00 00 00 
Software asks to swap F key functions
Send      report_id=10 short device=01 DEV1 type=80 SET_REG
    reg=09 FN_KEY_SWAP?  params=00 01 00 
(device responds, register is successfully changed)
Recv      report_id=10 short device=01 DEV1 type=80 SET_REG
    reg=09 FN_KEY_SWAP?  params=00 00 00 

ltunify: Logitech Unifying tool for Linux

ltunify is a program resulting from the gathered knowledge on the Logitech HID++ protocol. It allows you to pair additional devices like keyboards and mice to your Unifying receiver, unpair existing devices and list information about connected devices. This section will show you how to install the ltunify program.

Besides a C compiler and a way to fetch sources (wget+tar or git), you will need Linux 3.2 or newer with the hid-logitech-dj module. On Debian and Ubuntu distributions, the required packages can be installed using:

sudo apt-get install git gcc
Fetch the sources and install ltunify to $HOME/bin/ using the next commands:
git clone https://git.lekensteyn.nl/ltunify.git
cd ltunify
make install-home

The following steps will assume that $HOME/bin is available in your path. If not, run:

export PATH="$HOME/bin:$PATH"
If everything went well, you should be able to run ltunify --help to show the available options:
Usage: ltunify [options] cmd [cmd options]
Logitech Unifying tool version dev
Copyright (C) 2013 Peter Wu <lekensteyn@gmail.com>

Generic options:
  -d, --device path Bypass detection, specify custom hidraw device.
  -D                Print debugging information
  -h, --help        Show this help message

Commands:
  list            - show all paired devices
  pair [timeout]  - Try to pair within "timeout" seconds (1 to 255,
                    default 0 which is an alias for 30s)
  unpair idx      - Unpair device
  info idx        - Show more detailed information for a device
  receiver-info   - Show information about the receiver
In the above lines, "idx" refers to the device number shown in the
 first column of the list command (between 1 and 6). Alternatively, you
 can use the following names (case-insensitive):
 Keyboard Mouse Numpad Presenter Trackball Touchpad
The below session shows you how to use a device index to unpair a mouse.
$ sudo ltunify list
Devices count: 1
Connected devices:
idx=1   Mouse   M525

$ sudo ltunify unpair 1
Device 0x01 Mouse successfully unpaired

$ sudo ltunify list
Devices count: 0
Connected devices:

$ sudo ltunify pair
Please turn your wireless device off and on to start pairing.
Found new device, id=0x01 Mouse

$ sudo ltunify list
Devices count: 1
Connected devices:
idx=1   Mouse   M525
It is also possible to select a device by device type (case-insensitive). When multiple devices of the same type are available, the first one will be selected.
$ sudo ltunify unpair mouse
Device 0x01 Mouse successfully unpaired

Advanced: do not run as root

ltunify does not have to run as root, but it is easier and faster to say “run this program as root” instead of “change permissions of this file and then run the program”. “this file” refers to some /dev/hidrawX where X is some number. An example where I allow myself to do anything with the receiver:

$ ltunify list
/dev/hidraw0: Permission denied
Logitech Unifying Receiver device is not accessible.
Try running this program as root or enable read/write permissions
for /dev/hidraw0
$ ls -l /dev/hidraw0
crw------- 1 root root  251, 0 Apr 24 11:14 /dev/hidraw0
$ sudo chmod g+rw /dev/hidraw0 && sudo chgrp $USER /dev/hidraw0
$ ls -l /dev/hidraw0
crw-rw---- 1 root peter 251, 0 Apr 24 11:14 /dev/hidraw0
$ ltunify list
Devices count: 1
Connected devices:
idx=1   Mouse   M525

To make this permanent, one could create a udev rule such as 42-logitech-unify-permissions.rules.

Conclusion

Logitech has created drivers that provides flawless Linux support for their products, but full pairing support was missing. Thanks to tools like usbmon and QEMU, I was able to analyse the protocol and relate it with the given HID++ 1.0 specification. That allowed me to create the ltunify program for Linux that provides similar functionality as the official Unifying Software.

Future work on this project could move functionality into a library, that could be used to create a graphical user interface for (un)pairing devices. Perhaps the hid-logitech-dj kernel module can also be improved to provide an API for toggling notifications or unpairing devices. More features can also be added to ltunify, for example swapping the F function keys of a keyboard, but the main purpose of this tool is to enable users to pair and unpair devices.

Other improvements could be implementing version 2.0 of the HID++ protocol, whatever benefits it may give. Since I do not have a device using the HID++ 2.0 protocol, I did not bother implementing it. If you are looking for a 2.0 example, UPower has an implementation of HID++ 2.0 for checking battery levels, contributed by Julien Danjou. (Added on 26 April 2013:) there is another tool that I was unaware of although it existed over a half year ago. The graphical Python program Solaar supports HID++ 2.0 and adds some extra functionality like solar charge status for keyboard and swapping FN keys. Definitely worth checking out!

What can other companies learn from this? USB-based protocols are very easy to capture and analyse. Given enough time, there will ultimately be a program that can use this protocol. Provide sufficient specifications to the open-source community, this makes us happy and save time!

The following third-party software have been used for this project:

Related links: