Using the Linux kernel's USB authorization support to lockdown USB

USB is not secure. The way a USB device looks, doesn't necessarily indicate its real functionality. A device which looks like a USB flash drive could act as a keyboard once it is plugged into a machine and inject arbitrary key strokes (thus possibly allowing arbitrary malicious stuff). With BadUSB or devices like the USB rubber ducky such attacks are even more easy to achieve and available to the masses.

This post is about how you can use the Linux kernel's USB authorization support to lockdown USB and check a USB device before you allow the kernel to load the driver.

Update

This article has been updated on 17th February 2020. In the original article was a YouTube talk linked from Krzysztof Opasiak who explained the basics of USB. Unfortunately, this video was removed from YouTube. I updated the article with another video from the same author. This video is more detailed regarding USB authorization support. In addition, the tool was updated and the article reflects that.

USB Basics

Before we start it is helpful to get some more insight into USB. There is a great talk by Krzysztof Opasiak which I recommend to watch before you continue reading, he gives a nice intro to USB. The interesting part for this article are roughly the first 22 minutes.

So let's summarize the video a bit.

USB: Host, devices, endpoints, interfaces and configurations

In USB there is a host and there are devices. Devices provide functionality like printing or storage or even multiple functionalities. Only host and device can communicate directly with each other (point-to-point). A direct communication between multiple USB devices is not possible.

As mentioned in the video a USB device has endpoints (1-32). These are used for communication between host and device and have unique addresses. There are different endpoint types for different ways of transferring data. These endpoints are grouped into interfaces which implement functionality like storage, printing and so on. Interfaces again are grouped into configurations. One or multiple interfaces per configuration are possible. All interfaces of a configuration can be used at the same time, but only one configuration can be active at the same time, even if the device supports multiple configurations!

What happens if a USB device is plugged in?

Once a USB device is plugged in and detected a unique address will be assigned to the device. In order for the host to find out the device capabilities, it needs to get some info from the device. These information is stored in various descriptors (device, configuration, interface and endpoint descriptor). They store different information like the product and vendor ID (device descriptor), the maximum power which is needed to be drained from the host in a specific configuration (configuration descriptor) or information to which class a USB device or interface belongs to (device and interface descriptor). There are multiple "generic" classes available. If an interface implements a specific class, that means it implements a well standardized protocol for which a generic driver can be loaded. Here is a list of the currently defined USB class codes.

Above we mentioned, that only one configuration can be active at a time. So if a device has multiple configurations, which one is chosen? Krzysztof mentioned multiple requirements:

  1. Can the host provide enough power?
  2. Does the configuration have at least one interface?
  3. Does the device have only one config? Then it will be used.
  4. Otherwise choose one which first interface is not Vendor specific.

(Whether an interface is vendor specific can be told from the class. If you have a look at the list of class codes linked above, you will see that 0xffh marks a vendor specific interface.) Then all interfaces of a chosen configuration will be enabled and driver(s) need to be chosen.

How a suitable driver for an interface is chosen

If the interface needs a vendor specific driver, the vendor and product ID are used to find the right driver. Otherwise the class information provided by the descriptors is used to load a generic driver for the class. In this case the vendor and product ID are ignored. It is important to note, that a single driver can make use of multiple interfaces (we need that later).

What could possibly go wrong?

So depending on what sort of interface and driver we have, different things will happen. A new network interface might be set up and the network manager will handle the interface or a keyboard driver is loaded and x11 will listening for input events and so on. This is where the malicious USB device can start its evilness...

USB authorization to the rescue

...and where the USB authorization feature of the Linux kernel comes into play. The kernel basically allows to "lockdown" all interfaces by default so that no driver will be loaded automatically any longer. That gives the user the opportunity to examine the device first (e.g. via lsusb). Once the USB device has been examined by the user, specific interfaces can be "unlocked" and the kernel can be triggered to finally load the driver. This is all described here: https://www.kernel.org/doc/Documentation/usb/authorization.txt

In order to automate that, we need a script which locks all interfaces first and when waits for any new devices being plugged in. The script then should give us some information about the device like interface class information upon which the user can decide if he wants to allow an interface or not. Finally the user must be able to unlock certain interfaces (or keep them locked).

I wrote a python tool which does that. You can find the code on GitHub.

A Python Hack

usblockdown.py in action

The tool run can be seen from the screenshot. All interfaces will be locked when the script is started (you need root rights for that). Once you plug-in a USB device, it will give you a summary of the device, including descriptor information as well as how many configurations are supported including detailed information about all configuration's interfaces. In the example run, the device only has one configuration with one interface (Mass Storage).

For every interface the user will be prompted whether he wants to unlock the interface or not. In addition, it can be chosen for every interface whether the corresponding driver should be probed after unlocking or not. The reason for that is mentioned at the end of the documentation:

For drivers that need multiple interfaces all needed interfaces should be authroized first. After that the drivers should be probed. This avoids side effects.

If you don't probe for the driver, the script will print the interface to you. So you can manually probe the driver later. For example if the interface is "2-2:1.0" you can probe via:

# echo 2-2:1.0 > /sys/bus/usb/drivers_probe

At any point in time you can terminate the script via CTRL+C. You will then be asked if you want to unlock all interfaces. If you choose "no", the interfaces will still be locked (which means that if you plug in a USB device after that, it will not work).
I use pyudev to get informed about new USB devices. In addition pyusb is needed to get the device configuration(s) and interface(s).

A last note...

There is something left which I'd like to note. The USB authorization mechanism helps to protect against USB related attacks, but still indirect attacks over USB are possible.

Let's say you found a device which has two interfaces, one for Mss Storage and one for Human Interface Device (HID). You allow the Mass Storage one but you don't trust the HID one, since it could input malicious key strokes. An attacker could still have modified the Mass Storage device in a way, that it exploits a vulnerability in the file system driver of the Linux Kernel, once the device is being mounted. Of course, such an attack can the USB authorization mechanism not prevent directly.

comments (2) - add comment

mw
Took me a while, but the article is finally updated. :)
mw
I just realized that the YouTube video doesn't exist any longer. I will update the article soon.