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 ofcynthion 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.
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)