Sniffing Your Own Hardware: A Deep Dive into USB Protocol Reverse Engineering
Source: lobsters
There is a category of problem that sits at the intersection of systems programming and detective work: you have a USB device, no public protocol documentation, and you need to make it do something its vendor never intended. Whether that is writing a Linux driver for a Windows-only peripheral, automating a piece of lab equipment, or just satisfying curiosity, the starting point is almost always the same. You capture what the official software sends down the wire and work backward from there.
This walkthrough on crescentro.se covers exactly that process using Wireshark. The article itself is a solid practical guide, but it leaves some questions unaddressed: what is Wireshark actually capturing, how does usbmon work at the kernel level, and once you have a decoded protocol, what do you do with it? Those are worth unpacking in detail.
The Capture Layer: usbmon and USBPcap
On Linux, USB packet capture flows through usbmon, a kernel module that hooks into the USB core subsystem. When you load it with sudo modprobe usbmon, it exposes either character devices under /dev/usbmon0, /dev/usbmon1, etc., or entries under /sys/kernel/debug/usb/usbmon/ depending on your kernel configuration. Each numbered interface corresponds to a USB bus. The device at bus 1 shows up on usbmon1.
The data structure at the core of all this is the usbmon_packet, which the kernel defines in linux/usbdevice_fs.h. It records the URB id (so you can match a submit to its completion), the transfer type, endpoint number, device address on the bus, timestamps, and the actual payload bytes. Wireshark reads this in its raw binary form and reassembles it into something human-readable.
The workflow on Linux is:
# Find your device's bus and address
lsusb
# Bus 001 Device 007: ID 1234:5678 Some Vendor Some Device
# Load the capture module
sudo modprobe usbmon
# Capture on the right bus (bus 001 = usbmon1)
sudo wireshark -k -i usbmon1
On Windows, the equivalent is USBPcap, which installs as a Windows Filter Platform (WFP) driver and surfaces captured traffic to Wireshark through a named pipe. The experience is rougher than Linux because driver signing requirements and Windows security policy can complicate installation, especially on modern Windows 11 systems with Secure Boot. On macOS, the situation is worse still. Apple provides no first-party capture interface, and third-party options like XAirTracer or the community-maintained mac-usb-sniffer are inconsistently maintained.
If you are serious about cross-platform USB reverse engineering, a Linux VM with USB passthrough via QEMU or VirtualBox often gives you the best capture environment regardless of your primary OS.
What You Are Actually Looking At
Once Wireshark is running, USB traffic comes in as URBs (USB Request Blocks). The transfer type field tells you what kind of transaction you are looking at:
- Control transfers handle enumeration and configuration. Every USB device responds to a standard set of control requests when it first connects: GET_DESCRIPTOR, SET_CONFIGURATION, and so on. Wireshark dissects these automatically. Reading the descriptor data often tells you the device class (HID, CDC, mass storage, or vendor-specific) before you have captured any application traffic.
- Interrupt transfers carry low-latency, small-packet data. Keyboards, mice, and game controllers use these. The host polls the device at a fixed interval; the device returns data or a NAK if there is nothing to report.
- Bulk transfers are for larger payloads where latency is less critical: printers, USB storage, many custom devices.
- Isochronous transfers handle audio and video where timing guarantees matter more than error correction.
Most custom vendor devices end up using bulk transfers with a vendor-specific protocol layered on top. That is the interesting case, because there is no standard dissector to decode it.
Reading the Pattern
The practical work of reverse engineering happens in the payload bytes of those bulk transfers. A common structure for command-response protocols looks like this: the host sends a fixed-length or length-prefixed packet where the first byte (or two bytes) is a command identifier, followed by parameters, sometimes followed by a checksum. The device responds with a status byte and any requested data.
The systematic approach is to trigger discrete actions in the vendor’s software and look at what changes between captures. Press one button; record the traffic. Press a different button; compare. The differing bytes are your command parameters. The static prefix bytes are your command opcode and packet framing.
Wireshark’s Follow feature for USB streams and the Export Packet Dissections option for piping into Python or a spreadsheet are useful here. The display filter usb.addr == "1.7.1" (bus.device.endpoint) lets you isolate a single device’s traffic. You can also filter by usb.transfer_type == 0x03 for bulk transfers specifically.
From Capture to Code
Once you have a working protocol map, the standard path to implementation is libusb or its Python wrapper, pyusb. These let you open a USB device, claim its interface, and send raw transfers without writing a kernel driver.
import usb.core
import usb.util
# Open device by vendor and product ID
dev = usb.core.find(idVendor=0x1234, idProduct=0x5678)
if dev is None:
raise ValueError('Device not found')
# Detach any kernel driver that claimed the interface
if dev.is_kernel_driver_active(0):
dev.detach_kernel_driver(0)
dev.set_configuration()
# Send a command via bulk OUT endpoint (0x01)
dev.write(0x01, bytes([0x42, 0x00, 0x00, 0x00]))
# Read response from bulk IN endpoint (0x81)
response = dev.read(0x81, 64, timeout=1000)
print(list(response))
Endpoint addresses follow a convention: IN endpoints (device to host) have bit 7 set, so endpoint 1 IN is 0x81, endpoint 2 IN is 0x82, and so on. OUT endpoints are 0x01, 0x02, etc. You find the exact endpoint addresses in the descriptor data Wireshark shows during the enumeration phase.
For HID devices specifically, the protocol is often self-describing via the HID Report Descriptor, which Wireshark can decode. The USB HID usage tables specification published by the USB Implementers Forum maps the descriptor fields to human-readable meanings. Many devices that vendors label as “vendor-specific” are actually HID class devices using the generic vendor usage page, which means you get the descriptor parsing for free.
Where Software Capture Falls Short
usbmon and USBPcap both capture from the host side, which means you see what the host controller sends and receives. You do not see low-level bus events: SETUP handshake packets, NAKs, low-speed preamble tokens, or the line-level signaling that a hardware analyzer would show.
For most reverse engineering tasks this does not matter. But if you are debugging a device that fails enumeration, has timing-sensitive transfers, or exhibits behavior that differs between hardware revisions, you may need a dedicated USB analyzer. The Total Phase Beagle USB and the open-source OpenVizsla board both sit on the USB bus physically and capture everything at the protocol level, including the low-level handshakes that the host controller handles in silicon.
A cheaper middle ground is the Facedancer board from Great Scott Gadgets, which lets you emulate USB devices in Python. It is particularly useful for differential testing: if you have partially decoded a protocol, you can use Facedancer to emulate the device and feed a real host driver responses you construct, confirming your protocol understanding without needing the original hardware in the loop.
The Bigger Picture
USB reverse engineering used to require expensive dedicated hardware or deep knowledge of kernel driver development. The combination of usbmon, Wireshark’s USB dissector, and pyusb has made the software half of this accessible to any developer comfortable with a packet capture tool and Python. The skill gap is in pattern recognition: knowing what to look for in a stream of hex bytes, understanding how checksum schemes tend to be constructed, and having enough patience to trigger one action at a time in a GUI and compare the resulting traffic.
The crescentro.se article is a concrete worked example of that process. What makes it worth following is not the specific device, but the methodology: start from descriptors, isolate by device address, trigger discrete actions, compare captures, build a command table. That sequence works whether you are dealing with a discount drawing tablet, a custom industrial sensor, or a proprietary gaming peripheral whose vendor has stopped releasing macOS drivers.
The tooling exists. The protocol is rarely as obscure as it first appears. Most vendor protocols are command-response over bulk transfers with a one-byte opcode. The hard part is sitting down with Wireshark and doing the work.