---
title: "eBPF로 USB 전송을 실시간으로 훔쳐보는 시스템 전체 스니퍼"
published: 2026-05-31T03:41:11.000Z
canonical: https://jeff.news/article/3405
---
# eBPF로 USB 전송을 실시간으로 훔쳐보는 시스템 전체 스니퍼

usbsnoop은 리눅스 시스템 전체의 USB 트래픽을 실시간으로 보여주는 eBPF 기반 도구다. 컨트롤러별 트레이스포인트나 usbmon 없이, 모든 호스트 컨트롤러 드라이버가 지나가는 URB 제출과 완료 지점을 잡아 전송 내용, 지연시간, 에러, SCSI 명령까지 확인할 수 있다.

- usbsnoop은 리눅스 시스템 전체 USB 트래픽을 실시간으로 보여주는 eBPF 도구임
  - xHCI, EHCI, OHCI, dwc 같은 호스트 컨트롤러 종류와 상관없이 동작하는 구조
  - 컨트롤러별 트레이스포인트도, usbmon 설정도 필요 없다고 함
  - CO-RE 기반이라 커널 버전 차이를 어느 정도 흡수하는 쪽으로 설계됨

- 핵심 아이디어는 USB 전송이 반드시 지나가는 두 지점을 잡는 것
  - `usb_submit_urb`는 전송이 큐에 들어간 시점, 즉 호스트가 장치에 뭔가 보내거나 받을 준비를 한 순간
  - `usb_hcd_giveback_urb`는 전송이 완료된 시점, 즉 상태 코드와 실제 이동한 바이트 수가 나온 순간
  - URB 포인터를 키로 LRU 해시에 시작 시간을 저장했다가 완료 시점에 꺼내서 submit에서 complete까지의 지연시간을 계산함

```mermaid
sequenceDiagram
    participant 앱_드라이버 as 앱/드라이버
    participant USB스택 as 리눅스 USB 스택
    participant eBPF as usbsnoop eBPF
    participant 장치 as USB 장치
    앱_드라이버->>USB스택: URB 제출
    USB스택->>eBPF: usb_submit_urb 후킹
    USB스택->>장치: 전송 실행
    장치-->>USB스택: 응답 또는 에러
    USB스택->>eBPF: usb_hcd_giveback_urb 후킹
    eBPF-->>앱_드라이버: 이벤트, 지연시간, 페이로드 출력
```

- 출력은 “한 줄에 한 이벤트”라서 바쁜 USB 버스에서도 훑어보기 좋게 만든 쪽임
  - 처음 보는 장치는 버스-디바이스 번호, VID:PID, 제품명, 링크 속도를 legend로 찍음
  - 이후 이벤트는 짧은 DEV 태그, 시간, SUBMIT/CMPLT, 전송 타입, 엔드포인트, 방향, 바이트 수, 상태, 지연시간, 커널 드라이버를 보여줌
  - 데이터가 텍스트처럼 보이면 텍스트로, 아니면 hexdump로 렌더링함

> [!TIP]
> `--json`을 쓰면 NDJSON으로 이벤트를 뽑을 수 있어서 jq나 파일 저장으로 실행 간 페이로드 차이를 비교하기 좋음.

- 컨트롤 전송은 8바이트 SETUP 패킷까지 해석함
  - `GET_DESCRIPTOR`, `SET_CONFIGURATION` 같은 표준 요청 이름을 디코딩
  - 장치를 꽂고 enumeration 과정에서 어떤 vendor control request와 HID report가 오가는지 바로 볼 수 있음
  - 하드웨어 USB 스니퍼 없이 주변기기 리버스 엔지니어링을 할 수 있다는 게 꽤 큼

