Using Facedancer

Introduction

Facedancer allows you to easily define emulations using a simple declarative DSL that mirrors the hierarchical structure of the abstract USB device model.

Let’s look at a simple example that defines a USB device with two endpoints and a control interface:

 7import logging
 8
 9from facedancer import *
10from facedancer import main
11
12@use_inner_classes_automatically
13class MyDevice(USBDevice):
14    product_string      : str = "Example USB Device"
15    manufacturer_string : str = "Facedancer"
16    vendor_id           : int = 0x1209
17    product_id          : int = 0x0001
18    device_speed        : DeviceSpeed = DeviceSpeed.FULL
19
20    class MyConfiguration(USBConfiguration):
21
22        class MyInterface(USBInterface):
23
24            class MyInEndpoint(USBEndpoint):
25                number          : int          = 1
26                direction       : USBDirection = USBDirection.IN
27                max_packet_size : int          = 64
28
29                def handle_data_requested(self: USBEndpoint):
30                    logging.info("handle_data_requested")
31                    self.send(b"device on bulk endpoint")
32
33            class MyOutEndpoint(USBEndpoint):
34                number          : int          = 1
35                direction       : USBDirection = USBDirection.OUT
36                max_packet_size : int          = 64
37
38                def handle_data_received(self: USBEndpoint, data):
39                    logging.info(f"device received {data} on bulk endpoint")
40
41    @vendor_request_handler(number=1, direction=USBDirection.IN)
42    @to_device
43    def my_in_vendor_request_handler(self: USBDevice, request: USBControlRequest):
44        logging.info("my_in_vendor_request_handler")
45        request.reply(b"device on control endpoint")
46
47    @vendor_request_handler(number=2, direction=USBDirection.OUT)
48    @to_device
49    def my_out_vendor_request_handler(self: USBDevice, request: USBControlRequest):
50        logging.info(f"device received {request.index} {request.value} {bytes(request.data)} on control endpoint")
51        request.ack()
52
53
54main(MyDevice)

Device Descriptor

The entry-point for most Facedancer emulations is the USBDevice class which maintains the configuration as well as the transfer handling implementation of the device under emulation.

Note

In some cases you may want to use the USBBaseDevice class if you’d like to provide your own implementation of the standard request handlers.

See, for example, USBProxyDevice.

Starting with the initial class declaration we can define our device as:

from facedancer import *

@use_inner_classes_automatically
class MyDevice(USBDevice):
    product_string      : str = "Example USB Device"
    manufacturer_string : str = "Facedancer"
    vendor_id           : int = 0x1209 # https://pid.codes/1209/
    product_id          : int = 0x0001

We start by importing the Facedancer library and declaring a class MyDevice derived from USBDevice.

We also annotate our class with the @use_inner_classes_automatically decorator which allows us to use a declarative style when including our devices configuration, interface and endpoints. It’s magic!

Finally, we fill in some basic fields Facedancer will use to populate the device descriptor: product_string, manufacturer_string, vendor_id and product_id.

Note

You can find a full list of supported fields in the USBDevice API documentation.

Configuration Descriptor

Once we have provided Facedancer with the basic information it needs to build a device descriptor we can move on to declare and define our device’s configuration descriptor.

Most devices consist of a single configuration managed by the USBConfiguration class containing at least one USBInterface class containing zero or more USBEndpoint class.

Here we define a configuration with a single interface containing two endpoints. The first endpoint has direction IN and will be responsible for responding to data requests from the host. The second endpoint has direction OUT and will be responsible for receiving data from the host.

...
class MyDevice(USBDevice):
    ...

    class MyConfiguration(USBConfiguration):
        class MyInterface(USBInterface):
            class MyInEndpoint(USBEndpoint):
                number    : int          = 1
                direction : USBDirection = USBDirection.IN
            class MyOutEndpoint(USBEndpoint):
                number    : int          = 1
                direction : USBDirection = USBDirection.OUT

