📜 ⬆️ ⬇️

Reverse engineering of USB device drivers on the example of a radio-controlled car

Translation of the article DRIVE IT YOURSELF: USB CAR

image

One of the arguments of Windows lovers before Linux lovers is the lack of drivers for hardware for this OS. Over time, the situation is straightened. Now it is much better than 10 years ago. But sometimes you can find some device that is not recognized by your favorite distribution. Usually it will be some kind of USB peripherals.
')
The beauty of free software is that you can solve this problem yourself (if you are a programmer). Of course, it all depends on the complexity of the equipment. With a three-dimensional webcam, you may not succeed - but many USB devices are quite simple, and you don’t have to dive into the depths of the core or dig in C. In this lesson, you and I will use Python to create a driver for a toy radio-controlled machine .

The process will essentially be reverse engineering. First we will study the device in detail, then save the data it exchanges with the driver in Windows and try to understand what they mean. For non-trivial protocols, you may need both experience and luck.

Meet USB


USB - bus with host management. The host (PC) decides which device sends the data over the wire, and when. Even in the case of an asynchronous event (pressing a button on the keyboard), it is not sent to the host now. Since there can be up to 127 devices on each bus (and even more if through hubs), such a scheme of work facilitates management.

Also, USB has a multi-layer protocol system - just like the Internet. The lowest level is usually implemented in silicon. The transport layer works through tunnels (pipe). Streaming tunnels transmit different data, message tunnels - messages to control devices. Each device supports at least one message tunnel. At the top level of an application (or class) there are protocols like USB Mass Storage (flash drives) or Human Interface Devices (HID), devices for human-computer interaction.

In wires


A USB device can be thought of as a set of endpoints, or I / O buffers. Each has a data direction (input or output) and type of transfer. By type, the buffers are as follows: interrupts, isochronous, control, and packet.

Interrupts transmit data in a little bit in real time. If the user presses a key, the device waits until the host asks “have they pressed buttons there?”. The host should not slow down, and these events should not be lost. Isochronous work in approximately the same way, but not so hard - they allow you to transfer more data, while allowing them to be lost when it is not critical (for example, webcams).

Batch designed for large volumes. So that they do not clog the channel, they are given the entire place, which is now not occupied by other data. Managers are used to control devices, and only they have a rigidly defined format for requests and responses. A set of buffers with associated metadata is called an interface.

Any USB device has a buffer number zero — this is the default tunnel endpoint used for control data. But how does the host know how many buffers the device has and what type of buffers are they? For this purpose, different descriptors are used, sent on special requests through the default tunnel. They can be standard for all, special for specific classes of devices, or proprietary.

Descriptors make up a hierarchy that can be viewed with utilities like lsusb. Upstairs sits a device handle containing the Vendor ID (VID) and Product ID (PID). This pair is unique for each device, the system finds the necessary driver by it. A device can have several configurations, each with its own interface (for example, a printer, a scanner and a fax in an MFP). But usually one configuration with one interface is defined. They are described by the corresponding descriptors. Each endpoint has a descriptor containing its address (number), direction (input or output), and type of transmission.

Class specifications have their own descriptor types. The USB HID specification expects data transmission in the form of “reports” that are sent and received via a control buffer or interrupt. These descriptors define the format of the report (for example, “1 field 8 bits long”) and how it should be used (“offset in the X direction”). Therefore, the HID device describes itself and can be supported by a universal driver (usbhid in Linux). Otherwise, I would have to write my own driver for each mouse.

I will not try to describe hundreds of pages of specifications in a few paragraphs. I ’m referring to the book O'Reilly's “USB in a Nutshell”, which is available for free at www.beyondlogic.org/usbnutshell . Let's do something better.

Understanding permissions


By default, USB devices can only work from under the root. To prevent the test program from running this way, add the udev rule:

SUBSYSTEM=="usb", ATTRS{idVendor}=="0a81", ATTRS{idProduct}=="0702", GROUP="INSERT_HERE", MODE="0660" 


Insert the name of the group to which your user belongs, and add it to /lib/udev/rules.d/99-usbcar.rules.

Under the hood


Let's see how the machine looks via USB. lsusb is a tool for counting devices and decoding their descriptors. Included in the usbutils kit.

 [val@y550p ~]$ lsusb Bus 002 Device 036: ID 0a81:0702 Chesen Electronics Corp. Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub ... 


The machine is a Device 036 (to be sure, you can unplug it and run lsusb again). The ID field is a pair of VID: PID. To read the descriptors, run lsusb -v:

 [val@y550p ~]$ lsusb -vd 0a81:0702 Bus 002 Device 036: ID 0a81:0702 Chesen Electronics Corp. Device Descriptor: idVendor 0x0a81 Chesen Electronics Corp. idProduct 0x0702 ... bNumConfigurations 1 Configuration Descriptor: ... Interface Descriptor: ... bInterfaceClass 3 Human Interface Device ... iInterface 0 HID Device Descriptor: ... Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: ... bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt ... 


