Capturing the Protocol: USB Reverse Engineering from Wireshark to Working Code
Source: lobsters
A recent post on Lobsters walked through using Wireshark to capture and interpret USB traffic from an unknown device. The walkthrough is practical and grounded, but it leaves a useful question unaddressed: what is Wireshark actually capturing, and where does the work go after you understand the protocol? That gap is worth filling, because the path from pcap file to working implementation has several non-obvious steps.
What usbmon and USBPcap Are Actually Doing
On Linux, Wireshark captures USB traffic through usbmon, a kernel facility included since Linux 2.6.11. It is not a network-style promiscuous capture. Instead, usbmon intercepts USB Request Blocks (URBs) at the kernel’s USB core layer, between the driver and the host controller driver. The facility exposes data via debugfs:
sudo modprobe usbmon
ls /sys/kernel/debug/usb/usbmon/
# 0s 0u 1s 1u 2s 2u ...
# 'u' = binary pipe, 's' = text interface
# Bus 0 aggregates all buses
Wireshark reads from /sys/kernel/debug/usb/usbmon/1u (or similar) using the binary usbmon_packet format, a 64-byte header followed by up to the captured data length. Each URB generates two events: S (submit) when a driver queues the request, and C (complete) when the host controller finishes it. This means every transaction appears twice in the capture, which can be confusing until you filter on completion events only.
On Windows, USBPcap installs as a lower filter driver beneath usbhub.sys, intercepting IRPs in the driver stack. It exposes a named pipe interface (\\\\.\\ USBPcap1) that Wireshark reads like any other capture source. USBPcap ships bundled with the Wireshark installer since version 1.12; you opt in during setup.
The practical implication: on Linux you capture everything on a bus. On Windows, USBPcap’s filter dialog lets you limit capture to a single device address, which matters when the bus is busy.
Reading URBs: The Packet Structure
When you open a USB capture in Wireshark, the packet detail pane decodes the usbmon_packet header fields. The key fields are:
xfer_type: 0=isochronous, 1=interrupt, 2=control, 3=bulkepnum: endpoint number with direction bitdevnum: device address on the busflag_setup: set toswhen a SETUP packet is presentflag_data:<for IN (device to host),>for OUT (host to device)
For most reverse engineering work, the useful display filters are:
# All traffic from device at address 5 on bus 1
usb.device_address == 5 && usb.bus_id == 1
# Only interrupt transfers (HID devices, gamepads, mice)
usb.transfer_type == 0x01
# Only control transfers (enumeration, configuration)
usb.transfer_type == 0x02
# Vendor-specific control requests (custom commands)
usb.request_type == 0x40 || usb.request_type == 0xC0
# Get raw payload bytes for bulk/interrupt transfers
usb.capdata
The usb.capdata field is where the application protocol lives. Everything else is framing.
The Enumeration Phase: What Descriptors Tell You
Before looking at application data, the enumeration sequence tells you almost everything you need to know about how to talk to the device. When a USB device is plugged in, the host sends a series of GET_DESCRIPTOR control requests (bRequest=0x06). Filter for these with usb.setup.bRequest == 6 and expand the response packets.
The device descriptor gives you idVendor, idProduct, and bDeviceClass. If bDeviceClass is 0x00, the class is defined at the interface level. If it is 0xFF, the device uses a vendor-specific protocol with no standard class driver. Devices with 0xFF are the interesting ones for reverse engineering, because they have no published spec.
The interface descriptor gives you bInterfaceClass. Class 0x03 is HID, class 0x08 is mass storage, class 0x0E is USB Video Class. For class 0x03, there is an additional HID descriptor containing a report descriptor that completely specifies the packet format, which Wireshark will decode if you find the GET_DESCRIPTOR response with bDescriptorType == 0x22.
For a HID device, the report descriptor is a self-describing byte sequence parsed by a state machine. A minimal gamepad descriptor might look like this:
05 01 USAGE_PAGE (Generic Desktop)
09 05 USAGE (Gamepad)
A1 01 COLLECTION (Application)
09 30 USAGE (X)
09 31 USAGE (Y)
15 81 LOGICAL_MINIMUM (-127)
25 7F LOGICAL_MAXIMUM (127)
75 08 REPORT_SIZE (8)
95 02 REPORT_COUNT (2)
81 02 INPUT (Data, Var, Abs)
05 09 USAGE_PAGE (Buttons)
19 01 USAGE_MINIMUM (Button 1)
29 08 USAGE_MAXIMUM (Button 8)
15 00 LOGICAL_MINIMUM (0)
25 01 LOGICAL_MAXIMUM (1)
75 01 REPORT_SIZE (1)
95 08 REPORT_COUNT (8)
81 02 INPUT (Data, Var, Abs)
C0 END_COLLECTION
This tells you: byte 0 is the X axis as a signed 8-bit value, byte 1 is Y, and byte 2 packs eight button bits. No guessing required. If you want to paste raw descriptor bytes somewhere readable, the USB Descriptor Parser at eleccelerator.com takes hex input and outputs a structured view.
Vendor-specific devices do not have this luxury. For those, you capture while operating the device through its official software and correlate packet contents with known actions.
Extracting Raw Payload Data
Once you have a sense of the structure, tshark is faster than the Wireshark GUI for bulk extraction:
tshark -r capture.pcap \
-Y "usb.device_address==5 && usb.transfer_type==0x01" \
-T fields \
-e frame.number \
-e usb.endpoint_address \
-e usb.data_len \
-e usb.capdata \
> packets.tsv
The usb.capdata column gives you hex-encoded payload bytes. From there, a short Python script can parse each row, convert the hex string to bytes, and apply whatever field offsets you have identified. Correlation is usually straightforward: change one thing on the device, look at what changed in the packet; repeat.
For Xbox 360 controllers, this process revealed that despite looking like a gamepad, the device uses class 0xFF with bulk transfers rather than HID interrupt transfers. The IN endpoint delivers 32-byte state packets with axes at bytes 6-13 as int16_t little-endian values. The rumble motors are controlled by 8-byte OUT packets. The xpad kernel driver on GitHub is essentially the documentation this protocol reverse engineering produced.
From Packets to Code
User-space implementation with pyusb is the fastest path from captured understanding to working code:
import usb.core
import usb.util
VENDOR_ID = 0x1234
PRODUCT_ID = 0x5678
ENDPOINT_IN = 0x81
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
assert dev is not None, "Device not found"
if dev.is_kernel_driver_active(0):
dev.detach_kernel_driver(0)
dev.set_configuration()
usb.util.claim_interface(dev, 0)
while True:
try:
data = dev.read(ENDPOINT_IN, 64, timeout=100)
print(' '.join(f'{b:02X}' for b in data))
except usb.core.USBTimeoutError:
pass
pyusb wraps libusb 1.0, which handles the platform differences. On Linux it communicates through the usbfs interface at /dev/bus/usb. On Windows, you first need to replace the default driver with WinUSB using Zadig, which is a one-click operation for non-HID devices. After that, libusb speaks to WinUSB and the same pyusb code works.
For HID devices specifically, hidapi (version 0.14.0) is cleaner: it uses the OS HID layer on Windows and macOS, avoiding the Zadig step entirely, and falls back to either hidraw or libusb on Linux.
If you need kernel integration, a Linux kernel driver uses struct usb_driver and registers via module_usb_driver(). The probe function receives a struct usb_interface pointer, from which you enumerate endpoints, set up URBs, and register with the appropriate subsystem. For input devices, that means input_allocate_device() and reporting events through the input layer so that /dev/input/eventX works correctly with any application that understands evdev.
When Wireshark Is Not Enough
Wireshark captures software-visible traffic. For hardware-level debugging, timing analysis, or intercepting traffic between a device and a host you cannot modify, you need different tools. Facedancer from Great Scott Gadgets lets you emulate arbitrary USB devices from Python, which is useful for testing how a host responds to specific packets. USBProxy uses the Linux USB gadget framework to sit between a device and host, forwarding and optionally modifying packets in real-time. ViewSB provides an open-source analyzer frontend that works with multiple hardware backends.
For USB 3.x SuperSpeed, passive capture gets harder because the signal encoding changed and the timing requirements are tighter. Hardware analyzers start being necessary. The protocol layers above the physical layer are the same, but the capture setup is not.
The Value of the Exercise
Reverse engineering a USB device with Wireshark is not an exotic skill. It is the practical consequence of USB being a documented, structured protocol with standard enumeration. The kernel and Wireshark do most of the heavy lifting: parsing descriptors, labeling transfer types, decoding known class protocols. Your job is identifying which bytes change in response to which actions and then writing code that produces or consumes those bytes correctly.
The same approach applies whether you are writing a driver for a device that only has Windows support, building a custom interface for a hardware peripheral, or understanding why a device behaves differently under Linux than under macOS. The capture infrastructure is stable, the tools are mature, and the protocol is documented. Getting started requires a USB cable, Wireshark, and a few display filters.