From d436c82ff2b885328da9ecb9f1aa1b3b8b5105f4 Mon Sep 17 00:00:00 2001 From: ktemkin Date: Sun, 10 Sep 2017 00:35:34 -0600 Subject: [PATCH] Add support for detecting NAKs issued to the host, and finish USBProxy. --- facedancer-usbproxy.py | 19 +++--- facedancer/USB.py | 34 ++++++++++ facedancer/USBConfiguration.py | 85 +++++++++++++++++++++++-- facedancer/USBDevice.py | 7 +++ facedancer/USBEndpoint.py | 56 ++++++++++++++++- facedancer/USBInterface.py | 43 +++++++++++-- facedancer/USBProxy.py | 78 +++++++++++++++++++++-- facedancer/backends/GreatDancerApp.py | 64 ++++++++++++++++++- facedancer/filters/logging.py | 38 ++++++++--- facedancer/filters/standard.py | 90 ++++++++++++--------------- 10 files changed, 426 insertions(+), 88 deletions(-) diff --git a/facedancer-usbproxy.py b/facedancer-usbproxy.py index d8007e6..f1081dd 100755 --- a/facedancer-usbproxy.py +++ b/facedancer-usbproxy.py @@ -11,12 +11,13 @@ from facedancer.filters.logging import USBProxyPrettyPrintFilter import argparse - - def vid_pid(x): return int(x, 16) def main(): + + # TODO: Accept arguments that specify a list of filters to apply, + # from the local directory or the filters directory. parser = argparse.ArgumentParser(description="FaceDancer USB Proxy") parser.add_argument('-v', dest='vendorid', metavar='', type=vid_pid, help="Vendor ID of device", @@ -24,19 +25,19 @@ def main(): parser.add_argument('-p', dest='productid', metavar='', type=vid_pid, help="Product ID of device", required=True) - parser.add_argument('-f', dest='fastsetaddr', action='store_true', - help="Use fast set_addr quirk") args = parser.parse_args() quirks = [] - if args.fastsetaddr: - quirks.append('fast_set_addr') - - u = FacedancerUSBApp(verbose=0) + # Create a new USBProxy device. + u = FacedancerUSBApp(verbose=1) d = USBProxyDevice(u, idVendor=args.vendorid, idProduct=args.productid, verbose=2, quirks=quirks) + # Add our standard filters. + # TODO: Make the PrettyPrintFilter switchable? d.add_filter(USBProxyPrettyPrintFilter(verbose=5)) - d.add_filter(USBProxySetupFilters(d, verbose=0)) + d.add_filter(USBProxySetupFilters(d, verbose=2)) + + # TODO: Figure these out from the command line! d.connect() try: diff --git a/facedancer/USB.py b/facedancer/USB.py index 3261520..8c1f35d 100644 --- a/facedancer/USB.py +++ b/facedancer/USB.py @@ -51,3 +51,37 @@ class USB: def interface_class_to_descriptor_type(interface_class): return USB.if_class_to_desc_type.get(interface_class, None) + +class USBDescribable(object): + """ + Abstract base class for objects that can be created from USB descriptors. + """ + + # Override me! + DESCRIPTOR_TYPE_NUMBER = None + + @classmethod + def handles_binary_descriptor(cls, data): + """ + Returns truee iff this class handles the given descriptor. By deafault, + this is based on the class's DESCRIPTOR_TYPE_NUMBER declaration. + """ + return data[1] == cls.DESCRIPTOR_TYPE_NUMBER + + + + @classmethod + def from_binary_descriptor(cls, data): + """ + Attempts to create a USBDescriptor subclass from the given raw + descriptor data. + """ + + for subclass in cls.__subclasses__(): + # If this subclass handles our binary descriptor, use it to parse the given descriptor. + if subclass.handles_binary_descriptor(data): + return subclass.from_binary_descriptor(data) + + return None + + diff --git a/facedancer/USBConfiguration.py b/facedancer/USBConfiguration.py index 1112183..878b9ec 100644 --- a/facedancer/USBConfiguration.py +++ b/facedancer/USBConfiguration.py @@ -2,11 +2,26 @@ # # Contains class definition for USBConfiguration. -class USBConfiguration: - def __init__(self, configuration_index, configuration_string, interfaces, attributes=0xe0, max_power=250): +import struct + +from .USB import USBDescribable +from .USBInterface import USBInterface +from .USBEndpoint import USBEndpoint + +class USBConfiguration(USBDescribable): + + DESCRIPTOR_TYPE_NUMBER = 0x02 + + def __init__(self, configuration_index, configuration_string_or_index, interfaces, attributes=0xe0, max_power=250): self.configuration_index = configuration_index - self.configuration_string = configuration_string - self.configuration_string_index = 0 + + if isinstance(configuration_string_or_index, str): + self.configuration_string = configuration_string_or_index + self.configuration_string_index = 0 + else: + self.configuration_string_index = configuration_string_or_index + self.configuration_stirng = None + self.interfaces = interfaces self.attributes = attributes @@ -17,6 +32,68 @@ def __init__(self, configuration_index, configuration_string, interfaces, attrib for i in self.interfaces: i.set_configuration(self) + + @classmethod + def from_binary_descriptor(cls, data): + """ + Generates a new USBConfiguration object from a configuration descriptor, + handling any attached subordiate descriptors. + + data: The raw bytes for the descriptor to be parsed. + """ + + length = data[0] + + # Unpack the main colleciton of data into the descriptor itself. + descriptor_type, total_length, num_interfaces, index, string_index, \ + attributes, max_power = struct.unpack('".format( + self.configuration_index, len(self.interfaces), self.attributes, max_power_mA) + + def set_device(self, device): self.device = device diff --git a/facedancer/USBDevice.py b/facedancer/USBDevice.py index a24064d..ca0238e 100644 --- a/facedancer/USBDevice.py +++ b/facedancer/USBDevice.py @@ -200,6 +200,13 @@ def handle_buffer_available(self, ep_num): if callable(endpoint.handler): endpoint.handler() + def handle_nak(self, ep_num): + if self.state == USB.state_configured and ep_num in self.endpoints: + endpoint = self.endpoints[ep_num] + if callable(endpoint.nak_callback): + endpoint.nak_callback() + + # standard request handlers ##################################################### diff --git a/facedancer/USBEndpoint.py b/facedancer/USBEndpoint.py index d4eb02f..76694c7 100644 --- a/facedancer/USBEndpoint.py +++ b/facedancer/USBEndpoint.py @@ -2,7 +2,13 @@ # # Contains class definition for USBEndpoint. -class USBEndpoint: +import struct +from .USB import * + +class USBEndpoint(USBDescribable): + + DESCRIPTOR_TYPE_NUMBER = 0x05 + direction_out = 0x00 direction_in = 0x01 @@ -21,7 +27,7 @@ class USBEndpoint: usage_type_implicit_feedback = 0x02 def __init__(self, number, direction, transfer_type, sync_type, - usage_type, max_packet_size, interval, handler): + usage_type, max_packet_size, interval, handler=None, nak_callback=None): self.number = number self.direction = direction @@ -31,6 +37,7 @@ def __init__(self, number, direction, transfer_type, sync_type, self.max_packet_size = max_packet_size self.interval = interval self.handler = handler + self.nak_callback = nak_callback self.interface = None @@ -38,6 +45,43 @@ def __init__(self, number, direction, transfer_type, sync_type, 1 : self.handle_clear_feature_request } + @classmethod + def from_binary_descriptor(cls, data): + """ + Creates an endpoint object from a description of that endpoint. + """ + + # Parse the core descriptor into its components... + address, attributes, max_packet_size, interval = struct.unpack("xxBBHB", data) + + # ... and break down the packed fields. + number = address & 0x7F + direction = address >> 7 + transfer_type = attributes & 0b11 + sync_type = attributes >> 2 & 0b1111 + usage_type = attributes >> 4 & 0b11 + + return cls(number, direction, transfer_type, sync_type, usage_type, + max_packet_size, interval) + + + def set_handler(self, handler): + self.handler = handler + + def __repr__(self): + # TODO: make these nice string representations + transfer_type = self.transfer_type + sync_type = self.sync_type + usage_type = self.usage_type + direction = "IN" if self.direction else "OUT" + + # TODO: handle high/superspeed; don't assume 1ms frames + interval = self.interval + + return "".format( + self.number, direction, transfer_type, sync_type, usage_type, self.max_packet_size, interval + ) + def handle_clear_feature_request(self, req): print("received CLEAR_FEATURE request for endpoint", self.number, "with value", req.value) @@ -69,8 +113,13 @@ def send_packet(self, data): dev = self.interface.configuration.device dev.maxusb_app.send_on_endpoint(self.number, data) + def send(self, data): + # If we're sending something that's exactly divisible by the + # max packet size, we'll have to send a ZLP once the packet is complete. + send_zlp = (len(data) % self.max_packet_size == 0) and (len(data) > 0) + # Send the relevant data one packet at a time, # chunking if we're larger than the max packet size. # This matches the behavior of the MAX3420E. @@ -80,6 +129,9 @@ def send(self, data): self.send_packet(packet) + if self.send_zlp: + self.send_packet([]) + def recv(self): dev = self.interface.configuration.device diff --git a/facedancer/USBInterface.py b/facedancer/USBInterface.py index e020b1e..c03b3dc 100644 --- a/facedancer/USBInterface.py +++ b/facedancer/USBInterface.py @@ -2,14 +2,17 @@ # # Contains class definition for USBInterface. +import struct from .USB import * -class USBInterface: +class USBInterface(USBDescribable): + DESCRIPTOR_TYPE_NUMBER = 0x4 + name = "generic USB interface" def __init__(self, interface_number, interface_alternate, interface_class, interface_subclass, interface_protocol, interface_string_index, - verbose=0, endpoints=[], descriptors={}): + verbose=0, endpoints=None, descriptors=None): self.number = interface_number self.alternate = interface_alternate @@ -18,8 +21,8 @@ def __init__(self, interface_number, interface_alternate, interface_class, self.protocol = interface_protocol self.string_index = interface_string_index - self.endpoints = endpoints - self.descriptors = descriptors + self.endpoints = [] + self.descriptors = descriptors if descriptors else {} self.verbose = verbose @@ -32,12 +35,40 @@ def __init__(self, interface_number, interface_alternate, interface_class, self.configuration = None - for e in self.endpoints: - e.set_interface(self) + if endpoints: + for endpoint in endpoints: + self.add_endpoint(endpoint) self.device_class = None self.device_vendor = None + @classmethod + def from_binary_descriptor(cls, data): + """ + Generates an interface object from a descriptor. + """ + interface_number, alternate_setting, num_endpoints, interface_class, \ + interface_subclass, interface_protocol, interface_string_index \ + = struct.unpack("xxBBBBBBB", data) + return cls(interface_number, alternate_setting, interface_class, + interface_subclass, interface_protocol, interface_string_index) + + + def __repr__(self): + endpoints = [endpoint.number for endpoint in self.endpoints] + + return "".format( + self.number, self.alternate, self.iclass, self.subclass, self.protocol, self.string_index, endpoints + ) + + + def add_endpoint(self, endpoint): + """ + Adds + """ + self.endpoints.append(endpoint) + endpoint.set_interface(self) + def set_configuration(self, config): self.configuration = config diff --git a/facedancer/USBProxy.py b/facedancer/USBProxy.py index a9b8cde..b1743bb 100644 --- a/facedancer/USBProxy.py +++ b/facedancer/USBProxy.py @@ -54,6 +54,9 @@ class USBProxyDevice(USBDevice): filter_list = [] def __init__(self, maxusb_app, idVendor, idProduct, verbose=0, quirks=[]): + """ + Sets up a new USBProxy instance. + """ # Open a connection to the proxied device... self.libusb_device = usb.core.find(idVendor=idVendor, idProduct=idProduct) @@ -61,6 +64,7 @@ def __init__(self, maxusb_app, idVendor, idProduct, verbose=0, quirks=[]): raise DeviceNotFoundError("Could not find device to proxy!") # TODO: detach the right kernel driver every time + # TODO: do this on configuration so we detach the same interfaces try: self.libusb_device.detach_kernel_driver(0) except: @@ -72,6 +76,11 @@ def __init__(self, maxusb_app, idVendor, idProduct, verbose=0, quirks=[]): def connect(self): + """ + Initialize this device. We perform a reduced initilaization, as we really + only want to proxy data. + """ + max_ep0_packet_size = self.libusb_device.bMaxPacketSize0 self.maxusb_app.connect(self, max_ep0_packet_size) @@ -79,16 +88,37 @@ def connect(self): self.state = USB.state_powered + def configured(self, configuration): + """ + Callback that handles when the target device becomes configured. + If you're using the standard filters, this will be called automatically; + if not, you'll have to call it once you know the device has been configured. + + configuration: The configuration to be applied. + """ + + # Gather the configuration's endpoints for easy access, later... + self.endpoints = {} + for interface in configuration.interfaces: + for endpoint in interface.endpoints: + self.endpoints[endpoint.number] = endpoint + + # ... and pass our configuration on to the core device. + self.maxusb_app.configured(configuration) + configuration.set_device(self) + + def add_filter(self, filter_object, head=False): + """ + Adds a filter to the USBProxy filter stack. + """ if head: self.filter_list.insert(0, filter_object) else: self.filter_list.append(filter_object) - def handle_request(self, req): - self._proxy_request(req) - def _proxy_request(self, req): + def handle_request(self, req): """ Proxies EP0 requests between the victim and the target. """ @@ -169,5 +199,43 @@ def handle_data_available(self, ep_num, data): self.libusb_device.write(ep_num, data) - def handle_buffer_available(self, ep_num): - pass #print("FAIL! buffer available and we didn't do anything about it") + def handle_nak(self, ep_num): + """ + Handles a NAK, which means that the target asked the proxied device + to participate in a transfer. We use this as our cue to participate + in communications. + """ + + # TODO: Currently, we use this for _all_ non-control transfers, as we + # don't e.g. periodically schedule isochronous or interrupt transfers. + # We probably should set up those to be independently scheduled and + # then limit this to only bulk endpoints. + + # Get the endpoint object we reference. + endpoint = self.endpoints[ep_num] + + # Skip handling OUT endpoints, as we handle those in handle_data_available. + if not endpoint.direction: + return + + self._proxy_in_transfer(endpoint) + + + def _proxy_in_transfer(self, endpoint): + """ + Proxy OUT requests, which sends a request from the target device to the + victim, at the target's request. + """ + + # Read the target data from the target device. + endpoint_address = endpoint.number | 0x80 + data = self.libusb_device.read(endpoint_address, endpoint.max_packet_size) + + # Run the data through all of our filters. + for f in self.filter_list: + ep_num, data = f.filter_in(endpoint.number, data) + + # If our data wasn't filtered out, transmit it to the target! + if data: + endpoint.send_packet(data) + diff --git a/facedancer/backends/GreatDancerApp.py b/facedancer/backends/GreatDancerApp.py index 5c25c98..9d7de52 100644 --- a/facedancer/backends/GreatDancerApp.py +++ b/facedancer/backends/GreatDancerApp.py @@ -17,8 +17,9 @@ class GreatDancerApp(FacedancerApp): app_num = 0x00 # This doesn't have any meaning for us. # Interrupt register (USBSTS) bits masks. - USBSTS_D_UI = (1 << 0) - USBSTS_D_URI = (1 << 6) + USBSTS_D_UI = (1 << 0) + USBSTS_D_URI = (1 << 6) + USBSTS_D_NAKI = (1 << 16) # Number of supported USB endpoints. # TODO: bump this up when we develop support using USB0 (cables flipped) @@ -33,6 +34,7 @@ class GreatDancerApp(FacedancerApp): GET_ENDPTSETUPSTAT = 1 GET_ENDPTCOMPLETE = 2 GET_ENDPTSTATUS = 3 + GET_ENDPTNAK = 4 # Quirk flags QUIRK_MANUAL_SET_ADDRESS = 0x01 @@ -583,6 +585,14 @@ def _fetch_transfer_readiness(self): return self._fetch_status_register(self.GET_ENDPTSTATUS) + def _fetch_endpoint_nak_status(self): + """ + Queries the GreatFET for a bitmap describing the endpoints that have issued + a NAK since the last time this was checked. + """ + return self._fetch_status_register(self.GET_ENDPTNAK) + + def _prime_out_endpoint(self, endpoint_number): """ Primes an out endpoint, allowing it to recieve data the next time the host chooses to send it. @@ -647,6 +657,26 @@ def _is_ready_for_priming(self, ep_num, direction): return ready_for_in + @classmethod + def _has_issued_nak(cls, ep_nak, ep_num, direction): + """ + Interprets an ENDPTNAK status result to determine + whether a given endpoint has NAK'd. + + ep_nak: The status work read from the ENDPTNAK register + ep_num: The endpoint number in question. + direction: The endpoint direction in question. + """ + + in_nak = (ep_nak & (1 << (ep_num + 16))) + out_nak = (ep_nak & (1 << (ep_num))) + + if direction == cls.HOST_TO_DEVICE: + return out_nak + else: + return in_nak + + def _bus_reset(self): """ Triggers the GreatDancer to perform its side of a bus reset. @@ -658,6 +688,30 @@ def _bus_reset(self): self.device.vendor_request_out(self.vendor_requests.GREATDANCER_BUS_RESET) + def _handle_nak_events(self): + """ + Handles an event in which the GreatDancer has NAK'd an IN token. + """ + + # If we haven't been configured yet, we can't have any + # endpoints other than the control endpoint, and we don't need to + # handle any NAKs. + if not self.configuration: + return + + # Fetch the endpoint status. + status = self._fetch_endpoint_nak_status() + + # Iterate over each usable endpoint. + for interface in self.configuration.interfaces: + for endpoint in interface.endpoints: + + # If the endpoint has NAK'd, issued the relevant callback. + if self._has_issued_nak(status, endpoint.number, endpoint.direction): + self.connected_device.handle_nak(endpoint.number) + + + def _configure_endpoints(self, configuration): """ Configures the GreatDancer's endpoints to match the provided configuration. @@ -684,8 +738,9 @@ def configured(self, configuration): self.configuration = configuration # If we've just set up endpoints, check to see if any of them - # need to be primed. + # need to be primed, or have NAKs waiting. self._handle_transfer_readiness() + self._handle_nak_events() def service_irqs(self): @@ -711,3 +766,6 @@ def service_irqs(self): if status & self.USBSTS_D_URI: self._bus_reset() + if status & self.USBSTS_D_NAKI: + self._handle_nak_events() + diff --git a/facedancer/filters/logging.py b/facedancer/filters/logging.py index d32aed8..965dbf5 100644 --- a/facedancer/filters/logging.py +++ b/facedancer/filters/logging.py @@ -3,11 +3,12 @@ # import datetime - from ..USBProxy import USBProxyFilter class USBProxyPrettyPrintFilter(USBProxyFilter): - pass + """ + Filter that pretty prints USB transactions according to log levels. + """ def __init__(self, verbose, decoration=''): """ @@ -16,9 +17,14 @@ def __init__(self, verbose, decoration=''): self.verbose = verbose self.decoration = decoration + + def filter_control_in(self, req, data, stalled): + """ + Log IN control requests without modification. + """ - if req is None: + if self.verbose > 3 and req is None: print("{} {}< --filtered out-- ".format(self.timestamp(), self.decoration)) return req, data, stalled @@ -36,6 +42,9 @@ def filter_control_in(self, req, data, stalled): def filter_control_out(self, req, data): + """ + Log OUT control requests without modification. + """ # TODO: just call control_in, it's the same: @@ -53,6 +62,9 @@ def filter_control_out(self, req, data): def handle_out_request_stall(self, req, data, stalled): + """ + Handles cases where OUT requests are stalled (and thus we don't get data). + """ if self.verbose > 3 and req is None: if stalled: print("{} {}> --STALLED-- ".format(self.timestamp(), self.decoration)) @@ -63,33 +75,41 @@ def handle_out_request_stall(self, req, data, stalled): def filter_in(self, ep_num, data): + """ + Log IN transfers without modification. + """ - if self.verbose > 4: - print("IN", ep_num, data) + if self.verbose > 4 and data: + self._pretty_print_data(data, '<', self.decoration, ep_marker=ep_num) return ep_num, data def filter_out(self, ep_num, data): + """ + Log OUT transfers without modification. + """ - if self.verbose > 4: - print("OUT", ep_num, data) + if self.verbose > 4 and data: + self._pretty_print_data(data, '>', self.decoration, ep_marker=ep_num) return ep_num, data def timestamp(self): + """ Generate a quick timestamp for printing. """ return datetime.datetime.now().strftime("[%H:%M:%S]") def _magic_decode(self, data): + """ Simple decode function that attempts to find a nice string represetation for the console.""" try: return bytes(data).decode('utf-16le') except: return bytes(data) - def _pretty_print_data(self, data, direction_marker, decoration='', is_string=False): + def _pretty_print_data(self, data, direction_marker, decoration='', is_string=False, ep_marker=''): data = self._magic_decode(data) if is_string else bytes(data) - print("{} {}{}: {}".format(self.timestamp(), decoration, direction_marker, data)) + print("{} {}{}{}: {}".format(self.timestamp(), ep_marker, decoration, direction_marker, data)) diff --git a/facedancer/filters/standard.py b/facedancer/filters/standard.py index 7380f9e..c4430bf 100644 --- a/facedancer/filters/standard.py +++ b/facedancer/filters/standard.py @@ -20,63 +20,41 @@ class USBProxySetupFilters(USBProxyFilter): GET_DESCRIPTOR_REQUEST = 6 RECIPIENT_DEVICE = 0 + DESCRIPTOR_CONFIRGUATION = 0x02 + def __init__(self, device, verbose=0): self.device = device - self.configuration = None + self.configurations = {} self.verbose = verbose def filter_control_in(self, req, data, stalled): - # FIXME: replace Dominic's wonderful shotgun parser :) - if stalled: return req, data, stalled - if req.request == self.GET_DESCRIPTOR_REQUEST and \ - req.value == 0x0200 and req.length >= 32: - cfg = data[:data[0]] - rest = data[data[0]:] - iface = rest[:rest[0]] - rest = rest[rest[0]:] - x = iface[4] - eps = [] - while x: - eps.append(rest[:rest[0]]) - rest = rest[rest[0]:] - x -= 1 - endpoints = [ - USBEndpoint( - ep[2], - (ep[2]&0x80)>>7, - ep[3]&0x03, - (ep[3]>>2)&0x03, - (ep[3]>>4)&0x03, - ep[4] | ep[5]<<8, - ep[6], - None - ) - for ep in eps - ] - interface = USBInterface( - iface[2], - iface[3], - iface[5], - iface[6], - iface[7], - iface[8], - endpoints = endpoints - ) - - self.configuration = USBConfiguration( - cfg[5], - "", - [interface] - ) - if self.verbose > 1: - print("-- Storing configuration: {} --".format(self.configuration)) + + # If this is a read of a valid configuration descriptor (and subordinate + # descriptors, parse them and store the results for late). + if req.request == self.GET_DESCRIPTOR_REQUEST: + + # Get the descriptor type and index. + descriptor_type = req.value >> 8 + descriptor_index = req.value & 0xFF + + # If this is a configuration descriptor, store information relevant + # to the configuration. We'll need this to set up the endpoint + # hardware on the facedancer device. + if descriptor_type == self.DESCRIPTOR_CONFIRGUATION and req.length >= 32: + configuration = USBDescribable.from_binary_descriptor(data) + self.configurations[configuration.configuration_index] = configuration + + if self.verbose > 1: + print("-- Storing configuration {} --".format(configuration)) + return req, data, stalled + def filter_control_out(self, req, data): # Special case: if this is a SET_ADDRESS request, # handle it ourself, and absorb it. @@ -85,13 +63,25 @@ def filter_control_out(self, req, data): self.device.handle_set_address_request(req) return None, None + # Special case: if this is a SET_CONFIGURATION_REQUEST, + # pass it through, but also set up the Facedancer hardware + # in response. if req.get_recipient() == self.RECIPIENT_DEVICE and \ req.request == self.SET_CONFIGURATION_REQUEST: - if self.configuration and self.verbose > 1: - print("-- Applying configuration {} --".format(self.configuration)) + configuration_index = req.value + + # If we have a known configuration for this index, apply it. + if configuration_index in self.configurations: + configuration = self.configurations[configuration_index] + + if self.verbose > 0: + print("-- Applying configuration {} --".format(configuration)) + + self.device.configured(configuration) + + # Otherwise, the host has applied a configruation without ever reading + # its descriptor. This is mighty strange behavior! elif self.verbose > 0: - print("-- WARNING: no configuration to apply! --") + print("-- WARNING: Applying configuration {}, but we've never read that configuration's descriptor! --".format(configuration_index)) - if self.configuration: - self.device.maxusb_app.configured(self.configuration) return req, data