Standard hierarchy. Like most devices, it has only one configuration and interface. You can see one interrupt-in endpoint (except for the default point 0, which is always there, and therefore is not displayed in the list). The bInterfaceClass field indicates that this is a HID device. This is good - the protocol of communication with HID is open. It would seem that we will read the report descriptor in order to understand their format and use, and the trick is done. However, he has a mark ** UNAVAILABLE **. CHZN? Since the machine is a HID device, the usbhid driver assigned it to itself, but does not know what to do with it. We need to untie him from managing it.

First you need to find the bus address. Reconnect it, run dmesg | grep usb, and look at the last line, starting with usb XY.Z :. X, Y, and Z are integers that uniquely identify ports on a host. Then run

 [root@y550p ~]# echo -n XY.Z:1.0 > /sys/bus/usb/drivers/usbhid/unbind 


1.0 is the configuration and interface that the usbhid driver should release. To tie everything back, write the same thing in / sys / bus / usb / drivers / usbhid / bind.

Now the Report descriptor field provides information:

 Report Descriptor: (length is 52) Item(Global): Usage Page, data= [ 0xa0 0xff ] 65440 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) ... Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x02 ] 2 ... Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Output, data= [ 0x02 ] 2 ... 


Two reports are set. One reads from the device (input), the second writes (output). Both are in bytes. However, their use is not obvious. For comparison, here is what the mouse report descriptor looks like (not all, but the main lines):

 Report Descriptor: (length is 75) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x02 ] 2 Mouse Item(Local ): Usage, data= [ 0x01 ] 1 Pointer Item(Global): Usage Page, data= [ 0x09 ] 9 Buttons Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Button 1 (Primary) Item(Local ): Usage Maximum, data= [ 0x05 ] 5 Button 5 Item(Global): Report Count, data= [ 0x05 ] 5 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x02 ] 2 


It's all clear. It’s not clear with the machine, and we need to figure out how to use the bits ourselves.

Small bonus


Most radio-controlled toys are simple and use standard receivers operating at the same frequencies. So, our program can be used to control other toys, except this machine.

Job for detective


When analyzing network traffic using a sniffer. And in our case, such a thing is useful. There are special USB monitors for commercial use, but Wireshark will be suitable for our task.

Configure USB interception in Wireshark. First, enable USB monitoring in the kernel. Load the usbmon module:

 [root@y550p ~]# modprobe usbmon 


Mount the special debugfs file system:

 [root@y550p ~]# mount -t debugfs none /sys/kernel/debug 


The / sys / kernel / debug / usb / usbmon directory will appear, which can be used to record traffic using simple shell tools:

 [root@y550p ~]# ls /sys/kernel/debug/usb/usbmon 0s 0u 1s 1t 1u 2s 2t 2u 


There are files with mysterious names. Integer - bus number (the first part of the USB bus address); 0 means all buses on the host. s - statistics, t - transfers, u - URBs (USB Request Blocks logical entities representing ongoing transactions). To save all transfers on bus 2, enter:

 [root@y550p ~]# cat /sys/kernel/debug/usb/usbmon/2t ffff88007d57cb40 296194404 S Ii:036:01 -115 1 < ffff88007d57cb40 296195649 C Ii:036:01 0 1 = 05 ffff8800446d4840 298081925 S Co:036:00 s 21 09 0200 0000 0001 1 = 01 ffff8800446d4840 298082240 C Co:036:00 0 1 > ffff880114fd1780 298214432 S Co:036:00 s 21 09 0200 0000 0001 1 = 00 


For the untrained eye, nothing is clear here. Good thing Wireshark will decode the data.

Now we need Windows, which will work with the original driver. It’s best to install everything in VirtualBox (with the Oracle Extension Pack, since we need USB support). Make sure that VirtualBox can use the device, and launch KeUsbCar, which controls the machine in Windows. Run Wireshark to see which commands the driver sends to the device. On the first screen, select the usbmonX interface, where X is the bus to which the machine is connected. If Wireshark is not started from root, make sure that the / dev / usbmon * nodes have appropriate permissions.

image

Click the Forward button in KeUsbCar. Wireshark will intercept several outgoing control packets. The screenshot shows the one we need. Judging by the parameters, this is the SET_REPORT request (bmRequestType = 0x21, bRequest = 0x09), which is usually used to change the status of a device, such as lights on a keyboard. According to the Report Descriptor we have seen, the data length is 1 byte, and the report itself contains 0x01 (also highlighted).

Pressing the Right button results in a similar query. But the report already contains 0x02. You can guess that this means the direction of movement. In the same way, we find out that 0x04 is the right reverse, 0x08 is reverse, and so on. The rule is simple: the direction code is a binary one, shifted to the left by the position of the button in the KeUsbCar interface, if you count them clockwise.

