Inside USB: Reverse-Engineering a Device Protocol from Raw Wireshark Captures
Source: lobsters
Most USB devices ship without public protocol documentation. The vendor provides a Windows driver, maybe a companion app, and that is it. If you want Linux support, embedded integration, or just to understand what your hardware is doing, you need to read the wire yourself. This walkthrough from crescentro.se covers the core workflow, and it is a good starting point. What it leaves room for is a deeper look at the underlying mechanics: why the capture setup works the way it does, what the packet fields actually mean, and how to go from a Wireshark capture to code that talks to the device.
The Capture Layer: usbmon and USBPcap
Wireshark does not capture USB traffic the way it captures network traffic. There is no promiscuous mode to flip. Instead, you depend on a kernel facility to intercept USB Request Blocks (URBs) as they pass between the host controller driver and the device.
On Linux, that facility is usbmon, built into the kernel when CONFIG_USB_MON is enabled. It exposes per-bus character devices at /dev/usbmonN, where N matches the bus number shown by lsusb. To find which bus your target device is on:
lsusb
# Bus 002 Device 007: ID 1234:5678 MyDevice Corp MyDevice
Then open /dev/usbmon2 in Wireshark, or run:
sudo tcpdump -i usbmon2 -w capture.pcap
/dev/usbmon0 captures all buses aggregated, which is convenient but noisier.
On Windows, the equivalent is USBPcap, a kernel-mode filter driver that attaches above usbhub.sys. It ships bundled with Wireshark for Windows. After installation, Wireshark lists capture interfaces named USBPcap1, USBPcap2, and so on, one per root hub. You can pipe it directly:
USBPcapCMD.exe -d \\.\ USBPcap1 -o - | wireshark -k -i -
One practical detail: both usbmon and USBPcap start capturing from the moment they are attached to the bus. If your device was already plugged in, you have missed the enumeration phase. The workaround is to start Wireshark first, then unplug and replug the device. The first thirty or so packets will be descriptor requests, and those are the packets that tell you the most about the device.
What a URB Actually Is
Every USB packet in Wireshark represents one URB. The key thing to understand is that URBs come in pairs: a Submit (S) and a Complete (C). The Submit goes from the host to the device; the Complete comes back. Wireshark links them by the usb.usb_id field. When you see an interrupt endpoint producing data, the Submit is the host polling the endpoint, and the Complete carries the actual payload.
The transfer type field (usb.transfer_type) tells you the traffic class:
0x01Interrupt: periodic, bounded latency, used by mice, keyboards, gamepads0x02Control: bidirectional, used for enumeration and class requests0x03Bulk: high throughput, no latency guarantee, used by mass storage and printers0x00Isochronous: guaranteed bandwidth, no retries, used by audio and video
Most consumer devices that are not storage or audio will have one interrupt IN endpoint carrying sensor or state data, plus control transfers for configuration.
The Enumeration Phase
When a USB device is plugged in, the host runs through a fixed sequence of GET_DESCRIPTOR control requests. These requests use bRequest = 0x06 and encode the descriptor type in the high byte of wValue. The sequence looks like this:
GET_DESCRIPTOR: Device Descriptor (wValue = 0x0100)
GET_DESCRIPTOR: Configuration (wValue = 0x0200)
GET_DESCRIPTOR: String Descriptors (wValue = 0x0300, 0x0301, ...)
GET_DESCRIPTOR: HID Report Descriptor (wValue = 0x2200) <- if HID class
In Wireshark, filter for these with:
usb.transfer_type == 0x02 && usb.bRequest == 0x06
The Device Descriptor gives you VID and PID. The Configuration Descriptor, which includes all sub-descriptors in one response, tells you how many interfaces there are, what class each interface belongs to, and what endpoints are configured. The Interface Descriptor field bInterfaceClass is your first major branch point:
0x03(HID): the device follows the HID specification. There will be a Report Descriptor that defines the exact bit-layout of every input packet.0xFF(Vendor-specific): no standard protocol. Everything is proprietary, and you will need to correlate packets with observed device behavior.0x08(Mass Storage): wraps SCSI commands, handled by theusb-storagekernel module.
For HID devices, the Report Descriptor is worth fetching early. You can get it directly:
sudo usbhid-dump -a 2:7 -i 0 | grep -v : | xxd -r -p | hidrd-convert -i natv -o spec
This gives you a human-readable breakdown of every field the device sends, including bit widths, logical ranges, and HID usage codes. The HID Usage Tables document what each usage means. When you see Usage Page (0xFF00) or any vendor-defined range, that interface is carrying proprietary data; this is where the reverse engineering work concentrates.
Reading the Operational Traffic
Once enumeration is complete, filter down to the data-carrying packets:
usb.device_address == 7 && usb.transfer_type == 0x01 && usb.data_len > 0
For a standard mouse, interrupt IN packets carry a fixed-format report every 1 to 8 milliseconds. For a gaming peripheral with vendor extensions, there will be additional control or bulk traffic when you use the companion software to change DPI, update firmware, or configure macros.
The workflow for decoding vendor commands is systematic. Open the companion application on Windows, start a capture, then perform one action at a time: change DPI, change LED color, press a macro button. After each action, look for the OUT packet that appeared. Byte 0 or bytes 0 through 1 of the payload is almost always an opcode. Build a table:
| Opcode | Payload | Observed Effect |
|---|---|---|
0x02 | 01 [R] [G] [B] | Set LED color |
0x04 | [DPI_H] [DPI_L] | Set DPI |
0x10 | empty | Request device status |
This is slower than reading documentation but it is reliable. The companion app cannot do anything the protocol does not support.
From Capture to Code
Once you have a working packet map, implementing a userspace driver with libusb is straightforward. The critical steps are detaching the existing kernel driver, claiming the interface, and submitting transfers:
#include <libusb-1.0/libusb.h>
libusb_context *ctx;
libusb_device_handle *dev;
uint8_t buf[64];
libusb_init(&ctx);
dev = libusb_open_device_with_vid_pid(ctx, 0x1234, 0x5678);
if (libusb_kernel_driver_active(dev, 0))
libusb_detach_kernel_driver(dev, 0);
libusb_claim_interface(dev, 0);
/* Read from interrupt endpoint 0x81 */
int transferred;
libusb_interrupt_transfer(dev, 0x81, buf, sizeof(buf), &transferred, 1000);
For a device that accepts commands, use libusb_control_transfer for control messages or libusb_bulk_transfer for bulk endpoints. The endpoint address and transfer type you observed in Wireshark map directly to these libusb calls.
For devices that enumerate as HID class, a kernel driver using the hid_driver interface is often cleaner. The .raw_event callback fires on every input report, and hid_hw_raw_request lets you send output and feature reports. The digimend project maintains a collection of HID drivers for drawing tablets that were written entirely from USB captures, which is a useful reference for how this process looks at scale.
When Software Capture Is Not Enough
Some devices detect that a filter driver is present and change their behavior. Others complete enumeration before you can attach a capture. For those cases, hardware capture is the answer.
OpenVizsla is an open-source FPGA-based USB 2.0 sniffer. It taps the bus passively, captures at the full 480 Mbit/s high-speed signalling level, and outputs standard pcap readable by Wireshark. The device under test has no way to know it is being observed.
For USB Full-Speed and Low-Speed devices, a logic analyzer running sigrok with its usb_signalling, usb_packet, and usb_request protocol decoders gives you electrical-level visibility. This is useful for devices that use unusual timing or for debugging USB 1.1 compliance issues.
The Bigger Picture
USB reverse engineering is one of those areas where the tooling is genuinely good and the protocol is well-documented, which means the bottleneck is almost always methodical observation rather than tool limitations. Wireshark dissects URBs into human-readable fields, the USB specification is publicly available and thorough, and libusb makes writing a userspace driver straightforward once you know the protocol.
The pattern that emerges from projects like digimend, input-wacom, and various community driver efforts is that a few hours of careful capture work combined with disciplined action-to-packet correlation produces enough information to build a functional driver. The devices are not obscuring much; they are just not explaining themselves.