- 대용량 저장장치 쪽도 그냥 바이트 덤프만 하는 게 아님
  - Bulk-Only Transport 래퍼를 읽어서 SCSI 명령으로 보여줌
  - 예를 들면 `READ(10) lba=... blocks=...`, `WRITE(10)`, `CSW PASS/FAIL` 같은 식
  - 펌웨어나 드라이버가 실제로 어떤 블록을 읽고 쓰는지 추적할 때 유용함

- 에러 추적과 성능 분석도 들어가 있음
  - `--errors-only`는 stall, timeout, babble, CRC 에러 같은 실패 완료만 보여줌
  - `--secs`로 시간 제한 실행을 하면 디바이스별 요약과 log2 지연시간 히스토그램을 출력함
  - 필터링은 커널 안에서 처리되므로 걸러진 트래픽은 유저스페이스까지 올라오지 않음

> [!WARNING]
> scatter-gather 페이로드를 완전히 읽으려면 x86-64에서 KASLR로 랜덤화된 `page_offset_base`와 `vmemmap_base` 주소를 넘겨야 함. 안 넘기면 메타데이터는 보이지만 SG 페이로드 바이트는 빠짐.

- 설치와 실행도 개발자 장난감답게 단순하게 밀어붙임
  - GitHub에서 바로 실행하려면 `yeet run github:yeet-src/usbsnoop`
  - 로컬 빌드는 `make`
  - 필요 조건은 clang, bpftool, BTF가 있는 커널
  - 빌드 과정에서 커널 BTF를 `vmlinux.h`로 덤프해 `struct urb`, `usb_device`, 장치 디스크립터를 참조함

---
## 기술 맥락

- 이 도구가 재밌는 이유는 USB 컨트롤러별 구현을 따라가지 않고 공통 병목을 잡았기 때문이에요. 리눅스 USB 스택에서 전송은 결국 URB 제출과 완료라는 형태로 흐르기 때문에, 그 두 함수만 보면 시스템 전체 USB 흐름을 꽤 넓게 관찰할 수 있어요.

- eBPF를 쓴 것도 현실적인 선택이에요. 커널 모듈을 새로 넣거나 usbmon을 설정하는 방식은 운영 환경에서 부담이 크거든요. fentry로 커널 함수 입구를 붙잡으면 필요한 이벤트만 낮은 오버헤드로 가져올 수 있고, 필터도 커널 쪽에서 먼저 적용할 수 있어요.

- URB 포인터를 LRU 해시 키로 쓰는 구조는 요청과 응답을 매칭하려는 선택이에요. 제출 시점에는 시작 시간을 찍고, 완료 시점에는 같은 URB를 찾아 지연시간과 상태를 계산해요. HTTP 요청과 응답을 묶는 것처럼 USB 전송을 한 쌍으로 보는 셈이에요.

- scatter-gather 페이로드 처리가 까다로운 건 데이터가 하나의 연속 버퍼에 없기 때문이에요. 대용량 전송은 여러 페이지에 흩어진 버퍼를 쓰는 경우가 많아서, 페이지를 커널 가상주소로 바꾸려면 아키텍처와 KASLR 주소 정보를 알아야 해요. 그래서 이 기능은 x86-64에서 추가 플래그가 필요해요.

## 핵심 포인트

- usb_submit_urb와 usb_hcd_giveback_urb 두 지점을 fentry로 후킹해 USB 전송의 시작과 완료를 연결함
- URB 포인터를 키로 하는 LRU 해시를 사용해 제출 시각과 완료 시각을 매칭하고 지연시간을 계산함
- 컨트롤 전송의 SETUP 패킷, 대용량 저장장치의 SCSI 명령, 에러, 페이로드 미리보기, NDJSON 출력까지 지원함

## 인사이트

USB 디버깅은 보통 귀찮고 장비 의존적인데, 이 도구는 커널 공통 경로를 잡아서 꽤 우아하게 풀었음. 특히 리버스 엔지니어링, 펌웨어 디버깅, 수상한 HID 입력 추적 같은 데 바로 써먹을 수 있는 냄새가 남.
