Emulation of a USB Device

This tutorial walks through the whole process of emulating a USB device with Cynthion and Facedancer. We’ll emulate HackRF One, a software-defined radio platform. The goal of our emulation is to fool the hackrf_info command into reporting that a HackRF One is connected.

Prerequisites

  • Install the Cynthion tools by following Getting Started with Cynthion.

  • Install HackRF Tools by following Installing HackRF Software.

  • Install the Facedancer library and run the Facedancer bitstream and firmware as described in Using Cynthion with Facedancer.

    Note

    If you would like to configure your Cynthion for Facedancer operation permanently instead of temporarily, use cynthion flash facedancer instead of cynthion run facedancer.

Try to Detect a HackRF One

Use the hackrf_info command to detect any connected HackRF devices:

hackrf_info

The command output should indicate that no HackRF devices are found:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
No HackRF boards found.

Connect

We need to connect our Cynthion before we can use it to emulate a HackRF One. If you followed the prerequisites above, you should already have connected the Cynthion’s CONTROL port to your computer.

Now also connect the TARGET C port to your computer. Facedancer software uses CONTROL to control the Cynthion and TARGET C to connect to the target host, the computer which we’ll try to fool into thinking that there is a HackRF One connected. The control host and target host can be two separate computers, but in this tutorial we will use the same computer as both the control host and the target host.

Connection diagram for using Cynthion with Facedancer on a single host computer.

Emulate the Vendor ID and Product ID

Use your favorite text editor to create a new Python program called hackrf_emulation.py with the following contents:

from facedancer import *
from facedancer import main

@use_inner_classes_automatically
class HackRF(USBDevice):
    product_string       : str = "HackRF One (Emulated)"
    manufacturer_string  : str = "Facedancer"
    serial_number_string : str = "1234"
    vendor_id            : int = 0x1d50
    product_id           : int = 0x6089

    class DefaultConfiguration(USBConfiguration):
        class DefaultInterface(USBInterface):
            pass

main(HackRF)

Every USB device identifies itself to its host computer using a 16-bit Vendor ID and 16-bit Product ID. This program uses the Facedancer library to implement a device with the Vendor ID and Product ID associated with HackRF One. It also configures some strings which make our emulated HackRF distinguishable from an actual HackRF One (with tools such as lsusb) for convenience.

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, open another terminal and execute hackrf_info. It should display output similar to this:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
hackrf_board_id_read() failed: Pipe error (-1000)

We’ve just convinced hackrf_info that it has found a HackRF device! However, hackrf_info failed to read the HackRF’s board ID which distinguishes between the various hardware platforms supported by HackRF software. The pipe error indicates that the device did not provide the expected response to the host’s request for the board ID.

Terminate hackrf_emulation.py by typing ctrl-c. Because we used the --suggest option, it should provide output like this:

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=14, direction=USBDirection.IN)
    @to_device
    def handle_control_request_14(self, request):
        # Most recent request was for 1B of data.
        # Replace me with your handler.
        request.stall()

Try the Suggested Code

Add the suggested code to the HackRF class in hackrf_emulation.py. The program should now look like:

from facedancer import *
from facedancer import main

@use_inner_classes_automatically
class HackRF(USBDevice):
    product_string       : str = "HackRF One (Emulated)"
    manufacturer_string  : str = "Facedancer"
    serial_number_string : str = "1234"
    vendor_id            : int = 0x1d50
    product_id           : int = 0x6089

    class DefaultConfiguration(USBConfiguration):
        class DefaultInterface(USBInterface):
            pass

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

main(HackRF)

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
hackrf_board_id_read() failed: Pipe error (-1000)

It turns out that our emulation still results in a pipe error. This is because we are stalling vendor request number 14 which is meant to return a 1 byte board ID. Terminate hackrf_emulation.py and replace the request_stall() line with:

request.reply([1])

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
Board ID Number: 1 (Jawbreaker)
hackrf_version_string_read() failed: Pipe error (-1000)

We’ve now convinced hackrf_info that our Cynthion is a HackRF Jawbreaker which was the beta platform that preceded HackRF One. Let’s try a higher board ID number. Replace request.reply([1]) with:

request.reply([2])

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
Board ID Number: 2 (HackRF One)
hackrf_version_string_read() failed: Pipe error (-1000)

We did it! Our new board ID represents HackRF One! In this example we guessed low numbers for the board ID byte, but we could have discovered that 2 represents HackRF One by observing the behavior of an actual HackRF One or by reading the libhackrf source code or HackRF firmware source code.

Handle the Version String Request

