There is a category of hardware that ships with no public protocol documentation, a closed-source vendor app, and a reasonable expectation that users will never look too closely. USB devices fall into this category constantly: LED controllers, drawing tablets, game controllers, industrial sensors, custom HID gadgets. The crescentro.se walkthrough on Wireshark USB capture is a clean end-to-end example of how to approach one of these devices using tools most developers already have installed. This post goes further, covering the full capture stack, a systematic strategy for reading what you find, and the cases where Wireshark is not enough.
The Capture Stack
Wireshark does not talk to USB hardware directly. On Linux, it reads from usbmon, a kernel facility that hooks into the USB core at the URB (USB Request Block) level. Every time the kernel submits a URB to a device or receives a completion, usbmon copies the event into a ring buffer. Wireshark reads that buffer through /dev/usbmonN, where N is the bus number, via libpcap using link type DLT_USB_LINUX_MMAPPED (220).
The setup is two commands:
sudo modprobe usbmon
lsusb
# Bus 003 Device 005: ID 0483:5740 STMicroelectronics Virtual ComPort
The lsusb output tells you which bus the device is on. You then open usbmon3 in Wireshark (or tshark -i usbmon3) and you are reading live USB traffic. There is no need to install a kernel module specific to the device or intercept any driver.
On Windows, the equivalent is USBPcap, a WDM filter driver that inserts itself between the USB hub driver and the client driver, intercepting IRPs that carry URBs. It writes pcap-format data to a named pipe (\\.\ USBPcap1) which Wireshark reads as a live interface. The mechanics differ from usbmon, but the output is structurally the same.
The important thing to understand about both approaches is what they see. usbmon and USBPcap operate at the OS abstraction layer. They capture URBs after the host controller driver has already processed the physical transfers. This means they miss hardware-level details: NAK handshakes, retry attempts, SOF (Start of Frame) tokens, bit-stuffing artifacts, and any traffic the OS driver discards before the URB completion event fires. For most RE work this does not matter. For debugging a flaky device or a misbehaving driver, it might.
Enumeration Is the Map
Every USB device connection begins with an enumeration sequence that reveals the device’s full self-description. Capturing this is worth doing before anything else, because the descriptors tell you what to look for in operational traffic.
The sequence goes: USB RESET, GET_DESCRIPTOR (Device, partial), SET_ADDRESS, GET_DESCRIPTOR (Device, full), GET_DESCRIPTOR (Configuration), GET_DESCRIPTOR (String descriptors), SET_CONFIGURATION. For HID class devices, this is followed by a GET_DESCRIPTOR for the HID Report Descriptor.
In Wireshark, filter for just the enumeration:
usb.transfer_type == 0x02
This shows all control transfers, which are the only type used during enumeration. The descriptor hierarchy you reconstruct from this tells you the endpoint addresses, transfer types, maximum packet sizes, and polling intervals for every interface. For a vendor-class device with two bulk endpoints (common for custom protocols), you now know where commands go out and where responses come in.
For HID devices, the Report Descriptor is the most important artifact in the capture. It is a tag-length-value encoded specification that defines the exact layout of every byte in every HID report. A mouse report descriptor might look like:
05 01 Usage Page (Generic Desktop)
09 02 Usage (Mouse)
A1 01 Collection (Application)
05 09 Usage Page (Button)
19 01 Usage Minimum (1)
29 03 Usage Maximum (3)
25 01 Logical Maximum (1)
75 01 Report Size (1 bit)
95 03 Report Count (3)
81 02 Input (Data, Variable, Absolute)
75 05 Report Size (5 bits)
95 01 Report Count (1)
81 03 Input (Constant) -- padding
09 30 Usage (X)
09 31 Usage (Y)
15 81 Logical Minimum (-127)
25 7F Logical Maximum (127)
75 08 Report Size (8 bits)
95 02 Report Count (2)
81 06 Input (Data, Variable, Relative)
C0
This tells you unambiguously: byte 0 is a button bitfield (3 bits + 5 padding), byte 1 is X delta, byte 2 is Y delta. No guessing required. lsusb -v will print the raw descriptor bytes for any connected HID device; the USB HID specification defines the full encoding.
Filtering Operational Traffic
Once enumeration is done, the device settles into its operational pattern. The single most useful filter for RE work is:
usb.urb_type == 0x43 && usb.data_len > 0
URB type 0x43 is the ASCII code for C, meaning URB Complete. This filter shows every completed transfer that carried actual data from the device to the host. You are not looking at submit events (the host polling) or completions with zero-length payloads (the device saying it has nothing). You are looking at the data that matters.
For bulk transfers going from host to device (commands), the complement is:
usb.urb_type == 0x53 && usb.transfer_type == 0x03 && usb.endpoint_address.direction == 0
URB type 0x53 is S for Submit, transfer type 3 is bulk, direction 0 is OUT. These are the command packets you sent.
To extract the raw payload bytes for offline analysis:
tshark -r capture.pcapng \
-Y "usb.urb_type==67 && usb.data_len > 0 && usb.device_address==5" \
-T fields -e frame.time_relative -e usb.capdata \
> payloads.txt
This gives you a timestamped log of every data packet the device sent. Correlate timestamps with actions you took (button press, dial turn, color change) and the byte positions that change become clear.
What Vendor-Class Protocols Look Like
Most interesting RE targets use USB Class 0xFF (vendor-specific), typically over bulk endpoints. These devices speak whatever protocol the firmware engineer invented. The patterns are not random, though.
A command/response protocol over bulk almost always has a recognizable structure:
- A fixed magic header (common values:
0xAA 0xBB,0x55 0xAA,0x00 0xFF) - A command byte or command word
- A length field
- A payload
- Optionally a checksum
The RTL-SDR project is the most famous example of this approach at scale. Osmocom developers captured USB traffic from the proprietary Windows driver for the Realtek RTL2832U chip, systematically correlated vendor-specific bulk commands with tuner behavior, and produced an open driver that became the foundation of the software-defined radio hobby ecosystem. The entire protocol was recovered from USB captures.
For smaller-scale projects, pyusb is valuable for verification once you have a hypothesis. Rather than running the vendor app to generate traffic, you can write a short Python script that sends the specific command you think you understand and observe the response directly:
import usb.core
dev = usb.core.find(idVendor=0x1A86, idProduct=0x55D3)
dev.set_configuration()
# Send the command you decoded
dev.write(0x02, bytes([0x00, 0x03, 0x10, 0xFF, 0x00, 0x00]))
# Read the response
response = dev.read(0x82, 64, timeout=1000)
print(list(response))
This closes the loop between observation and understanding.
When Software Capture Is Not Enough
usbmon sees URBs. A hardware analyzer sees the wire. The difference matters in specific situations: when a device misbehaves during enumeration before the OS driver loads, when you suspect electrical signal integrity issues, when you need to see NAK/STALL handshakes, or when the OS driver is filtering traffic before it reaches the URB layer.
OpenVizsla is an open-source FPGA-based USB 2.0 analyzer that captures at full 480 Mbps high-speed rates. It outputs standard PCAP files readable by Wireshark, so the analysis workflow is identical once you have the capture. The LUNA project from Great Scott Gadgets goes further, providing a full USB multi-tool that can capture, emulate, and inject traffic, all from Python.
Saleae Logic analyzers are the commercial option with strong software support. A Logic Pro 8 or 16 handles USB 2.0 high-speed and includes a USB protocol decoder that produces decoded packet views alongside the raw signals. The cost is significant ($400 to $1,500 depending on model), which puts it out of reach for casual RE, but for production debugging it pays for itself quickly.
For most USB RE projects, these tools are unnecessary. The device is enumerated by the OS, usbmon captures everything above the driver layer, and Wireshark gives you the decoded view. The hardware path is for when the problem lives below the OS abstraction.
The Workflow in Practice
Pulled together, the approach for an unknown USB device is:
- Run
lsusband note the bus, device number, and vendor/product IDs. Look up the vendor ID in the USB ID database. - Start a
usbmoncapture before connecting the device, to catch enumeration. - Plug in the device and let enumeration complete. Stop the capture.
- In Wireshark, filter
usb.transfer_type == 0x02and read the descriptor tree. Note the endpoint addresses, transfer types, and for HID devices, fetch and decode the Report Descriptor. - Start a second capture. Interact with the device deliberately: one action at a time, with pauses between. Stop.
- Filter
usb.urb_type == 0x43 && usb.data_len > 0. Correlate packet timestamps with your actions. - Extract payloads with
tshark, write a decoder script, verify withpyusborlibusb.
This is the same process whether the target is a drawing tablet, a USB RGB controller, an industrial sensor, or a custom HID gadget. The USB protocol is standardized at the transport layer even when the application layer is entirely proprietary, and that standardization is what makes usbmon plus Wireshark so effective as a starting point.