You can also note periodic interrupt requests from Endpoint 1 (0x81, 0x80 means that this is an entry point; 0x01 is its address). What is it? In addition to the buttons, KeUsbCar has a charge indicator, so this is possibly battery data. Their value does not change (0x05) if the car does not leave the garage. Otherwise, interrupt requests do not occur, but they are renewed if we put it back. Then, apparently, 005 means "charging is in progress" (the toy is simple, therefore the charge level is not transmitted). When the battery is charged, the interrupt will begin to return 0x85 (0x05 with 7 bits set). Apparently, 7 bits means "charged." What bits 0 and 2, which are 0x05, do is not yet clear.

We write almost real driver


Making the program work with a device that was not previously supported is good, but sometimes you need to make sure that the rest of the system works with it. This means you need to do a driver, and this requires programming at the kernel level (http://www.linuxvoice.com/be-a-kernel-hacker/), and you hardly need it now. But perhaps we can manage without it, if we are talking about USB.

If you have a USB network card, you can use TUN / TAP to connect the PyUSB program to the Linux network stack. The TUN / TAP interfaces work as usual network, with names like tun0 or tap1, but through them all the packages become available in the / dev / net / tun node. The pytun module makes working with TUN / TAP simple. The speed suffers, but you can rewrite the program in C using libusb.

Another candidate is a USB display. Linux has a vfb module that allows you to access the framebuffer as / dev / fbX. You can use ioctls to redirect the console to it, and upload the contents of / dev / fbX to a USB device. This is also not fast, but you are not going to play 3D shooters via USB.

Write the code


Let's make the same program as for Windows. 6 arrows and charge level, which flashes when the machine is charging. The code is on GitHub github.com/vsinitsyn/usbcar.py

How do we work in USB under Linux? This can be done from user space using the libusb library. It is written in C and requires good USB knowledge. A simple alternative is PyUSB. For the user interface, I used PyGame.

Download PyUSB sources from github.com/walac/pyusb , and install via setup.py. You will also need to install the libusb library. Put the functionality to control the machine in the class with the original name USBCar.

 import usb.core import usb.util class USBCar(object): VID = 0x0a81 PID = 0x0702 FORWARD = 1 RIGHT = 2 REVERSE_RIGHT = 4 REVERSE = 8 REVERSE_LEFT = 16 LEFT = 32 STOP = 0 


We import the two main modules of PyUSB and insert the values ​​to control the machine, which we calculated when viewing traffic. The VID and PID are the machine id taken from the lsusb output.

 def __init__(self): self._had_driver = False self._dev = usb.core.find(idVendor=USBCar.VID, idProduct=USBCar.PID) if self._dev is None: raise ValueError("Device not found") 


The usb.core.find () function searches for a device by its ID. See github.com/walac/pyusb/blob/master/docs/tutorial.rst for details.

  if self._dev.is_kernel_driver_active(0): self._dev.detach_kernel_driver(0) self._had_driver = True 


We untie the kernel driver, as we did with lsusb. 0 - interface number. Upon exiting the program, it must be bound back through release (), if it was active. Therefore, we remember the initial state in self._had_driver.

  self._dev.set_configuration() 


Run the configuration. This code is equivalent to the following code, which PyUSB hides from the programmer:

  self._dev.set_configuration(1) usb.util.claim_interface(0) def release(self): usb.util.release_interface(self._dev, 0) if self._had_driver: self._dev.attach_kernel_driver(0) 


This method must be called before the end of the program. We release the interface we used and attach the kernel driver back.

Moving cars:

 def move(self, direction): ret = self._dev.ctrl_transfer(0x21, 0x09, 0x0200, 0, [direction]) return ret == 1 


direction is one of the values ​​defined at the beginning of the class. ctrl_transfer () passes control commands. Data is transmitted as a string or as a list. Returns the method the number of bytes written. Since we have only one byte, we will return True in this case, and False in another.

Method for battery status:

 def battery_status(self): try: ret = self._dev.read(0x81, 1, timeout=self.READ_TIMEOUT) if ret: res = ret.tolist() if res[0] == 0x05: return 'charging' elif res[0] == 0x85: return 'charged' return 'unknown' except usb.core.USBError: return 'out of the garage' 


The read () method takes the address of the endpoint and the number of bytes to read. The type of transfer is determined by the end point and is stored in the descriptor. We also set non-standard timeout time for the program to run faster. Device.read () returns an array that we convert to the list. We check its first byte to determine charging status. If the machine is not in the garage, then the read () call fails and throws usb.core.USBError error. We assume that this error occurs precisely because of this. In other cases, we return the status to 'unknown'.

The UI class encapsulates the user interface. Let's go through the main things. The main loop is in UI.main_loop (). We set the background with the picture, show the level of charge, if the machine is in the garage, and draw control buttons. Then we wait for the event - if it is a click, then we move the machine in the given direction via USBCar.move ().

The entire program, including the GUI, takes a little more than 200 lines. Not bad for a device without documentation.

Of course, we specifically took a fairly simple device. But there are quite a few devices in the world that are similar to ours, and many use protocols that are not very different from what we picked. Reverse engineering of a complex device is not an easy task, but now you can add support for Linux to some kind of trinket like a device that reports received e-mail. If it is not very useful - at least it is interesting.

Source: https://habr.com/ru/post/254251/


All Articles