Unfortunately, hackrf_info still indicates an error, this time with reading a version string. The --suggest option on your Facedancer program should give you an idea of how to handle that request:

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

Notice that this time the host has requested 255 bytes instead of just one byte. USB devices often return a smaller number of bytes than the length requested by the host. In this case we can guess that the host is requesting a maximum length string and that we can probably return something shorter. Let’s try adding this to the HackRF class in hackrf_emulation.py:

@vendor_request_handler(number=15, direction=USBDirection.IN)
@to_device
def handle_control_request_15(self, request):
    # Most recent request was for 255B of data.
    request.reply(b"tutorial version")

The complete program should now look like:

from facedancer import *
from facedancer import main

@use_inner_classes_automatically
class HackRF(USBDevice):
    product_string       : str = "HackRF One (Emulated)"
    manufacturer_string  : str = "Facedancer"
    serial_number_string : str = "1234"
    vendor_id            : int = 0x1d50
    product_id           : int = 0x6089

    class DefaultConfiguration(USBConfiguration):
        class DefaultInterface(USBInterface):
            pass

    @vendor_request_handler(number=14, direction=USBDirection.IN)
    @to_device
    def handle_control_request_14(self, request):
        # Most recent request was for 1B of data.
        # Replace me with your handler.
        request.reply([2])

    @vendor_request_handler(number=15, direction=USBDirection.IN)
    @to_device
    def handle_control_request_15(self, request):
        # Most recent request was for 255B of data.
        request.reply(b"tutorial version")

main(HackRF)

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
Board ID Number: 2 (HackRF One)
Firmware Version: tutorial version (API:0.00)
hackrf_board_partid_serialno_read() failed: Pipe error (-1000)

Handle the Part ID Request

Now we can see another unhandled request made by hackrf_info. The --suggest output tells us that we can handle it with something like:

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

The host is asking for 24 bytes this time, suggesting that it is looking for exactly 24 bytes. Let’s try replying with 24 bytes of dummy data:

@vendor_request_handler(number=18, direction=USBDirection.IN)
@to_device
def handle_control_request_18(self, request):
    # Most recent request was for 24B of data.
    request.reply(b"A" * 24)

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
Board ID Number: 2 (HackRF One)
Firmware Version: tutorial version (API:0.00)
Part ID Number: 0x41414141 0x41414141
hackrf_close() failed: Pipe error (-1000)

It looks like the part ID was interpreted as a valid number, and now hackrf_info is trying to close the device! We’re almost done!

Handle the Close Request

Based on the --suggest output, add the following to hackrf_emulation.py:

@vendor_request_handler(number=1, direction=USBDirection.OUT)
@to_device
def handle_control_request_1(self, request):
    request.ack()

Notice that this time the direction of the vendor request is OUT instead of IN. This means that the host is sending data to the device, not asking the device to send data to the host. We acknowledge the request instead of replying with data.

Execute the program:

python hackrf_emulation.py --suggest

While the program is running, execute hackrf_info in another terminal:

hackrf_info version: 2023.01.1
libhackrf version: 2023.01.1 (0.8)
Found HackRF
Index: 0
Serial number: 1234
Board ID Number: 2 (HackRF One)
Firmware Version: tutorial version (API:0.00)
Part ID Number: 0x41414141 0x41414141

Success! hackrf_info now exits without error!

Put It All Together

With a few edits based on what we’ve learned, our complete program might look like this:

from facedancer import *
from facedancer import main

@use_inner_classes_automatically
class HackRF(USBDevice):
    product_string       : str = "HackRF One (Emulated)"
    manufacturer_string  : str = "Facedancer"
    serial_number_string : str = "1234"
    vendor_id            : int = 0x1d50
    product_id           : int = 0x6089

    class DefaultConfiguration(USBConfiguration):
        class DefaultInterface(USBInterface):
            pass

    @vendor_request_handler(number=14, direction=USBDirection.IN)
    @to_device
    def handle_board_id_request(self, request):
        # return 1-byte board ID
        request.reply([2])

    @vendor_request_handler(number=15, direction=USBDirection.IN)
    @to_device
    def handle_version_string_request(self, request):
        # return up to 255 bytes
        request.reply(b"tutorial version")

    @vendor_request_handler(number=18, direction=USBDirection.IN)
    @to_device
    def handle_part_id_request(self, request):
        # return 24 byte part ID
        request.reply(b"A" * 24)

    @vendor_request_handler(number=1, direction=USBDirection.OUT)
    @to_device
    def handle_close_request(self, request):
        request.reply([])

main(HackRF)