Sometimes you have a USB device. It is not ordinary one, like pendrive or mouse, but something custom. It goes with its own Windows application. But we want open source Linux program, don’t we? In many such cases, the communication protocol is quite simple, we just need to get to know it. I demonstrate how to listen to USB communication between VirtualBox and your physical USB device. Having this information gathered, we will proceed to write ultra simple user-space application in Python and pyusb library.
For the demonstration I’ll use cheap relay board marked USB-Relay-2, HW-343. It has two 10 A 250 V relays. It is a common module widely available on eEbay or Aliexpress for few dollars. Your device might be different, but the simple ones are usually similar to each other and share basic communication protocol using USB control or HID transfers.
The basic idea of reverse engineering
- Plug the board to Linux host
- Attach USB device to VirtualBox running Windows 10
- Run original Windows application under virtual machine
- Capture USB packets using Wireshark and analyze them
- Write our own controlling application in Python and pyusb
Examining the device
Run dmesg -w
and plug the board in. The relevant entries (kernel 5.4.20):
[ 4560.000392] usb 10-1: new low-speed USB device number 7 using uhci_hcd
[ 4560.449478] usb 10-1: New USB device found, idVendor=16c0, idProduct=05df, bcdDevice= 1.00
[ 4560.449482] usb 10-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[ 4560.449484] usb 10-1: Product: USBRelay2
[ 4560.449486] usb 10-1: Manufacturer: www.dcttech.com
[ 4560.461818] hid-generic 0003:16C0:05DF.0010: hiddev2,hidraw6: USB HID v1.01 Device [www.dcttech.com USBRelay2] on usb-0000:00:1d.3-1/input0
Our relay board has vendor ID 16c0
and product ID 05df
. Lets look at the device more closely:
lsusb -d 16c0:05df -v
This command shows the whole USB device descriptor. The lines most relevant to us:
idVendor 0x16c0 Van Ooijen Technische Informatica
idProduct 0x05df HID device except mice, keyboards, and joysticks
iManufacturer 1 www.dcttech.com
iProduct 2 USBRelay2
Vendor 16c0
is particularly popular among hobby or semi-professional devices. Van Ooijen Technische Informatica made it available for every developer. They just need to differentiate their devices not by vendor ID, but vendor string. The relay board has vendor string: www.dcttech.com
and product string USBRelay2
.
Sometimes the kernel might recognise the device and load the driver. You have to blacklist the driver since you want to talk with the device directly.
echo 'blacklist unwanted-driver' > /etc/modprobe.d/unwanted-driver.conf
Attaching physical USB device to VirtualBox
Under Arch Linux, you have to be the member of vboxusers
group:
gpasswd -a your-user-name vboxusers
I assume you already have configured Windows 10 virtual machine. Guest Additions has to be installed too. If your device uses USB 2.0 or later, you have to install Oracle VM VirtualBox Extension Pack. For USB 1.1, extension pack is not required.
To attach USB, you have to have USB enabled in virtual machine settings. To have the device automatically attached, you can create filter. Open Machine -> Settings -> USB. Add new filter:

After that your device should be on the list if you click on the USB icon on the status bar, as shown in the screenshot:

Run your Windows application and verify that your device works properly.
Capturing packets with Wireshark
Wireshark is great tool for capturing and analyzing network packets. Lesser-known feature is capturing packets transmitted via USB subsystem. To do that under Linux, you have to load usbmon
kernel module first. More information on Wireshark wiki.
modprobe usbmon
Run Wireshark, preferably as root. You should see several usbmon
devices on interface list.

Each one represents one bus. Which one is our device connected to?
lsusb | grep '16c0:05df'
Returns:
Bus 010 Device 002: ID 16c0:05df Van Ooijen Technische Informatica HID device except mice, keyboards, and joysticks
It’s bus 10
, device 2
. Select usbmon10
then. Fiddle a bit with the Windows program. You’re supposed to see some packets. Since you may have several devices on the bus, it’s good idea to filter out packets not belonging to our device. Apply the following filter:
usb.addr contains "10.2"
Writing driver using pyusb
libusb is user-space library that provides generic access to USB devices. pyusb is Python front-end for libusb.
Under Arch Linux, install them:
pacman -S libusb python-pyusb
Lets write some Python code. Remember to detach the device from VirtualBox before running Python code. First import pyusb:
import usb.core
import usb.util
We can now detect our device:
import usb.core
import usb.util
VENDOR_ID = 0x16c0
DEVICE_ID = 0x05df
MANUFACTURER_NAME = "www.dcttech.com"
PRODUCT_NAME = "USBRelay2"
def check_manufacturer_and_product(dev):
"""Look only for devices which match our description strings."""
return dev.manufacturer == MANUFACTURER_NAME and dev.product == PRODUCT_NAME
def find_device_handle():
return usb.core.find(idVendor=VENDOR_ID, idProduct=DEVICE_ID,
custom_match=check_manufacturer_and_product)
dev_handle = find_device_handle()
print(dev_handle)
Function usb.core.find
returns first device handle which matches defined specification. If you set find_all
to True
, it will return the list of all matched devices. I also provided custom callback to filter by manufacturer and product name. If everything is OK, running this code should display the device descriptor.
If you got Access denied
error, your user has no permission to this specific device, you have to create udev rule to allow all users. You can also crate rule to allow specific group. Create and edit file /etc/udev/rules.d/100-usbrelay.rules
:
SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="05df", MODE="0666"
And reload the rules (as root):
udevadm control --reload-rules
Lets fiddle with the Windows application for a bit. We can see control packets in Wireshark when changing the relay state.
What do we want to achieve, anyway? Look at the USB architecture diagram:

Host communicates with the device using virtual data pipes called endpoints. If your device uses them, you should read about them more in USB in a NutShell. It is important to note that any kind of data transfer is initialized by the host. The device cannot initialize data transfer on its own.
Since majority of simple devices (so does the relay board) uses only default control transfer, which is always defined, lets examine it. Control request has following fields:
bmRequestType
bmRequest
wValue
wIndex
- data fragment - optional.
I noticed that when changing relay state, the the application always sends this request:

Let’s emulate it:
dev_handle.ctrl_transfer(0x21, 9, 0x300, 0, (0xff, 0x01, 0, 0, 0, 0, 0, 0))
bmRequestType
determines the type of the request. These are standard and for writing clear code it’s better to use function build_request_type
. You can deconstruct this value, but it is quicker to look at the tests in pyusb. All combinations of valid values are covered there.
requestType = usb.util.build_request_type(usb.util.CTRL_OUT,
usb.util.CTRL_TYPE_CLASS,
usb.util.CTRL_RECIPIENT_INTERFACE)
It is worth mentioning that values 0x21
and 0xa1
are so called HID reports. If your device uses them, it should conform to HID specification and might be better to use more high level library like hid. We will stick to generic libusb.
Running our code results in crash with following error:
usb.core.USBError: [Errno 16] Resource busy
What is going on? Since our device is HID, the kernel attaches it as HID device. But we want to control it ourselves! Lets detach it:
if dev_handle.is_kernel_driver_active(0):
try:
dev_handle.detach_kernel_driver(0)
print("kernel driver detached")
except usb.core.USBError as e:
sys.exit("Could not detach kernel driver: %s" % str(e))
Kernel should detach the device now. Sometimes setting the default configuration is required, it won’t hurt:
dev_handle.set_configuration()
Now we can call our control request. It should execute without any issue.
Discovering the command format
Fiddle with Windows application and look for any changes. I gathered all OUT requests. I noticed they always have the same type, only data fragment varies.
Command: | Bytes: |
---|---|
turn relay 1 on | ff:01:00:00:00:00:00:00 |
turn relay 1 off | fd:01:00:00:00:00:00:00 |
turn relay 2 on | ff:02:00:00:00:00:00:00 |
turn relay 2 off | fd:02:00:00:00:00:00:00 |
Do you notice a pattern here? Least significant bit of first byte determines on/off, second byte is the number of the relay. To test it, I did a stupid blinkenlight with relay 1:
while True:
dev_handle.ctrl_transfer(requestType, 9, 0x0300, 0, (0xfd, 0x01, 0, 0, 0, 0, 0, 0))
time.sleep(1)
dev_handle.ctrl_transfer(requestType, 9, 0x0300, 0, (0xff, 0x01, 0, 0, 0, 0, 0, 0))
time.sleep(1)
Relay 1 should click. The whole example code:
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
import time
import sys
import usb.core
import usb.util
VENDOR_ID = 0x16c0
DEVICE_ID = 0x05df
MANUFACTURER_NAME = "www.dcttech.com"
PRODUCT_NAME = "USBRelay2"
def check_manufacturer_and_product(dev):
return dev.manufacturer == MANUFACTURER_NAME and dev.product == PRODUCT_NAME
def find_device_handle():
return usb.core.find(idVendor=VENDOR_ID, idProduct=DEVICE_ID,
custom_match=check_manufacturer_and_product)
dev_handle = find_device_handle()
if dev_handle.is_kernel_driver_active(0):
try:
dev_handle.detach_kernel_driver(0)
print("kernel driver detached")
except usb.core.USBError as e:
sys.exit("Could not detach kernel driver: %s" % str(e))
requestType = usb.util.build_request_type(usb.util.CTRL_OUT,
usb.util.CTRL_TYPE_CLASS,
usb.util.CTRL_RECIPIENT_INTERFACE)
def set_relay(relay_number, enabled: bool):
b1 = 0xff if enabled else 0xfd
dev_handle.ctrl_transfer(requestType, 9, 0x0300, 0, (b1, relay_number, 0, 0, 0, 0, 0, 0))
while True:
set_relay(1, True)
time.sleep(1)
set_relay(1, False)
time.sleep(1)
set_relay(2, True)
time.sleep(1)
set_relay(2, False)
time.sleep(1)
I hope this extremely simple example will push you in the right direction to reverse engineer your own device. Let me know if you achieve anything!