yeet-src/usbsnoop
GitHub: yeet-src/usbsnoop
usbsnoop是一款基于eBPF的实时USB流量嗅探工具。
Stars: 68 | Forks: 4
# usbsnoop — 来自两个fentry钩子的实时USB传输嗅探器

系统范围内的实时、彩色USB流量 —— 建立在两个
通用URB瓶颈上,每个主机控制器驱动程序都会通过它们,因此
它可以在xHCI/EHCI/OHCI/dwc上工作,没有每个控制器的跟踪点,也没有
`usbmon`。完全CO-RE便携。
| fentry钩子 | 它告诉我们什么 |
| ----------- | ----------------------- |
| `usb_submit_urb` | 一个传输已被排队(设备、端点、类型、有效载荷) |
| `usb_hcd_giveback_urb` | 它已完成(状态、移动的字节数、延迟、有效载荷) |
一个以URB指针为键的`lru_hash`将这两个连接起来:提交戳记
一个开始时间,完成时读取它以获取提交→完成的延迟,然后
删除它。这反映了`httpbody`的请求/响应配对 —— **SUBMIT**是
“请求”(主机发送的内容),**COMPLETE**是“响应”(设备返回的内容)。
控制传输将它们的8字节SETUP数据包解码为标准
请求名称(`GET_DESCRIPTOR`、`SET_CONFIGURATION`等);数据阶段在它们看起来是文本时呈现为文本,否则为十六进制转储。
输出是**每行一个事件**(紧凑)。设备首次出现时,它会得到一个`▸`图例行(`bus-dev`、`vid:pid`、产品、链路速度);之后,每一行只携带短的`DEV`标签,因此左侧列保持对齐和可扫描,即使在大量流量下也是如此。每一行显示时间、类型(SUBMIT/CMPLT)、传输类型、`epNdir`、方向箭头(`←`设备→主机IN,`→`主机→设备OUT)、字节数、状态、延迟和拥有内核驱动程序,然后是一个`·`和最有用的细节(解码SETUP、SCSI命令或简短的有效载荷预览)。使用`--hex`获取完整的多行十六进制转储。十六进制字节根据值类别着色(null蓝色、可打印ASCII青色、空白绿色、其他控制品红色、高/非ASCII黄色)在TTY上;管道输出是纯文本。
## 用例
- **逆向工程外围设备** —— 观察设备实时枚举并交换
供应商控制请求和HID报告,无需硬件嗅探器或`usbmon`
设置。SETUP数据包和有效载荷在您戳击设备时解码。
- **驱动程序/固件调试** —— 看看您的驱动程序或
应用程序向设备发送的确切命令以及返回的内容,每个传输都有提交→完成延迟。
- **大量存储/SCSI检查** —— Bulk-Only Transport包装解码为
SCSI命令(`READ(10) lba=… blocks=…`、`WRITE(10)`、`CSW PASS/FAIL`)。
- **捕获错误** —— `--errors-only`将 stalls (`EPIPE`)、超时、
嘈杂和CRC错误同时显示在所有设备上。
- **检测恶意设备** —— 新插入的设备显示它连接时的行为;BadUSB风格的HID注入表现为`INT`报告或
您未触发的`SET_REPORT`控制写入。
- **离线分析捕获** —— `--json`输出NDJSON;通过`jq`或文件管道到
比较运行之间的有效载荷。
- **性能分类** —— 在定时退出时,您会得到每个设备的汇总和
log2延迟直方图,以找到缓慢或健谈的设备。
## 安装
```
curl -fsSL https://yeet.cx | sh
```
然后直接从GitHub运行它 —— yeet获取示例并为您构建它,无需克隆:
```
yeet run github:yeet-src/usbsnoop
```
## 构建
要从本地签出构建:
```
make
```
将内核的BTF转储到`vmlinux.h`(对于`struct urb`、`usb_device`和设备描述符),然后编译。需要`clang`、`bpftool`和具有BTF的内核。
## 运行
```
yeet run . # all devices, runs until Ctrl-C
yeet run . -- --secs 30 # stop after 30s (prints a summary)
yeet run . -- --vid 0x320f # one vendor
yeet run . -- --vendor-id 0x046d --product-id 0xc52b # one device by id
yeet run . -- --bus 3 --dev 4 # one device by bus address
yeet run . -- --type control,int # only these transfer types
yeet run . -- --no-data # metadata only, skip payload capture
yeet run . -- --max-data 64 # cap rendered payload at 64 bytes
yeet run . -- --errors-only # only failed completions (stalls, timeouts)
yeet run . -- --hex # full multi-line hexdump per transfer
yeet run . -- --json | jq . # NDJSON, one object per event
```
## 标志
| 标志 | 默认值 | 含义 |
| -------------- | ------- | ---------------------------------------------------- |
| `--secs` | 永远 | 运行多长时间;省略则运行到Ctrl-C(数字停止并打印摘要) |
| `--vid`、`--vendor-id` | 任何 | 通过供应商ID过滤(十六进制`0x1d6b`或十进制) |
| `--pid`、`--product-id` | 任何 | 通过产品ID过滤 |
| `--bus` | 任何 | 通过总线号过滤 |
| `--dev` | 任何 | 通过设备地址过滤 |
| `--type` | 所有 | `iso`、`int`、`control`、`bulk`的csv |
| `--no-data` | 关闭 | 不读取传输缓冲区(仅元数据) |
| `--max-data` | `4096` | 每个事件渲染的有效载荷字节数的最大值 |
| `--errors-only`| 关闭 | 仅显示非OK完成(跳过SUBMIT和OK) |
| `--hex` | 关闭 | 每个传输完整的多行十六进制转储(否则为紧凑的行内预览) |
| `--json` | 关闭 | 输出NDJSON(每个事件一个对象)而不是TTY视图 |
| `--page-offset-base` | 关闭 | 内核`page_offset_base`地址(十六进制)—— 启用SG有效载荷捕获(x86-64) |
| `--vmemmap-base` | 关闭 | 内核`vmemmap_base`地址(十六进制)—— 与`--page-offset-base`配对 |
所有过滤都在内核侧发生,因此过滤掉的流量永远不会到达用户空间。
每个事件行以拥有内核驱动程序结束(`[hid_irq_in]`、`[usb_api_blocking_completion]`)—— `urb->complete`通过`bpf_snprintf("%ps")`在内核中符号化,因此不需要`/proc/kallsyms`查找。
大量存储块传输将它们的Bulk-Only Transport包装解码为
SCSI命令(`CBW READ(10) lba=… blocks=…` / `CSW PASS`)。在定时退出
(达到`--secs`)时,打印每个设备的摘要和log2延迟直方图;
Ctrl-C退出跳过它(没有JS可见的信号钩子)。
## 散射/收集有效载荷
块流量(大量存储和类似设备)通常将`struct
scatterlist`数组(`urb->sg`)传递给堆栈,而不是单个线性`transfer_buffer`,因此有效载荷分散在多个页面上。usbsnoop遍历该数组并复制每个段的字节,但到达它们意味着将页面转换为它的内核虚拟地址——这是x86-64的`page_to_virt`的逆过程,它需要运行内核的`page_offset_base`和`vmemmap_base`(两者都是KASLR随机化)。
JS隔离区无法读取`/proc/kallsyms`,加载器没有ksym支持,因此您需要传递两个符号的*地址*,BPF端将其解引用:
```
yeet run . -- \
--page-offset-base 0x$(sudo awk '$3=="page_offset_base"{print $1}' /proc/kallsyms) \
--vmemmap-base 0x$(sudo awk '$3=="vmemmap_base"{print $1}' /proc/kallsyms)
```
没有这些标志,SG传输仍然显示完整的元数据,只是没有有效载荷字节——先前的行为。此路径是**仅限x86-64**:在其他架构上请关闭标志。
## 限制
- 每个传输仅捕获**16384字节**(2的幂——验证器读取夹具依赖于它)。更大的缓冲区被截断;标题仍然报告真实的`实际/请求`长度。每个环形记录携带完整的`data[16384]`,因此8 MiB环形缓冲区可以容纳约512个事件。
- 散射/收集有效载荷需要上面的`--page-offset-base` / `--vmemmap-base`标志
和一个x86-64主机;每个段捕获到一页,并且仅遍历一个传输的前64个段。
- 在usbsnoop附加之前提交的传输没有开始戳记,因此其
完成没有延迟。
- USB描述符是低位字节序,直接读取——在BPF运行的低位字节序主机上是正确的。
标签:Docker镜像, HID传输, SCSI传输, USB协议分析, USB流量监控, WSL, 内核钩子, 子域名枚举, 实时分析, 性能分析, 控制传输, 数据解码, 端点监控, 系统安全, 系统工具, 系统性能, 系统级监控, 系统调试, 系统运维, 自定义脚本