We’ve now provided enough information in our emulation for it to be successfully enumerated and recognized by the host but there is still one thing missing!

Request Handlers

For our device to actually do something we also need a way to:

  • Respond to a request for data from the host.

  • Receive data sent by the host.

Note

USB is a polled protocol where the host always initiates all transactions. Data will only ever be sent from the device if the host has first requested it from the device.

The Facedancer facedancer.endpoint and facedancer.request modules provides the functionality for responding to requests on the device’s endpoints and the control interface. (All USB devices support a control endpoint – usually endpoint zero.)

Endpoint Request Handlers

Endpoint request handlers are usually either class-specific or vendor-defined and can be declared inside the device’s endpoint declaration.

Here we will define two simple handlers for each endpoint.

For our IN endpoint we will reply to any data request from the host with a fixed message and for our OUT endpoint we will just print the received data to the terminal.

...
class MyDevice(USBDevice):
    ...

    class MyConfiguration(USBConfiguration):
        class MyInterface(USBInterface):
            class MyInEndpoint(USBEndpoint):
                number    : int          = 1
                direction : USBDirection = USBDirection.IN

                # called when the host requested data from the device on endpoint 0x81
                def handle_data_requested(self: USBEndpoint):
                    self.send(b"device sent response on bulk endpoint", blocking=True)

            class MyOutEndpoint(USBEndpoint):
                number    : int          = 1
                direction : USBDirection = USBDirection.OUT

                # called when the host sent data to the device on endpoint 0x01
                def handle_data_received(self: USBEndpoint, data):
                    logging.info(f"device received '{data}' on bulk endpoint")

For more information on supported endpoint operations and fields see the USBEndpoint documentation.

Control Request Handlers

Control Requests are typically used for command and status operations. While Facedancer will take care of responding to standard control requests used for device enumeration you may also want to implement custom vendor requests or even override standard control request handling.

To this end, Facedancer provides two sets of decorators to be used when defining a device’s control interface:

The first set of decorators allows you to specify the type of control request to be handled:

The second set defines the target for the control request:

For instance, to define some vendor request handlers you can do:

...
class MyDevice(USBDevice):
    ...
    class MyConfiguration(USBConfiguration):
    ...

    @vendor_request_handler(request_number=1, direction=USBDirection.IN)
    @to_device
    def my_vendor_request_handler(self: USBDevice, request: USBControlRequest):
        request.reply(b"device sent response on control endpoint")

    @vendor_request_handler(request_number=2, direction=USBDirection.OUT)
    @to_device
    def my_other_vendor_request_handler(self: USBDevice, request: USBControlRequest):
        logging.info(f"device received '{request.index}' '{request.value}' '{request.data}' on control endpoint")

        # acknowledge the request
        request.ack()

More information on the request parameter can be found in the USBControlRequest documentation.

Testing The Emulation

We now have a full USB device emulation that will enumerate and respond to requests from the host.

Give it a try!

 1import logging
 2
 3def main():
 4    import asyncio
 5    import usb1
 6
 7    VENDOR_REQUEST    = 0x65
 8    MAX_TRANSFER_SIZE = 64
 9
