Skip to content

Commit d84dfa6

Browse files
authored
Merge pull request #161 from akvadrako/hid-logger-filter
USBHID Logger Filter
2 parents e722e2e + 07e08b7 commit d84dfa6

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

facedancer/filters/hid.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#
2+
# USBProxy HID logging
3+
#
4+
5+
from warnings import filterwarnings
6+
import hid_parser
7+
from hid_parser import HIDComplianceWarning
8+
from enum import IntEnum
9+
10+
from facedancer.descriptor import USBDescriptorTypeNumber
11+
from facedancer.device import USBBaseDevice
12+
from facedancer.filters.base import USBProxyFilter
13+
from facedancer.request import USBControlRequest
14+
from facedancer.types import (
15+
USBDirection,
16+
USBRequestRecipient,
17+
USBRequestType,
18+
USBStandardRequests,
19+
)
20+
21+
from ..logging import log
22+
23+
GET_REPORT = 0x01
24+
SET_REPORT = 0x09
25+
26+
SET_IDLE = 0x0A
27+
28+
filterwarnings("ignore", r"Usage.* has no compatible usage types", HIDComplianceWarning)
29+
filterwarnings("ignore", r"Expecting 60 usages but got 1", HIDComplianceWarning)
30+
31+
32+
class HIDReportType(IntEnum):
33+
HID_TYPE_INPUT = 1
34+
HID_TYPE_OUTPUT = 2
35+
HID_TYPE_FEATURE = 3
36+
37+
38+
class USBProxyHIDFilter(USBProxyFilter):
39+
"""
40+
Print HID packets
41+
42+
If verbose > 2 - print all fields
43+
"""
44+
45+
def __init__(self, device: USBBaseDevice, verbose=1):
46+
self.device = device
47+
self.verbose = verbose
48+
self.rdescs = {}
49+
50+
def filter_control_in(self, req: USBControlRequest | None, data, stalled):
51+
if req:
52+
if req.type == USBRequestType.STANDARD and USBRequestRecipient.INTERFACE:
53+
if req.number == USBStandardRequests.GET_DESCRIPTOR:
54+
self._log_desc(req, data)
55+
56+
if req.type == USBRequestType.CLASS and USBRequestRecipient.INTERFACE:
57+
self._log_in(req, data)
58+
59+
return req, data, stalled
60+
61+
def _log_desc(self, req, data):
62+
kind = req.value_high
63+
iface = req.index
64+
# index = req.value_low
65+
66+
if kind == USBDescriptorTypeNumber.HID:
67+
log.info(f"GET_DESC HID_DEVICE I{iface} {dump(data)}")
68+
69+
if kind == USBDescriptorTypeNumber.REPORT:
70+
log.info(f"GET_DESC HID_REPORT I{iface} {dump(data)}")
71+
try:
72+
self.rdescs[iface] = rdesc = hid_parser.ReportDescriptor(data)
73+
except NotImplementedError as e:
74+
log.warning(f"Failed to parse report: {e}")
75+
self.rdescs[iface] = None
76+
return
77+
78+
if self.verbose > 2:
79+
for rid in rdesc.output_report_ids:
80+
log.info(f" output {rid} {rdesc.get_output_report_size(rid)}")
81+
82+
for rid in rdesc.input_report_ids:
83+
log.info(f" input {rid} {rdesc.get_input_report_size(rid)}")
84+
85+
for rid in rdesc.feature_report_ids:
86+
log.info(f" feature {rid} {rdesc.get_feature_report_size(rid)}")
87+
88+
def _log_in(self, req, data):
89+
iface = req.index
90+
91+
if req.number == GET_REPORT:
92+
kind = HIDReportType(req.value_high)
93+
log.info(f"GET_REPORT {kind} RID {req.value_low} I{iface} {dump(data)}")
94+
95+
self._report(iface, "parse_input_report", data)
96+
97+
def filter_control_out(self, req, data):
98+
if req and req.type == USBRequestType.CLASS and USBRequestRecipient.INTERFACE:
99+
self._log_out(req, data)
100+
101+
return req, data
102+
103+
def _log_out(self, req, data):
104+
iface = req.index
105+
106+
if req.number == SET_REPORT:
107+
kind = HIDReportType(req.value_high)
108+
log.info(f"SET_REPORT {kind} RID {req.value_low} I{iface} {dump(data)}")
109+
self._report(iface, "parse_output_report", data)
110+
111+
if req.number == SET_IDLE:
112+
dur = req.value_high * 4
113+
log.info(f"SET_IDLE {dur}ms RID {req.value_low} I{iface} {dump(data)}")
114+
115+
def filter_in(self, ep_num, data):
116+
if interface := self._find_interface(ep_num):
117+
self._log_ep_in(interface.number, ep_num, data)
118+
119+
return ep_num, data
120+
121+
def _find_interface(self, ep_num):
122+
"""Return the interface that has ep_num."""
123+
if not self.device.configuration:
124+
return
125+
126+
for interface in self.device.configuration.active_interfaces.values():
127+
if interface.has_endpoint(ep_num, USBDirection.IN):
128+
return interface
129+
130+
def _log_ep_in(self, iface, num, data):
131+
log.info(f"EP{num} I{iface} RID {data[0]} {dump(data[1:])}")
132+
self._report(iface, "parse_input_report", data)
133+
134+
def _report(self, iface: int, kind: str, data: bytes):
135+
if self.verbose < 3:
136+
return
137+
138+
rdesc = self.rdescs.get(iface)
139+
if len(data) > 1 and rdesc:
140+
try:
141+
# TODO - handle feature
142+
for usage, value in getattr(rdesc, kind)(data).items():
143+
log.info(f" {usage} {value}")
144+
except Exception as e:
145+
log.warning(f" {e}")
146+
147+
148+
def dump(raw: bytes):
149+
return raw.hex(" ", -2)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"pyserial",
3434
"prompt-toolkit",
3535
"libusb1",
36+
"hid-parser>=0.1.0",
3637
]
3738

3839
dynamic = ["version"]

0 commit comments

Comments
 (0)