Sniffing the Bus: USB Protocol Reverse Engineering from Raw Captures to Working Code
Source: lobsters
When a device has no open specification and the vendor SDK is either proprietary or nonexistent, the USB bus itself becomes the documentation. This is the core premise behind this walkthrough of using Wireshark to reverse-engineer a USB device, and it is a premise with a long and productive history in open-source hardware support.
Projects like libmtp, libgphoto2, and OpenRGB all owe some portion of their existence to exactly this technique: capture what a working proprietary client sends, reconstruct the protocol, implement it cleanly. The tools have gotten better over the years, but the fundamental approach has not changed.
What You Are Actually Looking At
USB operates on a host-centric model. The host, your computer, always initiates. Devices respond. Everything on the bus goes through a hierarchy of descriptors: device, configuration, interface, endpoint. The endpoint is the fundamental communication channel, and every device has at least one: endpoint zero, the control endpoint, used for device enumeration and configuration.
Beyond endpoint zero, devices declare additional endpoints based on what they need to do. A keyboard uses an interrupt endpoint, polled by the host every few milliseconds. A webcam uses isochronous endpoints for real-time video. A USB storage device uses bulk endpoints for reliable, high-throughput transfers that tolerate latency. Understanding which transfer type a device uses is the first thing to determine when approaching an unknown protocol, because the transfer type constrains what the communication can look like.
The kernel communicates with USB devices through structures called URBs, USB Request Blocks. Each transfer generates at minimum two URBs: a submit (the host sending a request) and a complete (the result). Every URB carries the bus number, device address, endpoint number, transfer type, direction, payload length, and the raw data. This is everything Wireshark shows you when you capture USB traffic.
Setting Up the Capture
On Linux, USB capture goes through usbmon, a kernel subsystem that exposes USB traffic through debugfs. Load it with:
sudo modprobe usbmon
Once loaded, you will find per-bus text interfaces at /sys/kernel/debug/usb/usbmon/1t, /sys/kernel/debug/usb/usbmon/2t, and so on, plus binary interfaces at /dev/usbmon1, /dev/usbmon2, etc. Wireshark reads the binary interfaces directly. Launch it, and you will see USB buses listed alongside your network interfaces.
Before capturing, identify your device with lsusb:
Bus 003 Device 007: ID 1234:5678 SomeVendor MyDevice
That tells you which bus to capture on. In Wireshark, select usbmon3 for bus 003, then filter by device address:
usb.device_address == 7
On Windows, the equivalent tool is USBPcap, a kernel-mode driver that integrates with Wireshark as a capture interface. The workflow is the same once capture is running; the URB format that Wireshark presents is identical.
Reading the Capture
The URB columns in Wireshark are dense at first. The most important fields are transfer type, endpoint, direction, and the data payload. A useful starting filter for interrupt devices:
usb.transfer_type == 0x01
For control transfers (common during initialization and configuration):
usb.transfer_type == 0x02
Control transfers have a structured setup phase. The eight-byte setup packet contains bmRequestType, bRequest, wValue, wIndex, and wLength. Wireshark dissects these for you. Vendor-specific requests (where the high bit of bmRequestType is set and the type bits indicate vendor) are the ones to pay close attention to; they are the device’s private command vocabulary.
For a complete picture of the device before diving into traffic, run:
lsusb -v -d 1234:5678
The verbose output shows every descriptor the device exports: interface classes, endpoint addresses, polling intervals, and maximum packet sizes. The bInterfaceClass value tells you whether the device speaks a standard class protocol (HID, CDC, Mass Storage) or is fully vendor-defined. If it is 0xFF (vendor-specific), the entire protocol is undocumented and you will need to reconstruct it entirely from traffic.
The Reverse Engineering Loop
The workflow settles into a cycle: trigger a single action in the vendor software, stop capture, examine the resulting URBs, annotate what you observed, repeat. The goal is to map actions to byte sequences.
With interrupt endpoints, each packet tends to be a fixed size, and the structure is often static: the first byte is a report ID or command type, subsequent bytes are parameters or state. Control transfers are more varied; the vendor command in bRequest plus wValue and wIndex often form a command tuple, with the data phase carrying arguments.
Color-coding rules in Wireshark help here. Mark packets containing recognizable state changes (device on, device off, a specific mode change) so you can visually separate the initialization sequence from steady-state operation. The initialization sequence is usually the most complex part; the runtime protocol tends to be simpler once the device is configured.
Pay attention to the direction. Host-to-device packets are commands; device-to-host packets are responses or unsolicited reports. For devices that send periodic state updates (sensors, input devices), the reports often contain more information than you initially need. Strip away bytes that do not change across multiple captures under different conditions; focus on the bytes that vary in ways that correlate with observable behavior.
From Capture to Code
Once the protocol is understood well enough, libusb provides the lowest-friction path to a working implementation in C:
#include <libusb-1.0/libusb.h>
libusb_context *ctx;
libusb_init(&ctx);
libusb_device_handle *handle =
libusb_open_device_with_vid_pid(ctx, 0x1234, 0x5678);
if (libusb_kernel_driver_active(handle, 0))
libusb_detach_kernel_driver(handle, 0);
libusb_claim_interface(handle, 0);
unsigned char data[64];
int transferred;
libusb_interrupt_transfer(handle, 0x81, data, sizeof(data),
&transferred, 1000);
For Python, pyusb wraps libusb with a cleaner interface:
import usb.core
import usb.util
dev = usb.core.find(idVendor=0x1234, idProduct=0x5678)
if dev is None:
raise ValueError('Device not found')
dev.set_configuration()
# Send a vendor control request observed during capture
dev.ctrl_transfer(
bmRequestType=0x40, # vendor, host-to-device, device recipient
bRequest=0x01,
wValue=0x0000,
wIndex=0x0000,
data_or_wLength=bytes([0x01, 0x02])
)
# Read interrupt response
data = dev.read(0x81, 64, timeout=1000)
print(list(data))
The ctrl_transfer call maps directly to what you observed in the Wireshark capture. The bmRequestType, bRequest, wValue, and wIndex values come straight from the setup packet dissection.
Where This Gets Complicated
Some devices use HID class but define their own report format on top. Wireshark’s HID dissector will parse the report descriptor if you can get it (a GET_DESCRIPTOR control transfer to the HID interface), but vendor-specific usage IDs mean the dissector cannot tell you what the fields mean. You still have to correlate bytes to behavior manually.
Devices that do isochronous transfers, common for audio and video, present additional complexity. Isochronous URBs carry multiple packets per transfer, and the framing is tied to USB frame timing (1ms for full-speed, 125 microseconds for high-speed). Wireshark shows the individual micro-frame data, but reassembling a stream from isochronous captures requires understanding the device’s fragmentation strategy.
On Windows, some devices use WinUSB or vendor-supplied kernel drivers that intercept traffic before usbmon (or USBPcap) can see it cleanly. In those cases, a hardware-level USB analyzer, a dedicated device that sits between host and peripheral, is the more reliable option. Software capture works for most devices; hardware capture is the fallback when software capture produces incomplete or corrupt traces.
The Broader Context
USB sniffing is one of several approaches to protocol reverse engineering. For devices that also communicate over serial or I2C internally, logic analyzers with sigrok can capture at the electrical level. For devices with companion mobile apps, intercepting the app’s USB communication through an Android emulator with USB passthrough is sometimes easier than setting up host-side capture.
The USB approach remains compelling because the tooling is mature, the captures are high-fidelity, and the path from capture to implementation is direct. Wireshark’s dissectors handle the framing layer; you focus entirely on the application layer above it. For anyone writing open-source support for hardware that lacks it, this is still the most reliable starting point.