Reverse Engineering Logitech Unifying USB Protocol

*For those of you looking for something to let you attach more than one logitech device in linux, you should go to the downloads page.

Just finished a reverse engineering and building a “driver” that will let you link more than one device to a Logitech unifying receiver in Linux!  Yay!  (Big thanks here goes to Kevin, who helped me through some bugs/was a console guru)

For those of you who haven’t used the “unified” series of products from logitech, the selling point is that you can have multiple wireless peripherals (Keyboards or mice) that all link up to single TINY USB dongle.  To pair to the first device, all you do is plug in the receiver, and turn on your peripheral.  After that, they are paired!  This is awesome for windows/Mac users, because they can use the provided software to connect more devices.  For linux users, this is not so easy.  There are no drivers, and this means there is no way to pair more than one device to a receiver.  This means that to use a keyboard and a mouse, you need to:

  1. Find a win/mac machine, pair, and basically pray that the devices never get unpaired
  2. Use one receiver per device (expensive in terms of usb ports)
  3. Write your own program to pair devices

Since I had already cracked a receiver open and soldered it onto the mobo of my EEEpc, I decided that 1) and 2) were out of the question.  Thankfully I had another receiver and another machine to test on.  I decided my program really only needs to do one thing, which is to pair to devices not near other receivers.  I figured I could take advantage of a case that the Logitech engineers must have planned for, loosing a receiver.  Since these things are so tiny, and can be easily misplaced/stolen/eaten, I assumed that already paired devices would be willing to pair to another device given that they are not within range of a paired receiver.  If within range of a paired receiver, it doesn’t make sense for a device to pair, because then your mouse would pair to other peoples receivers more or less at random (ever time you turned it on).

I started by booting into windows and using a program called usblyser to analyze usb traffic.  It turns out it was mostly useless, due to the clunky interface, and I didn’t want to install pyusb on my windows partition, so I moved on to a different solution.  This time around I installed virtual box from oracle (NOT the OSE edition, which doesn’t have usb extensions).  I created a VM running windows XP, downloaded the software from logitech, and let the VM grab the usb dongle.  I paired and unpaired a few devices to test it.

Now I was ready for some sniffing.  I started out by mounting the debug filesystem so I could use usbmon (usb monitoring) which is some kind of linux utility.  To do this I used:

sudo mount -t debugfs none_debugfs /sys/kernel/debug

sudo modprobe usbmon

The I used wireshark to look at the output.  This was a HUGE MISTAKE.  For some reason, wireshark thinks USB is big endian.  This is basically the direction it “reads” data that is transmitted.  for example, if it received ‘011’ it would think that the most significant bit was 0, then 1 then 1, so it would say the decimal value was 3.  a little endian interpretation would say the MSB was 1, then 1 then 0 so that the value is 6.

For the longest time I thought the wValue and wIndex of the control command I was trying to send were 0x1002 and 0x0200, because Wireshark was telling me that the setup packet in the USB request block was “21 09 10 02 02 00 07 00”.  I should have known something was fishy, because I knew the request length was 7 because 7 bytes were returned.  What didn’t dawn on me until I looked at the raw usbmon logs was that the values of the wValue, wIndex and wLength were reversed.  It should have read:

“21 09 02 10 00 02 00 07”

Without the right setup packet, I kept getting USB I/O and USB pipe errors.  The moral of the story is that usbmon doesn’t need no stinkin’ wireshark.  Just use:

cat /sys/kernel/debug/usb/usbmon/u0

To look at the raw output.  When I did that I got a lot of garbage, which I then had to sort through carefully and think about.  I went tested the logitech software by pressing buttons and watching the USB requests it generated.  Here what I noticed:

Opening the program:  This generated TONS of USB traffic that I really didn’t want to sift though.  I made a gamble here and assumed logitech didn’t want this to be hard to do, and that this traffic was the program asking the dongle things like “Are you there? do you have any attached devices? what can you tell me about them?” and such.  If worst had come to worst, I would have just replicated these packets without knowing what they did.

Hitting the “Advanced” button:  This also generated tons of USB traffic.  Boo!  I think what mostly goes on here is that the program queries the device to see if anything new has been attached/dropped out of range/has been used, because the GUI here shows all the attached devices, and if you use a device a little icon blinks.  The requests looked like a simple call and response, but I didn’t want to mess with that much traffic.

Hitting the “next” button to get to the pairing scree:  This generated very little traffic (3 requests) and it also seemed to be what put the dongle “in the mood” to pair.  I decided that this was a good point to start my “attack” on the protocol.  I also made the call here that there would probably not be a ton of complications or security here, because there is no reason to make this protocol less likely to work/install security.

Taking that as my starting point, I booted/shut down my VM and the program several times, each time hitting the next button.  The following always occurred when I did that:

e20bb300 755547433 S Co:2:008:0 s 21 09 0210 0002 0007 7 = 10ff80b2 01503c
e20bb300 755547611 C Co:2:008:0 0 7 >
e7b69400 755549571 C Ii:2:008:3 0:2 7 = 10ff4a01 000000
e20bb300 755550445 S Ii:2:008:3 -115:2 32 <
e20bb300 755551614 C Ii:2:008:3 0:2 7 = 10ff80b2 000000
efb5ef00 755552476 S Ii:2:008:3 -115:2 32 <

This is in dense usbmon-speak, but if you take a look at the documentation its not so bad.  The deciphered version of the first two lines is just that a control transfer was submitted (S) and completed (C).  The setup packet for the transfer was “21 09 0210 0002 0007” meaning it was a class request sending 7 bytes of data out, and the data was “10ff80b2 01503c”.  I guessed that this was probably the “Go pair with stuff” command, and wrote a little python to send it:

dev.ctrl_transfer(0x21,0x09,0x0210,0x0002, [0x10,0xff,0x80,0xb2,0x01,0x50,0x3c])

This gave me an error that was something like “device busy”.  This is because my computer thought that the dongle was a HID device, which it is.  But for now, we want to suspend that function.  So I closed down my VM and typed:

sudo modprobe -r usbhid

to temporarily stop usbhid from using it.  Once I got the control transfer to work, I tried pairing a device.  Nothing appeared to happen, other than the initial pairing of one device to the receiver.  I decided that I would be best to duplicate the rest of the traffic that happened when hitting the next button before I gave up.

The next request I wanted to duplicate was a read of 7 bytes from endpoint 3 (based on usbmon).  For the longest time I thought the right code was:

dev.read(3,7,0)

Because I wanted to read from endpoint 3, 7 bytes, interface 0.  WRONG.  It turns out that endpoint 3 has an address described by the usb protocol, a useful explanation of which can be found here.  What I also discovered with the verbose mode (lsusb -v) of lsusb, was this:

bEndpointAddress     0x83  EP 3 IN

hey, thats not 3.  The right line was actually:

dev.read(0x83,0x7,2)

The two came from guessing an interface by luck, but I don’t think there are normally out of the range [0,4].

Testing this again showed that this was indeed the right command to do something…?  It at least lets me pair my keyboard and my mouse to the same receiver!  I don’t completely understand what this line means, but I also saw it when USB requests were made when leaving pairing mode.  Maybe it says something about the device, like how many devices are paired to it, or their names; all I know is that it is necessary to push the receiver into pairing.  The final thing to do is to turn HID devices back on with:

sudo modprobe usbhid

The little script that plays these requests will be available on the Downloads page.  The modprobing needs to be done manually.  If you want to help improve it, or if you have some logs from your receiver that look different on the “next”/pairing step, I would love to see them.

36 thoughts on “Reverse Engineering Logitech Unifying USB Protocol