10    with usb1.USBContext() as context:
11        #logging.info("Host: waiting for device to connect")
12        #await asyncio.sleep(1)
13
14        device_handle = context.openByVendorIDAndProductID(0x1209, 0x0001)
15        if device_handle is None:
16            raise Exception("device not found")
17        device_handle.claimInterface(0)
18
19        # test IN endpoint
20        logging.info("Testing bulk IN endpoint")
21        response = device_handle.bulkRead(
22            endpoint = 0x81,
23            length   = MAX_TRANSFER_SIZE,
24            timeout  = 1000,
25        )
26        logging.info(f"[host] received '{response}' from bulk endpoint")
27        print("")
28
29        # test OUT endpoint
30        logging.info("Testing bulk OUT endpoint")
31        response = device_handle.bulkWrite(
32            endpoint = 0x01,
33            data     = b"host say oh hai on bulk endpoint",
34            timeout  = 1000,
35        )
36        print(f"sent {response} bytes\n")
37
38        # test IN vendor request handler
39        logging.info("Testing IN control transfer")
40        response = device_handle.controlRead(
41            request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE,
42            request      = 1,
43            index        = 2,
44            value        = 3,
45            length       = MAX_TRANSFER_SIZE,
46            timeout      = 1000,
47        )
48        logging.info(f"[host] received '{response}' from control endpoint")
49        print("")
50
51        # test OUT vendor request handler
52        logging.info("Testing OUT control transfer")
53        response = device_handle.controlWrite(
54            request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE,
55            request      = 2,
56            index        = 3,
57            value        = 4,
58            data         = b"host say oh hai on control endpoint",
59            timeout      = 1000,
60        )
61        print(f"sent {response} bytes\n")
62
63
64if __name__ == "__main__":
65    logging.getLogger().setLevel(logging.DEBUG)
66    main()

Suggestion Engine

Facedancer provides a suggestion engine that can help when trying to map an undocumented device’s control interface.

It works by monitoring the control requests from the host and tracking any which are not supported by your emulation.

You can enable it by passing the –suggest flag when running an emulation:

python ./emulation.py --suggest

When you exit the emulation it can then suggest the handler functions you still need to implement in order to support the emulated device’s control interface:

Automatic Suggestions
---------------------
These suggestions are based on simple observed behavior;
not all of these suggestions may be useful / desirable.

Request handler code:

@vendor_request_handler(number=1, direction=USBDirection.IN)
@to_device
def handle_control_request_1(self, request):
    # Most recent request was for 64B of data.
    # Replace me with your handler.
    request.stall()

Annotated template

The Facedancer repository contains an annotated template which provides an excellent reference source when building your own devices:

  8import logging
  9
 10from facedancer         import main
 11from facedancer         import *
 12from facedancer.classes import USBDeviceClass
 13
 14@use_inner_classes_automatically
 15class TemplateDevice(USBDevice):
 16    """ This class is meant to act as a template to help you get acquainted with FaceDancer."""
 17
 18    #
 19    # Core 'dataclass' definitions.
 20    # These define the basic way that a FaceDancer device advertises itself to the host.
 21    #
 22    # Every one of these is optional. The defaults are relatively sane, so you can mostly
 23    # ignore these unless you want to change them! See the other examples for more minimal
 24    # data definitions.
 25    #
 26
 27    # The USB device class, subclass, and protocol for the given device.
 28    # Often, we'll leave these all set to 0, which means the actual class is read
 29    # from the interface.
 30    #
 31    # Note that we _need_ the type annotations on these. Without them, Python doesn't
 32    # consider them valid dataclass members, and ignores them. (This is a detail of python3.7+
 33    # dataclasses.)
 34    #
 35    device_class             : int  = 0
 36    device_subclass          : int  = 0
 37    protocol_revision_number : int  = 0
 38
 39    # The maximum packet size on EP0. For most devices, the default value of 64 is fine.
 40    max_packet_size_ep0      : int  = 64
 41
 42    # The vendor ID and product ID that we want to give our device.
 43    vendor_id                : int  = 0x610b
 44    product_id               : int  = 0x4653
 45
 46    # The string descriptors we'll provide for our device.
 47    # Note that these should be Python strings, and _not_ bytes.
 48    manufacturer_string      : str  = "FaceDancer"
 49    product_string           : str  = "Generic USB Device"
 50    serial_number_string     : str  = "S/N 3420E"
 51
 52    # This tuple is a list of languages we're choosing to support.
 53    # This gives us an opportunity to provide strings in various languages.
 54    # We don't typically use this; so we can leave this set to a language of
 55    # your choice.
 56    supported_languages      : tuple = (LanguageIDs.ENGLISH_US,)
 57
 58    # The revision of the device hardware. This doesn't matter to the USB specification,
 59    # but it's sometimes read by drivers.
 60    device_revision          : int  = 0
 61
 62    # The revision of the USB specification that this device adheres to.
 63    # Typically, you'll leave this at '2'.
 64    usb_spec_version         : int  = 0x0002
 65
 66
 67    #
 68    # We'll define a single configuration on our device. To be compliant,
 69    # every device needs at least a configuration and an interface.
 70    #
 71    # Note that we don't need to do anything special to have this be used.
 72    # As long as we're using the @use_inner_classes_automatically decorator,
 73    # this configuration will automatically be instantiated and used.
 74    #
 75    class TemplateConfiguration(USBConfiguration):
 76
 77        #
 78        # Configuration fields.
 79        #
 80        # Again, all of these are optional; and the default values
 81        # are sane and useful.
 82        #
 83
 84        # Configuration number. Every configuration should have a unique
 85        # number, which should count up from one. Note that a configuration
 86        # shouldn't have a number of 0, as that's USB for "unconfigured".
 87        configuration_number : int            = 1
 88
 89        # A simple, optional descriptive name for the configuration. If provided,
 90        # this is referenced in the configuration's descriptor.
 91        configuration_string : str            = None
 92
 93        # This setting is set to true if the device can run without bus power,
 94        # or false if it pulls its power from the USB bus.
 95        self_powered           : bool         = False
 96
 97        # This setting is set to true if the device can ask that the host
 98        # wake it up from sleep. If set to true, the host may choose to
 99        # leave power on to the device when the host is suspended.
