yeet-src/usbsnoop

GitHub: yeet-src/usbsnoop

usbsnoop是一款基于eBPF的实时USB流量嗅探工具。

Stars: 68 | Forks: 4

# usbsnoop — 来自两个fentry钩子的实时USB传输嗅探器 ![usbsnoop 演示](https://raw.githubusercontent.com/yeet-src/usbsnoop/master/assets/demo.gif) 系统范围内的实时、彩色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, 内核钩子, 子域名枚举, 实时分析, 性能分析, 控制传输, 数据解码, 端点监控, 系统安全, 系统工具, 系统性能, 系统级监控, 系统调试, 系统运维, 自定义脚本