· 6 min read ·

Decoding a USB Device's Protocol with Wireshark and a Bit of Patience

Source: lobsters

Most USB devices that fall outside the standard HID, CDC, or mass storage classes speak a protocol their manufacturer never published. If you want to write your own driver, build integration tooling, or just understand what a device is actually doing, you eventually end up at the same place: capturing raw USB traffic and working backward from the bytes.

This walkthrough on crescentro.se documents exactly that process, using Wireshark to sniff traffic from an undocumented device and reconstruct enough of the protocol to drive it independently. The write-up is worth reading on its own terms, but there is a lot of surrounding context about USB architecture, capture setup, and the jump from capture to code that is worth unpacking separately.

Getting Wireshark to See USB at All

Wireshark’s USB support is mature but requires some configuration that is easy to miss.

On Linux, the kernel’s usbmon module exposes USB traffic through debugfs. Load it with modprobe usbmon, and you will find per-bus character devices under /dev/usbmon0, /dev/usbmon1, and so on. Bus 0 is an aggregate of all buses; the numbered devices correspond to the physical USB controllers visible in lsusb. Wireshark lists these as capture interfaces once you have read permissions on the device nodes, which usually means adding your user to the wireshark group or running with elevated privileges.

On Windows, USBPcap provides equivalent functionality as a kernel filter driver. It has been bundled with Wireshark since version 1.10, so if you have a reasonably current install you likely already have it. Wireshark on Windows will show USB capture interfaces in the same interface list as network adapters once USBPcap is active.

Before you start a capture, run lsusb (or check Device Manager) to identify which bus your target device is on, then capture only that bus. Capturing bus 0 on a machine with active USB hubs generates a lot of noise from keyboards, mice, webcams, and anything else connected. Filtering down to a single bus early saves significant time later.

Understanding What You Are Looking At

USB traffic in Wireshark is structured around URBs, USB Request Blocks, which are the kernel’s internal representation of a USB transaction. Each URB record shows:

  • Bus ID and device address: which physical device generated this packet
  • Endpoint number and direction: endpoint 0x01 IN versus 0x81 OUT (the high bit encodes direction)
  • Transfer type: control (0), isochronous (1), bulk (2), or interrupt (3)
  • URB status: whether the transfer completed successfully
  • Data payload: the actual bytes

The first thing to do when you start a new capture is watch the enumeration sequence. When a device is plugged in, the host issues a series of GET_DESCRIPTOR control requests: device descriptor, configuration descriptor, interface descriptors, endpoint descriptors, and string descriptors. All of this happens on endpoint 0 (the default control pipe) before any application-level communication begins.

The endpoint descriptors are particularly useful. They tell you the transfer type each endpoint uses, the maximum packet size, and for interrupt endpoints, the polling interval. A device with one bulk OUT endpoint and one bulk IN endpoint almost certainly uses a command/response protocol over those pipes. A device with an interrupt IN endpoint and no bulk endpoints is probably sending periodic status updates or sensor readings. These structural observations constrain the space of possible protocols considerably before you look at a single byte of payload.

For HID-class devices, there is an additional step: fetching the HID report descriptor. This is a complex binary structure that formally defines the meaning of every bit in every report the device sends and receives. The hid-recorder utility from the hidtools package can fetch and pretty-print this descriptor, saving you from decoding it manually out of Wireshark.

Filtering and Pattern Recognition

Raw captures are noisy. Wireshark’s display filter language has reasonable USB support:

usb.device_address == 3
usb.transfer_type == 2          # bulk only
usb.endpoint_number.direction == 1   # device-to-host only
usb.data_len > 0                # skip zero-length packets

The workflow that tends to work is to use the vendor’s own software to trigger specific, discrete actions while capturing, then correlate those actions to packet sequences. Set something to a known value, capture the resulting traffic, set it to a different known value, capture again. Differences between the two captures isolate the bytes that encode that parameter.

Vendor protocols for simple devices tend to follow recognizable structural patterns. A common one is a fixed-size command packet with a command byte in position zero, a length field, a data payload, and a checksum at the end. The checksum is often a simple XOR of the preceding bytes or a CRC-8. Once you have identified the command byte for a few operations, the rest usually falls into place quickly because similar operations share similar byte layouts.

Wireshark’s “Follow” feature, which for USB shows all traffic to and from a specific endpoint in sequence, is more useful for this kind of analysis than the standard packet list. It lets you see request/response pairs without hunting through interleaved traffic from other endpoints.

From Capture to Code

Once you have a working mental model of the protocol, libusb is the standard way to implement it without writing a kernel driver. For rapid prototyping, PyUSB wraps libusb in a clean Python API:

import usb.core
import usb.util

def open_device(vid, pid):
    dev = usb.core.find(idVendor=vid, idProduct=pid)
    if dev is None:
        raise ValueError('Device not found')
    dev.set_configuration()
    return dev

def send_command(dev, endpoint_out, endpoint_in, cmd):
    written = dev.write(endpoint_out, cmd)
    response = dev.read(endpoint_in, 64, timeout=1000)
    return bytes(response)

dev = open_device(0x1234, 0x5678)
reply = send_command(dev, 0x01, 0x81, bytes([0x10, 0x00, 0x10]))

On Linux, if the kernel has already claimed the device under a built-in driver (which happens automatically for HID and CDC devices), you need to detach it first:

if dev.is_kernel_driver_active(0):
    dev.detach_kernel_driver(0)

For anything that needs to run in production rather than a prototype script, the C libusb API gives you more control over transfer timeouts, asynchronous I/O, and hotplug events. The libusb documentation is thorough and the API is stable across platforms.

Where This Approach Has Limits

Wireshark shows you the data layer. It does not help with devices that encrypt or obfuscate their payloads at the application level. A growing number of consumer devices, particularly in the fitness and medical instrument space, use session keys negotiated during connection setup, which makes payload analysis significantly harder. In those cases you are looking at the cryptographic envelope rather than the protocol itself, and the analysis necessarily shifts toward the host-side software.

USB 3.x adds another layer of complexity. The SuperSpeed protocol introduces new transfer types and a fundamentally different physical layer. USBPcap and usbmon can still capture the logical USB traffic above the SuperSpeed layer, but if you need to debug link-layer issues, a hardware protocol analyzer becomes necessary. The Ellisys USB Explorer and Total Phase Beagle are the standard tools in that space, both expensive enough that they only make sense for professional work.

For a low-cost hardware option on the simpler end, OpenVizsla is an open hardware USB analyzer that handles full-speed and high-speed capture. It is a good middle ground between software-only capture and a commercial analyzer if you regularly work on USB projects.

The Facedancer project from Great Scott Gadgets takes a complementary approach: rather than passively sniffing traffic, it lets you emulate a USB device at the software level, which is useful for testing host-side software and fuzzing drivers without physical hardware.

The Bigger Picture

USB reverse engineering used to require either expensive commercial tools or a lot of patience with underdocumented kernel interfaces. The combination of usbmon in the mainline Linux kernel since 2.6.11, USBPcap’s integration into Wireshark, and the maturity of libusb and PyUSB has made the full workflow, from first capture to working implementation, accessible to anyone willing to spend time on it.

The skill transfers well. The same systematic approach, capture with known inputs, isolate differences, identify structural patterns, applies to other undocumented binary protocols over serial, Bluetooth RFCOMM, or proprietary RF links. The tools change; the methodology does not. Understanding USB through Wireshark is a reasonable place to build that intuition.

Was this interesting?