100        supports_remote_wakeup : bool         = True
101
102        # The maximum power the device will use in this configuration, in mA.
103        # Typically, most devices will request 500mA, the maximum allowed.
104        max_power              : int            = 500
105
106
107        class TemplateInterface(USBInterface):
108
109            #
110            # Interface fields.
111            # Again, all optional and with useful defaults.
112            #
113
114            # The interface index. Each interface should have a unique index,
115            # starting from 0.
116            number                 : int = 0
117
118            # The information about the USB class implemented by this interface.
119            # This is the place where you'd specify if this is e.g. a HID device.
120            class_number           : int = USBDeviceClass.VENDOR_SPECIFIC
121            subclass_number        : int = 0
122            protocol_number        : int = 0
123
124            # A short description of the interface. Optional and typically only informational.
125            interface_string       : str = None
126
127
128            #
129            # Here's where we define any endpoints we want to add to the device.
130            # These behave essentially the same way as the above.
131            #
132            class TemplateInEndpoint(USBEndpoint):
133
134                #
135                # Endpoints are unique in that they have two _required_
136                # properties -- their number and direction.
137                #
138                # Together, these two fields form the endpoint's address.
139                number               : int                    = 1
140                direction            : USBDirection           = USBDirection.IN
141
142                #
143                # The remainder of the fields are optional and have useful defaults.
144                #
145
146                # The transfer type selects how data will be transferred over the endpoints.
147                # The currently supported types are BULK and INTERRUPT.
148                transfer_type        : USBTransferType        = USBTransferType.BULK
149
150                # The maximum packet size determines how large packets are allowed to be.
151                # For a full speed device, a max-size value of 64 is typical.
152                max_packet_size      : int = 64
153
154                # For interrupt endpoints, the interval specifies how often the host should
155                # poll the endpoint, in milliseconds. 10ms is a typical value.
156                interval             : int = 0
157
158
159                #
160                # Let's add an event handler. This one is called whenever the host
161                # wants to read data from the device.
162                #
163                def handle_data_requested(self):
164
165                    # We can reply to this request using the .send() method on this
166                    # endpoint, like so:
167                    self.send(b"Hello!")
168
169                    # We can also get our parent interface using .parent;
170                    # or a reference to our device using .get_device().
171
172
173            class TemplateOutEndpoint(USBEndpoint):
174                #
175                # We'll use a more typical set of properties for our OUT endpoint.
176                #
177                number               : int                    = 1
178                direction            : USBDirection           = USBDirection.OUT
179
180
181                #
182                # We'll also demonstrate use of another event handler.
183                # This one is called whenever data is sent to this endpoint.
184                #
185                def handle_data_received(self, data):
186                    logging.info(f"Received data: {data}")
187
188
189    #
190    # Any of our components can use callback functions -- not just our endpoints!
191    # The callback names are the same no matter where we use them.
192    #
193    def handle_data_received(self, endpoint, data):
194
195        #
196        # When using a callback on something other than an endpoint, our function's
197        # signature is slightly different -- it takes the relevant endpoint as an
198        # argument, as well.
199        #
200
201        # We'll delegate this back to the core handler, here, so it propagates to our subordinate
202        # endpoints -- but we don't have to! If we wanted to, we could call functions on the
203        # endpoint itself. This is especially useful if we're hooking handle_data_requested(),
204        # where we can use endpoint.send() to provide the relevant data.
205        super().handle_data_received(endpoint, data)
206
207        # Note that non-endpoints have a get_endpoint() method, which you can use to get references
208        # to endpoints by their endpoint numbers / directions. This is useful if you want to
209        # send something on another endpoint in response to data received.
210        #
211        # The device also has a .send() method, which accepts an endpoint number and the data to
212        # be sent. This is equivalent to calling .send() on the relevant endpoint.
213
214
215    #
216    # We can very, very easily add request handlers to our devices.
217    #
218    @vendor_request_handler(number=12)
219    def handle_my_request(self, request):
220
221        #
222        # By decorating this function with "vendor_request_handler", we've ensured this
223        # function is called to handle vendor request 12. We can also add other arguments to
224        # the vendor_request_handler function -- it'll accept a keyword argument for every
225        # property on the request. If you provide these, the handler will only be called
226        # if the request matches the relevant constraint.
227        #
228        # For example, @vendor_request_handler(number=14, direction=USBDirection.IN, index_low=3)
229        # means the decorated function is only called to handle vendor request 14 for IN requests
230        # where the low byte of the index is 3.
231        #
232        # Other handler decorators exist -- like "class_request_handler" or "standard_request_handler"
233        #
234
235        # Replying to an IN request is easy -- you just provide the reply data using request.reply().
236        request.reply(b"Hello, there!")
237
238
239    @vendor_request_handler(number=1, direction=USBDirection.OUT)
240    @to_device
241    def handle_another_request(self, request):
242
243        #
244        # Another set of convenience decorators exist to refine requests.
245        # Decorators like `to_device` or `to_any_endpoint` chain with our
246        # request decorators, and are syntax sugar for having an argument like
247        # ``recipient=USBRequestRecipient.DEVICE`` in the handler decorator.
248        #
249
250        # For out requests, in lieu of a response, we typically want to acknowledge
251        # the request. This can be accomplished by calling .acknowledge() or .ack()
252        # on the request.
253        request.ack()
254
255        # Of course, if we want to let the host know we can't handle a request, we
256        # may also choose to stall it. This is as simple as calling request.stall().
257
258
259    #
260    # Note that request handlers can be used on configurations, interfaces, and
261    # endpoints as well. For the latter two cases, the decorators `to_this_interface`
262    # and `to_this_endpoint` are convenient -- they tell a request to run only if
263    # it's directed at that endpoint in particular, as selected by its ``index`` parameter.
264    #
265
266
267# FaceDancer ships with a default main() function that you can use to set up and run
268# your device. It ships with some nice features -- including a ``--suggest`` function
269# that can suggest pieces of boilerplate code that might be useful in device emulation.
270#
271# main() will accept either the type of device to emulate, or an device instance.
272# It'll also accept asyncio coroutines, in case you want to run things alongside the
273# relevant device code. See e.g. `examples/rubber-ducky.py` for an example.
274#
275main(TemplateDevice)
276