USB MITM: Building a Transparent USB Proxy with raw-gadget and libusb
Intercept, inspect, and modify USB traffic in real time
Aristo Chen · COSCUP 2026
github.com/AristoChen/usb-proxy
277
Stars
48
Forks
Apache-2.0
License
2 / 28
📢 Language Notice
This presentation will be delivered in Mandarin Chinese.
If you do not speak Mandarin, you are very welcome to stay after the session. I am happy to walk through the content again in English.
Aristo Chen
3 / 28
Embedded Linux engineer
usb-proxy
Built it to understand the Linux USB stack from the ground up, and it is what this talk is about.
Upstream contributions
U-Boot and the Linux kernel, focused on bootloader enablement and hardware bringup.
Day to day
Embedded Linux and IoT platform work on ARM SoCs.
Questions, issues and pull requests are all welcome.
Agenda
4 / 28
1
The Problem
Why USB is hard to debug, and what the existing tools can and cannot do
2
What is usb-proxy?
A transparent USB man-in-the-middle proxy on commodity hardware
3
Building Blocks
libusb, descriptors, transfer types, USB OTG, gadget modes, raw-gadget
4
Inside the Proxy
Component architecture, how packets flow, and a quick demo of it working
5
Injection Engine + Live Demo
Three ways to modify traffic: patterns, declarative operations, Lua
6
Setup, Limitations, Q&A
Running it yourself, CLI flags, current limits and where it is heading
The Problem
5 / 28
⌨
USB is everywhere
Keyboards, mice, webcams, storage devices, embedded hardware test fixtures: USB touches nearly every device we use.
🔧
How do you actually debug USB?
When a device misbehaves, what are your options for seeing, and changing, what is actually on the wire?
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
✗
You cannot change what the device sends
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
✗
You cannot change what the device sends
✗
You cannot test how the host reacts to something unexpected
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
✗
You cannot change what the device sends
✗
You cannot test how the host reacts to something unexpected
✗
You cannot emulate a device you do not physically own
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
Your USB device and your computer are having a conversation.
You are not part of it.
How Do You Debug USB?
6 / 28
Several approaches exist, each with different trade-offs:
Approach
Cost
Read
Modify
Open Source
usbmon + Wireshark
Linux kernel capture + packet analysis
Free
✓
✗
✓
Hardware Sniffer
DIY ~NT$1,000 / Pro: Beagle, Ellisys NT$13,000+
NT$1k~100k+
✓
✗
◐
usb-proxy
Raspberry Pi + raw-gadget + libusb
~NT$1,500+
✓
✓
✓
◐ = DIY builds are open source; commercial analyzers are proprietary
Only usb-proxy lets you modify traffic in real time, using a general-purpose Linux machine you may already own.
What is usb-proxy?
7 / 28
USB
Device
Real hardware
usb-proxy
Linux + raw-gadget
+ libusb
USB
Host
Sees a synthetic
device
libusb
raw-gadget
Transparent MITM Proxy
▸ Runs on a Raspberry Pi 4 (or any Linux machine with a USB OTG port)
▸ Use cases: protocol reverse engineering, HID remapping, hardware testing, and USB security research
What is libusb?
8 / 28
The library usb-proxy uses to talk to the real USB device.
Real USB Device
the hardware
libusb
userspace library
usb-proxy
your code
libusb handles the USB protocol details so usb-proxy does not have to.
What it is
▸ An open-source C library (LGPL-2.1) for accessing USB devices from userspace
▸ No custom kernel driver needed; works through the kernel usbfs interface
▸ Cross-platform and widely used: lsusb, OpenOCD, Wireshark all rely on it
What usb-proxy uses it for
▸ Reading the real device’s descriptors so the proxy can clone them
▸ Forwarding every control, interrupt, bulk, and isochronous transfer
▸ Returning the device’s replies so they can be passed back to the host
What is a USB Descriptor?
9 / 28
When a device is plugged in, it tells the host what it is. That self-description is a set of descriptors.
Descriptor Hierarchy
Device
Vendor ID, Product ID, USB version
Configuration
Power needs, number of interfaces
Interface
Device class (HID, storage, audio…)
Endpoint
Address, direction, transfer type
Why it matters here
▸ usb-proxy reads the real device’s descriptors via libusb, then replays them to the host through raw-gadget
▸ The host reads those descriptors and believes it is talking to the original device
▸ Endpoint descriptors tell the proxy which endpoints exist, and injection rules target them by ep_address
▸ Some UDCs cannot support every endpoint address, which is why --auto_remap_endpoints rewrites them
USB Transfer Types
10 / 28
Every endpoint declares one of four transfer types. usb-proxy forwards all four.
Control
Endpoint 0, always present
▸ Descriptor requests, configuration, class commands
▸ Error-checked with guaranteed delivery
▸ Injection matches on setup packet fields
Interrupt
Small and periodic, bounded latency
▸ HID devices: mouse, keyboard, game controllers
▸ Host polls at a fixed interval
▸ Injection matches on ep_address
Bulk
Large payloads, no timing guarantee
▸ Mass storage, printers, network adapters
▸ Error-checked and retried; uses spare bandwidth
▸ Injection matches on ep_address
Isochronous
Continuous stream, reserved bandwidth
▸ Webcam video and USB audio
▸ No retries; late data is simply dropped
▸ Injection matches on ep_address
The endpoint descriptor is what declares the type, so the proxy knows how to handle each endpoint.
What is USB OTG?
11 / 28
Normal USB
Device
(Mouse, Keyboard…)
Host
(Computer)
Fixed roles: Host always controls, Device always responds
USB OTG (On-The-Go)
Raspberry Pi 4
(with USB OTG port)
Host
(Computer)
can act as a USB device
▸ An OTG port can dynamically act as both a USB host and a USB device
▸ This is what lets the Raspberry Pi impersonate the real device to the host computer
▸ On the Raspberry Pi 4, the USB-C power port provides OTG capability via the dwc2 kernel driver
USB Gadget Modes
12 / 28
The kernel gadget framework offers several ways to define a USB device:
Mode / Module
Flexibility
Runtime Handled By
g_* pre-built modules
g_serial, g_ether, g_mass_storage, g_zero
Fixed function
Kernel
gadgetfs
answers some requests itself, using your descriptors
Custom logic
Userspace
raw-gadget
passes every request up to userspace
Full packet control
Userspace
Also worth knowing:
configfs and functionfs offer other ways to compose or customise gadgets, each with different trade-offs.
usb-proxy uses raw-gadget: the lowest-level option, giving full packet control from userspace.
What is raw-gadget?
13 / 28
The kernel interface usb-proxy uses to impersonate the device to the host.
usb-proxy
your code
/dev/raw-gadget
kernel interface
USB Host
the computer
Every request from the host is passed up to usb-proxy to answer.
What it is
▸ A kernel module (CONFIG_USB_RAW_GADGET) that lets userspace emulate USB devices
▸ Merged into mainline Linux 5.7; written by Andrey Konovalov for kernel fuzzing
▸ Driven by ioctl calls on /dev/raw-gadget, not a filesystem interface
What usb-proxy uses it for
▸ Replaying the real device’s descriptors so the host sees the original device
▸ Receiving every host request, including ones gadgetfs would answer itself
▸ Reporting UDC endpoint capabilities, which is what --auto_remap_endpoints relies on
Inside the Proxy Machine
14 / 28
Proxy Machine (Raspberry Pi 4 / Linux + OTG)
Device Side
Application
Host Side
libusb
Userspace
usbfs / USB Core
Kernel
← USB Device (real hardware)
usb-proxy
C++ Application
/dev/raw-gadget
Userspace
Gadget Framework
Kernel
UDC Driver
Kernel
USB Host (computer) →
Packet Flow: Setup and Control
15 / 28
1 Startup: before the host is connected
libusb reads the real
device descriptors
converted into
raw-gadget structs
usb_raw_init + run:
gadget goes live
The gadget’s descriptor structures are built once here, before the host sees anything.
2 Runtime: control transfers on endpoint 0
A single ep0_loop handles both directions. No reader/writer split, no queue.
Real USB Device
hardware
ep0_loop
forwards both ways
USB Host
the computer
libusb
raw-gadget
A GET_DESCRIPTOR from the host is forwarded to the real device via libusb, and the reply is passed straight back. The host sees exactly what the real device would have said.
Packet Flow: Data Endpoints
16 / 28
Interrupt, bulk, and isochronous endpoints each get a reader thread and a writer thread, with a queue between them.
IN endpoint: device → host (e.g. mouse reports, webcam frames)
Real USB
Device
hardware
ep_loop_read
via libusb
queue
per endpoint
ep_loop_write
via raw-gadget
USB Host
the computer
OUT endpoint: host → device (e.g. commands, bulk writes)
Real USB
Device
hardware
ep_loop_write
via libusb
queue
per endpoint
ep_loop_read
via raw-gadget
USB Host
the computer
The same chain, traversed in opposite directions. The queue decouples the two sides, so a slow reader or writer never stalls the other.
Quick Demo
17 / 28
Before we change anything, let us see the plain proxy working.
🖱
# no injection, no remapping, just proxy
sudo ./usb-proxy \
--vendor_id=1b3f --product_id=2247
▸ The host enumerates it as the same mouse: same VID / PID, same descriptors
▸ Move it, click it, scroll: everything behaves exactly as before
▸ Every packet is passing through the Pi, and the host cannot tell
That is what transparent means. Now: what happens when we start changing things?
The Injection Rule Engine
18 / 28
injection.json defines rules for interrupt, bulk, and isochronous endpoints by address, plus control-transfer rules that match on setup packet fields and can modify, ignore, or stall a request.
01
Pattern
Replacement
Match fixed hex byte
sequences and substitute
them. Simple and fast.
02
Declarative
Operations
Arithmetic transforms on
byte offsets: negate,
scale, xor, swap, set…
03
Lua
Scripting
Full programmability for
stateful or conditional
logic across packets.
Steps run in order within a single rule. For int, bulk, and isoc, the first rule that modifies a packet wins and the rest are skipped.
Injection is off unless you pass --enable_injection (uses injection.json) or --injection_file PATH.
Where Injection Happens
19 / 28
Injection is off by default. With --enable_injection, rules hook into the paths from earlier.
IN endpoint: device → host
Real USB
Device
hardware
ep_loop_read
via libusb
queue
per endpoint
ep_loop_write
via raw-gadget
USB Host
the computer
◀ injection rules run here
OUT endpoint: host → device
Real USB
Device
hardware
ep_loop_write
via libusb
queue
per endpoint
ep_loop_read
via raw-gadget
USB Host
the computer
◀ injection rules run here
Either way, rules run on the read side, before the packet is queued.
Control transfers (EP0) are different: rules match on setup packet fields, evaluated inside ep0_loop.
--auto_remap_endpoints is a separate optional layer, patching endpoint addresses in descriptor replies on EP0.
Approach 1: Pattern Replacement
20 / 28
Match a fixed hex byte sequence and substitute it. Ideal for simple, deterministic substitutions.
Example: Swap left and right mouse button clicks
"int": [
{
"ep_address": 81,
"enable": true,
"content_pattern": ["\\x01\\x00\\x00\\x00"], // left click
"replacement": "\\x02\\x00\\x00\\x00" // right click
},
{
"ep_address": 81,
"enable": true,
"content_pattern": ["\\x02\\x00\\x00\\x00"], // right click
"replacement": "\\x01\\x00\\x00\\x00" // left click
}
]
Two mutually exclusive rules: once the first matches and modifies the packet, the second is skipped.
Approach 2: Declarative Operations
21 / 28
Apply arithmetic transforms on byte offsets with no scripting required. Operations run in order.
negate
Two's-complement negate
scale
Multiply by float, clamped
add
Add a signed constant, clamped
clamp
Clamp to min/max range
xor
XOR a byte with a mask
swap
Swap two bytes at given offsets
copy
Copy a byte to another offset
set
Force a byte to a fixed value
negate, scale, add, and clamp also accept "size": 2 for int16 little-endian values.
Example: Flip left/right buttons and halve cursor speed in one rule
"operations": [
{ "type": "xor", "offset": 0, "mask": 3 }, // flip button bits
{ "type": "scale", "offset": 1, "factor": 0.5 }, // halve X speed
{ "type": "scale", "offset": 2, "factor": 0.5 } // halve Y speed
]
Approach 3: Lua Scripting
22 / 28
For logic that cannot be expressed declaratively: conditionals, loops, and state across packets.
Key Properties
▸ Each script gets its own Lua state, persisting across packets
▸ Enables dead zones, speed caps, conditional transforms
▸ Combine with pattern replacement and operations in one rule
▸ script_file paths are resolved relative to the proxy’s working directory, e.g. scripts/mouse_invert.lua
▸ Optional; requires liblua5.4-dev or libluajit-5.1-dev at build time
Example: Dead zone + speed cap
local DEAD_ZONE = 2
local MAX_SPEED = 20
local function process(raw)
local v = (raw > 127) and (raw - 256) or raw
if math.abs(v) <= DEAD_ZONE then return 0 end
v = math.max(-MAX_SPEED,
math.min(MAX_SPEED, v))
return v & 0xFF
end
function transform(data, len)
if len < 3 then return data, len end
data[2] = process(data[2]) -- X axis
data[3] = process(data[3]) -- Y axis
return data, len
end
Live Demo
23 / 28
🖱
1
Run usb-proxy on Raspberry Pi 4 between a USB mouse and host computer
2
Intercept raw HID interrupt packets in real time with -v -v verbose output
3
Apply injection rules: swap left/right clicks, invert axes, halve cursor speed
4
Switch to a Lua script for stateful dead zone and speed cap behavior
Fallback: dummy_hcd software-only demo if OTG hardware is unavailable at the venue
Responsible Use
24 / 28
This tool reads and rewrites traffic between a device and its host. That capability cuts both ways.
✓ Built for this
▸ Hardware you own
▸ Devices you are authorised to test
▸ Protocol reverse engineering for interoperability
▸ Hardware QA and regression testing
▸ Security research with permission
✗ Not this
▸ Devices or systems you do not own or have permission to test
▸ Capturing another person’s keystrokes, credentials, or data
▸ Leaving it inline on hardware that is not yours
Unauthorised interception may be unlawful, and the rules vary by jurisdiction.
usb-proxy needs physical access to the USB path, the same position a malicious cable or dock would occupy. Understanding how that works is part of being able to defend against it.
Hardware Setup & dummy_hcd
25 / 28
With OTG Hardware
# Enable dwc2 (older Pi OS: /boot/config.txt)
echo "dtoverlay=dwc2" | sudo tee -a \
/boot/firmware/config.txt
echo "dwc2" | sudo tee -a /etc/modules
sudo reboot
# Load raw-gadget (mainline 5.7+, or build
# it from github.com/xairy/raw-gadget)
sudo modprobe raw_gadget
# Run
sudo ./usb-proxy \
--device=fe980000.usb \
--driver=fe980000.usb \
--vendor_id=1b3f --product_id=2247
Without OTG (dummy_hcd)
# Load raw-gadget (mainline 5.7+, or build
# it from github.com/xairy/raw-gadget)
sudo modprobe raw_gadget
# Load the virtual host/device pair
sudo modprobe dummy_hcd
# Run (dummy_hcd is used by default)
# when no --device / --driver given
sudo ./usb-proxy \
--vendor_id=1b3f \
--product_id=2247
Automatic Endpoint Remapping
Some UDCs cannot support every endpoint address a real device advertises. --auto_remap_endpoints rewrites config and UVC descriptors so the host sees a compatible set, while the proxy maps internally to the original device addresses.
Useful CLI Arguments
26 / 28
--auto_remap_endpoints
Rewrites endpoint addresses in config and UVC descriptors to match UDC capabilities. Essential when the target device uses endpoint numbers the host UDC does not support.
--iso_batch_size N
Number of isochronous packets per transfer (1-32, default 8). Increase for smoother high-bandwidth streams; decrease to reduce latency for audio or low-rate devices.
--injection_file PATH
Path to a custom injection.json rule file. Defaults to injection.json in the current directory.
--pcapng_file PATH
Capture all proxied traffic to a Wireshark-compatible pcapng file. No libpcap needed, zero overhead when unused. Packets are recorded after injection, so the file shows what was actually transmitted.
--inject_socket PATH
Unix domain socket for runtime injection. Send "INJECT <ep_hex> <hex_bytes>" to push a fabricated packet toward the host (IN endpoint) or the device (OUT endpoint).
-v / --verbose
Repeatable, three levels. -v: endpoint table, injection before/after dumps, queue counts. -v -v: full hex dump of every packet. -v -v -v: libusb transfer sizes and isochronous batch details.
Limitations & Future Work
27 / 28
Current Limitations
▸ Isochronous injection works, but changing packet length inside a rule is unsafe for isochronous endpoints, and injection.json currently ships no isoc example rules
▸ Lua scripting adds per-packet overhead; avoid it for high-bandwidth isochronous streams such as webcam video and prefer declarative operations instead
Future Work
▸ Example isochronous injection rules and per-packet drop support
▸ Dynamic rule reloading without restarting the proxy
▸ Web-based packet inspector and rule editor
▸ Community contributions welcome: issues and PRs open on GitHub
28 / 28
Q & A
Thank you for joining the session!
usb-proxy:
github.com/AristoChen/usb-proxy
raw-gadget:
github.com/xairy/raw-gadget
libusb:
github.com/libusb/libusb
Apache-2.0 · PRs and issues welcome
github.com/AristoChen/usb-proxy