UBOAT or CVE-2016-3955

[U]SB/IP [B]uffer [O]verflow [AT]tack

UBOAT is a vulnerability in USB over IP framework, which is part of Linux kernel code. This framework was originally developed by USB/IP project and merged into mainline Linux kernel since version 3.17 and allows hardware to share connected USB devices over IP network: devices, connected to USB/IP server, appear on the client as if they were plugged in locally.

UBOAT allows an attacker to write arbitrary data to Linux kernel memory heap on USB/IP client, possibly causing denial of service (DoS) or arbitrary code execution at privileged levels.

How it works? (or fails..)

The project website and related paper have detailed explanation of the architecture and implementation of the USB/IP framework. To read further, first let’s identify core roles:

  • server - machine, which has physical USB devices connected and “exports” them over network connection
  • client - machine that connects to the server and “imports” devices over the network (devices appear to be as plugged in directly to this machine)

Basically, the framework consists of these components:

  • USB stub driver - runs on the server, intercepts all USB request blocks (URBs) from the physical device and transmits them over the network
  • USB virtual host controller - runs on the client, receives URBs from the network and passes them to appropriate device drivers through local USB subsystem
  • userspace helper tools - run on client and server, establish a network connection between client and server, handle configuration etc

The key part here is the connection management: unlike conceptually similar subsystems (like TUN/TAP driver for example), where kernel just exposes some low-level structure or blob to userspace and userspace is responsible to deliver it appropriately, USB/IP takes a bit different approach. Userspace helper applications basically just establish a TCP connection between client and server and then pass the socket descriptor to the kernel. Then the kernel itself actually implements all the protocol logic and network communication over this socket.

Once the connection is established USB/IP network protocol is rather simple: it contains a header with some URB metadata and URB itself. The vulnerability lies in the protocol parsing logic on the client side.

linux/drivers/usb/usbip/vhci_rx.c:

static void vhci_recv_ret_submit(struct vhci_device *vdev,
                                 struct usbip_header *pdu)
{
...
    /* unpack the pdu to a urb */
    usbip_pack_pdu(pdu, urb, USBIP_RET_SUBMIT, 0);

    /* recv transfer buffer */
    if (usbip_recv_xbuff(ud, urb) < 0)
        return;

    /* recv iso_packet_descriptor */
    if (usbip_recv_iso(ud, urb) < 0)
        return;
...

Above is the client-side kernel code for receiving an URB from the server: it just parses the USB/IP protocol header and treats rest of the data as the URB itself:

linux/drivers/usb/usbip/usbip_common.c

static void usbip_pack_ret_submit(struct usbip_header *pdu, struct urb *urb,
                  int pack)
{
    struct usbip_header_ret_submit *rpdu = &pdu->u.ret_submit;

    if (pack) {
        rpdu->status        = urb->status;
        rpdu->actual_length    = urb->actual_length;
        rpdu->start_frame    = urb->start_frame;
        rpdu->number_of_packets = urb->number_of_packets;
        rpdu->error_count    = urb->error_count;
    } else {
        urb->status        = rpdu->status;
        urb->actual_length    = rpdu->actual_length;
        urb->start_frame    = rpdu->start_frame;
        urb->number_of_packets = rpdu->number_of_packets;
        urb->error_count    = rpdu->error_count;
    }
}

void usbip_pack_pdu(struct usbip_header *pdu, struct urb *urb, int cmd,
            int pack)
{
    switch (cmd) {
    case USBIP_CMD_SUBMIT:
        usbip_pack_cmd_submit(pdu, urb, pack);
        break;
    case USBIP_RET_SUBMIT:
        usbip_pack_ret_submit(pdu, urb, pack);
        break;
    default:
        /* NOT REACHED */
        pr_err("unknown command\n");
        break;
    }
}
...
int usbip_recv_xbuff(struct usbip_device *ud, struct urb *urb)
{
    int ret;
    int size;

    if (ud->side == USBIP_STUB) {
        /* the direction of urb must be OUT. */
        if (usb_pipein(urb->pipe))
            return 0;

        size = urb->transfer_buffer_length;
    } else {
        /* the direction of urb must be IN. */
        if (usb_pipeout(urb->pipe))
            return 0;

        size = urb->actual_length;
    }

    /* no need to recv xbuff */
    if (!(size > 0))
        return 0;

    ret = usbip_recv(ud->tcp_socket, urb->transfer_buffer, size);
    if (ret != size) {
        dev_err(&urb->dev->dev, "recv xbuf, %d\n", ret);
        if (ud->side == USBIP_STUB) {
            usbip_event_add(ud, SDEV_EVENT_ERROR_TCP);
        } else {
            usbip_event_add(ud, VDEV_EVENT_ERROR_TCP);
            return -EPIPE;
        }
    }

    return ret;
}

We see that usbip_pack_pdu calls usbip_pack_ret_submit, which on unpack operation just puts the received actual_length from the server in the urb structure. Later, the code in usbip_recv_xbuff uses this urb->actual_length to receive the actual URB and place it in the urb->transfer_buffer. The problem here is that there are no checks, that the received previously urb->actual_length can fit in the urb->transfer_buffer.

Can URB transfer buffer be smaller than the length of the actual URB data?

In short in real world normal USB implementation: no. But…

Let’s recap how USB protocol works quickly: USB is a master-slave protocol, where USB host (usually your PC) is the master and different plugged-in USB devices (such as thumbdrives, cameras, phones etc) are slaves. Each act of communication (sending an URB) between USB host and a device is initiated by the host (even if the communication direction is device->host). So (at least in Linux USB implementation) URB structures and associated buffers are allocated either by USB core subsystem or by specific device drivers, which still talk to the actual hardware through USB core subsystem. That means that USB host is expected to allocate large enough buffer for the response it expects. This expectation makes sense in a normal USB scenario, because:

  • USB specification clearly defines maximum buffer sizes for all standard URBs, so Linux USB core subsystem can handle those
  • device drivers are expected to properly handle their respective devices (that what they are for in the first place), so they should “know” needed buffer sizes

Above can only be abused, if you have hand-crafted, possibly malicious “misbehaving” device, plugged-in to your USB host. That’s partially why plugging in USB devices of unknown origin (like a USB stick you might find on the street or get for free at a conference) is a bad idea.

But USB/IP subsystem broadens the attack surface: since now the URB data is transferred over the network in unprotected form, a MiTM attack is possible, where an attacker can modify the URB data in transit even if it comes from a legitimate hardware device. And not only the attacker can make the response larger than the USB host (client in USB/IP terminology) expects, he can actually modify and append the URB data at his will. Which brings us to…

Linux kernel heap exploitation

So, the attacker can write any data with (almost) any length into urb->transfer_buffer. Why is it dangerous? The most dangerous part here is if the attacker’s data is larger than urb->transfer_buffer, Linux will write the excessive part past the buffer boundary (welcome to the world of dangerous C programming and buffer out-of-bounds access).

But what is there past urb->transfer_buffer? Usually this buffer is allocated from the kernel heap using kmalloc function (if you do not know, what a memory heap is, please at least read this). Linux kernel heap implementation is much more complicated, of course, but the basic idea that there may be other “kernel objects” located after the urb->transfer_buffer. And since kernel is essentially “one big program”, these objects may not be even from USB subsystem, but any other subsystem. Since most of these kernel objects contain some control data or state, an attacker can override this state gaining some benefits. The simplest attack is to write some random data to these objects and in most of the cases the kernel will get confused and eventually crash. This is usually called a denial of service attack, where an attacker can remotely “shutdown” hardware at his will mostly to cause service outage.

More advanced attack would be to try to overwrite some control structures. For example, let’s assume that memory after urb->transfer_buffer contains a task_struct (which describes every process in the system). One of the members of this structure is the process effective user id. By overwriting this id (make it zero) attacker can elevate privileges of this process (make it run from root).

One may argue that because of non-deterministic nature of the heap the attacker has to be “really lucky” to catch needed object in the proper place (heap slot), however there are certain techniques to force the system to put this object there with high probability. Previous heap vulnerabilities proved such attacks not only possible, but even practical, for example, this detailed description on how to exploit kernel heap overflow reported in CVE-2010-3874.

Who is affected?

In short, if your are not using this framework, you are safe. Most distributions by default compile USB/IP code as loadable modules. These modules will be loaded by USB/IP userspace helper tools only if you start using them.

If you do use the framework, the vulnerable code runs on hardware which imports USB devices. Since, by design, this hardware runs USB/IP client software, to exploit this vulnerability remotely the attacker either needs an existing network connection for already imported USB device or to somehow trick the client to import a new device from the USB/IP server (because it is the client who initiates the network connection). In other words, if you do run USB/IP client software, you should be safe from remote attacks until you actually import at least one device.

Ways to protect yourself and others

  1. Patch quickly and patch often.

    Security patches are usually distributed via separate repository by most popular Linux distributions. Unlike other updates they should be safe to apply and do not introduce any side-effects (unless some software depends on the vulnerability itself). Specifically for UBOAT, make sure your kernel codebase has this patch in it.

  2. Protect your traffic.

    Even if you have already patched this vulnerability sending low-level kernel structures (which later control execution flow of the most privileged part of your system) over the network is a risky thing. For example, even on non-vulnerable system, an attacker can modify network traffic and add another “virtual” USB keyboard device and use it to control your server remotely. Therefore it is vital to understand the risks involved when using such framework/solution. This critical network traffic should always be encrypted and authenticated. Unfortunately, USB/IP project does not support communicating over secure protocols (such as TLS), however you can use third-party tools such as stunnel to wrap insecure connections into TLS.

  3. Rethink your architecture from security perspective

    This is more for project designers/maintainers. Handling L7 “application-level” network communication directly in kernel is probably a bad idea. The general Linux approach “tools in kernel and policy in userspace” is preferable in this case. Specifically for USB/IP, the design should probably be similar to other “virtual” appliances/frameworks (like TUN/TAP, FUSE etc): kernel code exposes low-level structures to some userspace process through well-defined tightly controlled interfaces and this process is responsible for implementing actual processing logic.