USB Reverse Engineering in Full: From Wireshark Captures to a Working libusb Driver
Source: lobsters
Wireshark captures USB traffic, and with the right setup and filters, you can reconstruct exactly what passes between a host and an unfamiliar device. This article at crescentro.se demonstrates that workflow hands-on. What most USB RE writeups don’t cover in depth is the second half: how you go from seeing packets to having code that talks to this device reliably. That path runs through descriptor parsing, transfer type identification, HID report decoding, and finally libusb implementation.
Setting Up Capture Infrastructure
On Linux, capture runs through usbmon, a kernel facility introduced in Linux 2.6.11. Loading the module is one command:
sudo modprobe usbmon
Wireshark then surfaces USB buses as usbmon0 (all buses combined), usbmon1, usbmon2, and so on. Running Wireshark as root is one option; the cleaner approach is a udev rule granting the wireshark group access:
SUBSYSTEM=="usbmon", GROUP="wireshark", MODE="640"
One consistent recommendation: start the capture before plugging in the device. The enumeration sequence, where the host negotiates descriptors and assigns the device address, contains the structural map you need for everything else.
On Windows, the capture path runs through USBPcap (version 1.5.4.0 current), a WDF filter driver that intercepts I/O Request Packets flowing between the USB hub driver and function drivers. It is bundled with recent Wireshark installers. USBPcap surfaces as \\.\USBPcap1, \\.\USBPcap2, one per root hub. The packet format differs from Linux: USBPcap uses DLT type 249 vs. DLT_USB_LINUX_MMAPPED (220) on Linux. Wireshark handles both transparently, but the distinction matters if you are writing tooling to parse pcap files programmatically.
What a URB Actually Contains
Every frame Wireshark shows in a USB capture represents a URB (USB Request Block). The fields you will read constantly:
- Transfer type: control, bulk, interrupt, or isochronous.
- Endpoint address: bit 7 encodes direction (1 = IN, device to host), the low nibble is the endpoint number.
- URB type: submit (outgoing request) or complete (response).
- Data payload: the bytes exchanged.
Transfer types determine where different kinds of device data live. Control transfers on endpoint 0 handle enumeration and configuration. Interrupt transfers carry time-sensitive small payloads; for HID devices, this means every button press, axis position, and sensor reading. Bulk transfers carry larger data without timing guarantees, standard for storage and printers. Isochronous transfers handle audio and video streams where dropped packets are tolerable.
For peripheral reverse engineering, the work concentrates on interrupt IN or bulk IN transfers once enumeration is complete.
Reading Descriptors
During enumeration, the host sends GET_DESCRIPTOR control requests to endpoint 0. Filtering for them in Wireshark:
usb.setup.bRequest == 6
The responses give you the device’s complete structural layout. The device descriptor provides VID/PID and USB version. The configuration descriptor tree enumerates every interface and, under each interface, every endpoint with its address, transfer type, max packet size, and polling interval. For HID class devices, there is also a HID descriptor that points to the report descriptor, a separate artifact worth extracting on its own.
To filter specifically for the HID report descriptor request:
usb.setup.wValue == 0x2200
The HID report descriptor is a compressed bytecode that specifies every field in every report the device sends or receives. It maps byte offsets and bit ranges to USB HID usage IDs, defines value ranges, and specifies whether values are absolute or relative. Once decoded, it gives you the complete data layout of the device’s output.
The usbhid-dump tool on Linux extracts report descriptors directly:
usbhid-dump -a 1:5
For decoding raw report descriptor bytes, hidrd-convert -i hex -o spec produces a human-readable breakdown of every item. The USB-IF HID Descriptor Tool is the reference implementation on Windows.
Filtering for Device Data
After enumeration, to watch the device’s actual reports, filter for interrupt IN transfers from a specific device on bus 1, address 3:
usb.bus_id == 1 && usb.device_address == 3 && usb.transfer_type == 0x01 && usb.endpoint_number.direction == 1
Wireshark’s transfer type encoding follows USBPcap conventions: 0 = isochronous, 1 = interrupt, 2 = control, 3 = bulk. The same filter structure applies to bulk IN with usb.transfer_type == 0x03.
tshark makes it straightforward to extract payload bytes for analysis outside Wireshark:
tshark -i usbmon1 -T fields -e usb.capdata \
-Y 'usb.device_address==3 && usb.transfer_type==1 && usb.endpoint_number.direction==1' \
2>/dev/null
The protocol decoding process from here is empirical: trigger one device action at a time, observe which bytes change in isolation, and build a field map. For devices with multiple report IDs, the first byte in each report identifies the report type. Cross-referencing the report descriptor with observed byte changes is faster than trying to decode either one in isolation.
Writing the Implementation with libusb
The standard implementation path from a decoded USB protocol is libusb (version 1.0.27 current), a portable userspace library that communicates with USB devices without requiring a custom kernel driver.
Before claiming an interface, detach any existing kernel driver:
libusb_context *ctx;
libusb_init(&ctx);
libusb_device_handle *h = libusb_open_device_with_vid_pid(ctx, 0x046d, 0xc52b);
if (libusb_kernel_driver_active(h, 0)) {
libusb_detach_kernel_driver(h, 0);
}
libusb_claim_interface(h, 0);
Reading interrupt reports mirrors what you observed in Wireshark:
uint8_t buf[64];
int transferred;
libusb_interrupt_transfer(h, 0x81, buf, sizeof(buf), &transferred, 5000);
/* 0x81 = endpoint 1, IN direction */
Sending a control transfer to configure the device or trigger vendor-specific behavior:
libusb_control_transfer(h,
LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT,
0x09, /* SET_REPORT, HID class request */
0x0200, /* report type output, report ID 0 */
0, /* interface 0 */
payload, payload_len, 5000);
For HID devices where you don’t need full libusb control, hidapi (0.14.0 current) wraps libusb on Linux and uses the native HID API on Windows and macOS:
hid_device *dev = hid_open(0x046d, 0xc52b, NULL);
uint8_t buf[65] = {0};
hid_read_timeout(dev, buf, sizeof(buf), 5000);
On Windows, replacing a vendor driver with something libusb can claim requires Zadig, which installs WinUSB, libusb-win32, or libusbK as the function driver for the selected device. On Linux, a udev rule grants non-root access without any driver replacement:
SUBSYSTEM=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52b", MODE="0666"
Asynchronous Transfers for Production Code
The synchronous libusb_interrupt_transfer and libusb_bulk_transfer calls work fine for exploration and scripting, but production code that reads a device continuously should use the libusb asynchronous API. The pattern: allocate a transfer, fill it, submit it, and handle events in a loop.
struct libusb_transfer *t = libusb_alloc_transfer(0);
libusb_fill_interrupt_transfer(t, h, 0x81, buf, sizeof(buf),
report_callback, NULL, 0);
libusb_submit_transfer(t);
while (running) {
libusb_handle_events(ctx);
}
Inside report_callback, resubmit the transfer to keep reading continuously. This matches how kernel HID drivers work: they submit URBs in a ring, always keeping at least one outstanding so no report is dropped between completions.
Projects Built on This Technique
The full workflow from Wireshark capture to libusb implementation has produced several well-maintained open-source projects. OpenRGB reverse engineered RGB lighting control protocols for peripherals from Corsair, Razer, ASUS, and MSI through USB sniffing, then implemented all of them with libusb. libratbag applied the same approach to gaming mice from Logitech, Roccat, and Steelseries, embedding the protocol documentation in source comments alongside the implementation. OpenTabletDriver did it for drawing tablets from Wacom, Huion, and XP-Pen. All three captured on Windows where vendor drivers exist, decoded the protocol from Wireshark captures, and implemented with libusb or hidapi for cross-platform support.
The crescentro.se article gives a solid demonstration of the observation half of this workflow: how to capture, how to navigate Wireshark’s USB dissection, and how to read raw byte values from a device in context. The implementation half requires understanding descriptor structures well enough to identify endpoints and transfer types, decoding HID report descriptors to map byte positions to semantics, and translating observed packet sequences into libusb calls that replicate what the vendor driver does. The two halves together cover the full path from a black-box USB device to working, portable code.