Repository: greatscottgadgets/Facedancer Branch: main Commit: 5e4ff94aff49 Files: 77 Total size: 498.6 KB Directory structure: gitextract_tf2gfbvd/ ├── .editorconfig ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Release.make ├── docs/ │ ├── Makefile │ ├── make.bat │ ├── requirements.txt │ └── source/ │ ├── conf.py │ ├── facedancer_examples.rst │ ├── getting_started.rst │ ├── howto_facedancer_backend.rst │ ├── index.rst │ ├── library_overview.rst │ ├── using_facedancer.rst │ └── using_usb_proxy.rst ├── examples/ │ ├── coroutine.py │ ├── ftdi-echo.py │ ├── hackrf-info.py │ ├── imperative.py │ ├── mass-storage.py │ ├── minimal.py │ ├── rubber-ducky.py │ ├── template.py │ ├── test_minimal.py │ └── usbproxy.py ├── facedancer/ │ ├── __init__.py │ ├── backends/ │ │ ├── MAXUSBApp.py │ │ ├── __init__.py │ │ ├── base.py │ │ ├── goodfet.py │ │ ├── greatdancer.py │ │ ├── greathost.py │ │ ├── hydradancer.py │ │ ├── libusbhost.py │ │ ├── moondancer.py │ │ └── raspdancer.py │ ├── classes/ │ │ ├── __init__.py │ │ └── hid/ │ │ ├── __init__.py │ │ ├── descriptor.py │ │ ├── keyboard.py │ │ └── usage.py │ ├── configuration.py │ ├── core.py │ ├── descriptor.py │ ├── device.py │ ├── devices/ │ │ ├── __init__.py │ │ ├── ftdi.py │ │ ├── keyboard.py │ │ └── umass/ │ │ ├── __init__.py │ │ ├── disk_image.py │ │ └── umass.py │ ├── endpoint.py │ ├── errors.py │ ├── filters/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── hid.py │ │ ├── logging.py │ │ └── standard.py │ ├── interface.py │ ├── logging.py │ ├── magic.py │ ├── proxy.py │ ├── request.py │ └── types.py ├── pyproject.toml └── test/ ├── README.md ├── __init__.py ├── base.py ├── device.py ├── test_alternate.py ├── test_descriptors.py ├── test_stress.py └── test_transfers.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # Set our default format parameters. [*] charset = utf-8 indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true max_line_length = 120 # Use python standard indentation for python files. [*.py] indent_style = space ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv .venv/ venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject tags # IntelliJ Project .idea/ # Release files VERSION release-files host-packages ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools build: os: ubuntu-22.04 tools: python: "3.12" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/source/conf.py # Build PDF for docs formats: - pdf python: install: - requirements: docs/requirements.txt ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [3.1.2] - 2025-12-05 ### Fixed * FTDI emulation stopped working with recent Windows releases. (tx @gniezen!) ### Added * Added a usbproxy filter for logging high-level HID requests. (tx @akvadrako!) ### Security * Updated `jinja2` from 3.1.5 to 3.1.6. ## [3.1.1] - 2025-08-01 ### Added * Hydradancer: Handle `clear_halt` (tx @kauwua!) * Add `parent` field to all descriptors. (tx @kauwua!) * Extend mass storage device constructor to be more configurable. (tx @gniezen!) ### Fixed * Mass storage device was unable to use custom descriptors. (tx @gniezen!) ## [3.1.0] - 2025-01-08 > This is a breaking release which may require updates to your usage of Facedancer API's. ### Changed * Dropped support for Python 3.8 and 3.9. The minimum supported Python version is now Python 3.10. * The descriptor API has been changed and expanded to handle more complex device definitions: - New USBDescriptor property: `include_in_config`, which specifies whether the descriptor should be included in a GET_CONFIGURATION response. - Descriptors attached to endpoints are now instantiated (replaces #139) - The `instantiate_subordinates` function is redesigned to avoid silent dropping of subordinates with duplicate identifiers. - Orderings of declaration/insertion of subordinates are preserved, allowing control of ordering in binary configurations. - Fixes to convert some fields to the right types in `from_binary_descriptor` methods. - A dictionary of known strings and their indexes may be passed to `from_binary_descriptor` methods. - The `number` field of `USBDescriptor` is made optional, as it is not required for descriptors attached in a configuration. - The `type_number` field will now be inferred from the `raw` bytes if not otherwise specified. - Add `@include_in_config` and `@requestable(number=N)` decorators for use on declared descriptor classes. - Add docstrings for all `USBDescriptor` fields. - More information: #126 #141 ### Fixed * USBProxy errors after changes for alternate interface settings. ### Added * Round-trip support for creating Facedancer devices, configurations, interfaces, endpoints and descriptors from binary data to objects, to code, to objects and back to binary data. * New backend method: `validate_configuration` for rejecting USB device configurations that are not supported by a given backend. ## [3.0.6] - 2024-11-27 ### Fixed * Updated Keyboard device / rubber-ducky to work with new descriptor handling features. ## [3.0.5] - 2024-11-25 ### Added * Support switching between alternate interface settings. * Improved Facedancer descriptor functionality. * Log a warning when Moondancer needs system permissions for the interface. * Group Facedancer request handler suggestions by their recipients. * Implement the `raw` field in `HIDReportDescriptor`. (tx @jalmeroth!) ### Fixed * Moondancer: Only prime control endpoints on receipt of a setup packet. * Moondancer: Use `ep_out_interface_enable` instead of `ep_out_prime_endpoint` where appropriate. ## [3.0.4] - 2024-10-10 ### Added * Example: `examples/coroutine.py` demonstrates how to create a custom main function and the use of coroutines. * Keyboard shortcut: `Ctrl-C` will now gracefully exit a Facedancer emulation. ## [3.0.3] - 2024-09-19 ### Added * Support for specifying string descriptor indices. * Allow `supported_languages = None` for device definitions. * Provide an error message when device claim/release fails. * New backend method: `clear_halt()` * New backend method: `send_on_control_endpoint()` * [HydraDancer](https://github.com/HydraDancer) backend. (tx @kauwua!) ### Fixed * Correct byteorder for bcdUSB and bcdDevice. * Older facedancer backends were not derived from `FacedancerBackend`. * Log message in `handle_set_interface_request` was using the incorrect logging method. (tx @kawua!) ## [3.0.2] - 2024-08-20 ### Changed * Added support for Cynthion on Windows. * Update docs to reflect current status of GreatFET support on Windows. ## [3.0.1] - 2024-08-19 ### Changed * USBProxy now auto-detaches kernel drivers for the device being proxied. * Updated documentation with current status of Facedancer support on Windows. ### Fixed * Clarify the explanatory text for endpoint numbers in the app template. (tx @salcho!) * Shutting down Facedancer proxy devices could result in a `LIBUSB_ERROR_BUSY` (tx @mipek!) * Facedancer devices would be incorrectly identified as `goodfet` when `/dev/ttyUSB0` exists on the host device. * Fixed ambiguous documentation terminology to always use one of "Target Host", "Control Host". ## [3.0.0] - 2024-06-18 ### Added - Facedancer documentation has been updated and can be found at: [https://facedancer.readthedocs.io](https://facedancer.readthedocs.io) - A new backend has been added for the Great Scott Gadgets Cynthion. - Emulations can now set USB device speed on supported boards. ### Changed - The Facedancer core API has been rewritten. See the Facedancer documentation for details. - Some legacy applets have been replaced with new examples based on the modern Facedancer core: - `facedancer-ftdi.py` => `ftdi-echo.py` - `facedancer-keyboard.py` => `rubber-ducky.py` - `facedancer-umass.py` => `mass-storage.py` ### Fixed - 64bit LBA support has been added to the `mass-storage.py` example. (Tx @shutingrz!) ### Removed - The legacy Facedancer core has been removed. If you're using scripts or training materials that depend on features or APIs removed in `v3.0.x` please use `v2.9.x`. - All legacy applets not ported to the modern Facedancer core have been removed. ## [2.9.0] - 2024-02-09 This release is intended as a reference point for anyone who has scripts, training materials etc. that are based on Facedancer `v2.x` features or API's that have been deprecated from `v3` onwards. Any future bug-fixes or backports to Facedancer `2.9.x` should use the [`v2.9.x branch`](https://github.com/greatscottgadgets/facedancer/tree/v2.9.x) as the starting point for forks or PR's. ### Deprecated - The current Facedancer core will be supersed by the implementation in `future/` with the `v3.0` release. [Unreleased]: https://github.com/greatscottgadgets/facedancer/compare/3.1.2...HEAD [3.1.2]: https://github.com/greatscottgadgets/facedancer/compare/3.1.1...3.1.2 [3.1.1]: https://github.com/greatscottgadgets/facedancer/compare/3.1.0...3.1.1 [3.1.0]: https://github.com/greatscottgadgets/facedancer/compare/3.0.6...3.1.0 [3.0.6]: https://github.com/greatscottgadgets/facedancer/compare/3.0.5...3.0.6 [3.0.5]: https://github.com/greatscottgadgets/facedancer/compare/3.0.4...3.0.5 [3.0.4]: https://github.com/greatscottgadgets/facedancer/compare/3.0.3...3.0.4 [3.0.3]: https://github.com/greatscottgadgets/facedancer/compare/3.0.2...3.0.3 [3.0.2]: https://github.com/greatscottgadgets/facedancer/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/greatscottgadgets/facedancer/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/greatscottgadgets/facedancer/compare/2.9.0...3.0.0 [2.9.0]: https://github.com/greatscottgadgets/facedancer/releases/tag/2.9.0 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at straithe@greatscottgadgets.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ Copyright (c) 2019 Katherine J. Temkin Copyright (c) 2018 Dominic Spill Copyright (c) 2018 Travis Goodspeed Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # Facedancer 3.0 This repository houses the next generation of Facedancer software. Descended from the original GoodFET-based Facedancer, this repository provides a python module that provides expanded Facedancer support -- including support for multiple boards and some pretty significant new features. ## Project Documentation Facedancer's documentation is captured on [Read the Docs](https://facedancer.readthedocs.io/en/latest/). Raw documentation sources are in the [`docs/`](docs/) folder. ## Installation Install this package with the following command: pip install facedancer After that you can import the facedancer package as usual: $ python >>> import facedancer ## Where are my scripts? Facedancer 3.0 is a ground-up rewrite of the original emulation core and does not support legacy scripts. If you're using scripts or training materials that depend on features or APIs deprecated in `v3.0.x` you can install the latest `v2.9.x` release of Facedancer with: pip install "facedancer<=3" Legacy applets and examples can be found in the [`v2.9.x`](https://github.com/greatscottgadgets/facedancer/tree/v2.9.x) branch. ## What is a Facedancer? Facedancer boards are simple hardware devices that act as "remote-controlled" USB controllers. With the proper software, you can use these boards to quickly and easily emulate USB devices -- and to fuzz USB host controllers! This particular software repository currently allows you to easily create emulations of USB devices in Python. Control is fine-grained enough that you can cause all kinds of USB misbehaviors. :) For more information, see: * [Travis Goodspeed's blog post on Facedancer](http://travisgoodspeed.blogspot.com/2012/07/emulating-usb-devices-with-python.html) * [The Facedancer 21, the original supported board](http://goodfet.sourceforge.net/hardware/facedancer21/) ## USBProxy 'Nouveau' and Protocol Analysis A major new feature of the newer Facedancer codebase is the ability to MITM (Meddler-In-The-Middle) USB connections -- replacing one of the authors' original [USBProxy](https://github.com/dominicgs/usbproxy) project. This opens up a whole new realm of applications -- including protocol analysis and live manipulation of USB packets -- and is especially useful when you don't control the software running on the target device (e.g. on embedded systems or games consoles). ``` +-----------------------------------------------------------------------+ +------------+ | +--------------------------------+ +---------------------------+ | +--------------+ | | | | | | | | | | | PROXIED | | | HOST COMPUTER | | FACEDANCER DEVICE | | | TARGET USB | | DEVICE <------> running Facedancer software <---> acts as USB-Controlled <------> HOST | | | | | | | USB Controller | | | | | | | | | | | | | | +------------+ | +--------------------------------+ +---------------------------+ | +--------------+ | | | MITM Setup (HOST + FACEDANCER) | +-----------------------------------------------------------------------+ ``` This feature is complete, but could use more documentation. Pull requests are welcome. :) ## How do I use this repository? First, you'll likely want to set the ```BACKEND``` environment variable, which lets the software know which type of Facedancer board you'd like to use. If this variable isn't set, the software will try to guess for you based on what's connected. It doesn't always make the best guesses, so you're probably better off setting it yourself. Next, you'll probably want to check out one of the examples, or one of the pre-made scripts. Examples in the new syntax are located under `examples`. The core Facedancer scripts in the "old" syntax are located in `legacy-applets`. For example: ```sh export BACKEND=greatfet ./examples/rubber-ducky.py ``` ## What boards are currently supported? * The [Cynthion USB Test Instrument](http://greatscottgadgets.com/cynthion/) (```BACKEND=cynthion```) * The [GreatFET One](http://greatscottgadgets.com/greatfet/) (```BACKEND=greatfet```) * The NXP LPC4330 Xplorer board. (```BACKEND=greatfet```) * The CCCamp 2015 rad1o badge with GreatFET l0adable (```BACKEND=greatfet```) * All GoodFET-based Facedancers, including the common Facedancer21 (```BACKEND=goodfet```) * RPi + Max3241 Raspdancer boards (```BACKEND=raspdancer```) * HydraDancer and HydraUSB3 boards (```BACKEND=hydradancer```) Note that hardware restrictions prevent the MAX3420/MAX3421 boards from emulating more complex devices -- there's limitation on the number/type of endpoints that can be set up. The LPC4330 boards -- such as the GreatFET -- have fewer limitations. For a similar reason, the MAX3420/MAX3421 boards (`BACKEND=goodfet` or `BACKEND=raspdancer`) currently cannot be used as USBProxy-nv MITM devices. All modern boards (`BACKEND=greatfet`, `BACKEND=hydradancer`) should be fully functional. Note that the HydraDancer and HydraUSB3 boards (`BACKEND=hydradancer`) do not currently support host-mode. Note actual FaceDancer 3.0 does not work on Windows(some issues in pyusb...) and only GNU/Linux ## What boards could be supported soon? * Any Linux computer with gadgetfs support (e.g. the Pi Zero or Beaglebone Black) * Anything supporting USB-over-IP. ## What features do you plan on adding? The roadmap is under development, but in addition to multi-board support, this repository will eventually be home to some cool new features, including: * High-speed ("USB 2.0") device emulation on devices with USB 2.0 PHYs. * On-the-fly generation of USB device controllers in gateware. ## Whose fault _is_ this? There are a lot of people to blame for the awesomeness that is this repo, including: * Kate Temkin (@ktemkin) * Travis Goodspeed (@travisgoodspeed) * Sergey Bratus (@sergeybratus) * Dominic Spill (@dominicgs) * Michael Ossmann (@michaelossmann) * Mikaela Szekely (@Qyriad) * anyone whose name appears in the git history :) ## Contributions? ... are always welcome. Shoot us a PR! ================================================ FILE: Release.make ================================================ # # This file is part of Facedancer. # Maintainer quick actions for generating releases. # # By default, use the system's "python3" binary; but note that some distros now # correctly have 'python' as python3. PYTHON ?= python3 all: prepare_release .PHONY: prepare_release PROJECT = facedancer ifndef VERSION $(error This Makefile is for release maintainers; and requires VERSION to be defined for a release.) endif # Flags for creating build archives. # These effectively tell the release tool how to modify git-archive output to create a complete build. ARCHIVE_FLAGS = \ --extra=VERSION $(HOST_PACKAGE_FLAGS) --prefix=$(PROJECT)-$(VERSION)/ # # Prepares a Facedancer release based on the VERSION arguments. # Currently, we don't yet have a RELEASENOTE filel or anything like that. # prepare_release: @mkdir -p release-files/ @echo Tagging release $(VERSION). @git tag -a v$(VERSION) $(TAG_FORCE) -m "release $(VERSION)" @echo "$(VERSION)" > VERSION @echo --- Creating our host-python distribution. @rm -rf host-packages @mkdir -p host-packages @#Build the host libraries. @$(PYTHON) setup.py sdist bdist_wheel -d host-packages @echo --- Preparing the release archives. $(eval HOST_PACKAGE_FLAGS := $(addprefix --extra=, $(wildcard host-packages/*))) @git-archive-all $(ARCHIVE_FLAGS) release-files/$(PROJECT)-$(VERSION).tar.xz @git-archive-all $(ARCHIVE_FLAGS) release-files/$(PROJECT)-$(VERSION).zip @echo @echo Archives seem to be ready in ./release-files. @echo If everything seems okay, you probably should push the relevant tag: @echo " git push origin v$(VERSION)" @echo @echo And push the relevant packages to Pypi: @echo " python3 setup.py dsit bdist_wheel register upload" ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: docs/requirements.txt ================================================ setuptools sphinx==7.2.6 sphinx_rtd_theme==2.0.0 sphinxcontrib-apidoc readthedocs-sphinx-search==0.3.2 jinja2==3.1.6 # needed to build api docs facedancer @ git+https://github.com/greatscottgadgets/facedancer ================================================ FILE: docs/source/conf.py ================================================ import os, pkg_resources, sys, time sys.path.insert(0, os.path.abspath("../../")) sys.path.insert(0, os.path.abspath('../../facedancer')) import sphinx_rtd_theme extensions = [ 'sphinx_rtd_theme' ] # -- Project information ----------------------------------------------------- project = 'Facedancer' copyright = time.strftime('2018-%Y, Great Scott Gadgets') author = 'Great Scott Gadget' version = pkg_resources.get_distribution('facedancer').version release = '' # -- General configuration --------------------------------------------------- templates_path = ['_templates'] exclude_patterns = ['_build'] source_suffix = '.rst' master_doc = 'index' language = "en" exclude_patterns = [] pygments_style = None extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'sphinxcontrib.apidoc', ] # configure extension: sphinxcontrib.apidoc apidoc_module_dir = '../../facedancer' apidoc_output_dir = 'api_docs' apidoc_excluded_paths = ['test'] apidoc_separate_modules = True # configure extension: extlinks extlinks = { 'repo': ('https://github.com/greatscottgadgets/facedancer/blob/main/%s', '%s'), 'example': ('https://github.com/greatscottgadgets/facedancer/blob/main/examples/%s', '%s'), } # configure extension: napoleon napoleon_google_docstring = True napoleon_numpy_docstring = False napoleon_include_init_with_doc = True napoleon_use_ivar = True napoleon_include_private_with_doc = False napoleon_include_special_with_doc = True napoleon_use_param = False # -- Options for HTML output ------------------------------------------------- # run pip install sphinx_rtd_theme if you get sphinx_rtd_theme errors html_theme = "sphinx_rtd_theme" html_css_files = ['status.css'] ================================================ FILE: docs/source/facedancer_examples.rst ================================================ =================== Facedancer Examples =================== .. warning:: Facedancer and GreatFET are not currently supported with Windows as the Control Host. Windows is however supported as the Target Host when using Linux or macOS for the Control Host. For more information please see the tracking issue: `#170 `__ There are a number of :repo:`Facedancer examples` available that demonstrate emulation of various USB device functions. :example:`rubber-ducky.py` -------------------------- The canonical "Hello World" of USB emulation, the rubber-ducky example implements a minimal subset of the USB HID class specification in order to emulate a USB keyboard. .. list-table:: Target Host Compatibility :widths: 30 30 30 :header-rows: 1 * - Linux - macOS - Windows * - ✅ - ✅ - ✅ :example:`ftdi-echo.py` ----------------------- An emulation of an FTDI USB-to-serial converter, the ftdi-echo example converts input received from a connected terminal to uppercase and echoes the result back to the sender. .. list-table:: Target Host Compatibility :widths: 30 30 30 :header-rows: 1 * - Linux - macOS - Windows * - ✅ - ❌ - ✅ :example:`mass-storage.py` -------------------------- An emulation of a USB Mass Storage device, the mass-storage example can take a raw disk image file as input and present it to a target host as drive that can be mounted, read and written to. You can create an empty disk image for use with the emulation using: .. code-block :: sh dd if=/dev/zero of=disk.img bs=1M count=100 mkfs -t ext4 disk.img You can also test or modify the disk image locally by mounting it with: .. code-block :: sh mount -t auto -o loop disk.img /mnt Remember to unmount it before using it with the device emulation! .. list-table:: Target Host Compatibility :widths: 30 30 30 :header-rows: 1 * - Linux - macOS - Windows * - ✅ - ✅ - ❌ ================================================ FILE: docs/source/getting_started.rst ================================================ ================================================ Getting started with Facedancer ================================================ .. warning:: Facedancer and USBProxy are not currently supported in a Control Host role on Windows with GreatFET. For more information please see the tracking issue: `#170 `__ Install the Facedancer library ------------------------------ You can install the Facedancer library from the `Python Package Index (PyPI) `__, a `release archive `__ or directly from `source `__. Install From PyPI ^^^^^^^^^^^^^^^^^ You can use the `pip `__ tool to install the Facedancer library from PyPI using the following command: .. code-block :: sh pip install facedancer For more information on installing Python packages from PyPI please refer to the `"Installing Packages" `__ section of the Python Packaging User Guide. Install From Source ^^^^^^^^^^^^^^^^^^^ .. code-block :: sh git clone https://github.com/greatscottgadgets/facedancer.git cd facedancer/ Once you have the source code downloaded you can install the Facedancer library with: .. code-block :: sh pip install . Run a Facedancer example ------------------------ Create a new Python file called `rubber-ducky.py` with the following content: .. code-block :: python import asyncio import logging from facedancer import main from facedancer.devices.keyboard import USBKeyboardDevice from facedancer.classes.hid.keyboard import KeyboardModifiers device = USBKeyboardDevice() async def type_letters(): # Wait for device to connect await asyncio.sleep(2) # Type a string with the device await device.type_string("echo hello, facedancer\n") main(device, type_letters()) Open a terminal and run: .. code-block :: sh python ./rubber-ducky.py ================================================ FILE: docs/source/howto_facedancer_backend.rst ================================================ ===================================== How to write a new Facedancer Backend ===================================== Facedancer board backends can be found in the :repo:`facedancer/backends/` directory. To create a new backend, follow these steps: 1. Derive a new backend class ----------------------------- All Facedancer board backends inherit from the ``FacedancerApp`` and ``FacedancerBackend`` classes. Begin by deriving your new backend class from these base classes, as shown below: .. code-block :: python from facedancer.core import FacedancerApp from facedancer.backends.base import FacedancerBackend class MydancerBackend(FacedancerApp, FacedancerBackend): app_name = "Mydancer" 2. Implement backend callback methods ------------------------------------- Your new backend must implement the required callback methods defined in the ``FacedancerBackend`` class. These methods contain the functionality specific to your Facedancer board: .. literalinclude:: ../../facedancer/backends/base.py :language: python :emphasize-lines: 0 :linenos: 3. Implement the backend event loop ----------------------------------- Facedancer uses a polling approach to service events originating from the Facedancer board. The actual events that need to be serviced will be specific to your Facedancer board but will generally include at least the following: * Receiving a setup packet. * Receiving data on an endpoint. * Receiving NAK events (e.g. host requested data from an IN endpoint) Facedancer will take care of scheduling execution of the ``service_irqs()`` callback but it is up to you to dispatch any events generated by your board to the corresponding methods of the Facedancer ``USBDevice`` object obtained in the ``FacedancerBackend.connect()`` callback. That said, most backend implementations will follow a pattern similiar to the pseudo-code below: .. code-block :: python class MydancerBackend(FacedancerApp, FacedancerBackend): ... def service_irqs(self): """ Core routine of the Facedancer execution/event loop. Continuously monitors the Moondancer's execution status, and reacts as events occur. """ # obtain latest events and handle them for event in self.mydancer.get_events(): match event: case USB_RECEIVE_SETUP: self.usb_device.create_request(event.data) case USB_RECEIVE_PACKET: self.usb_device.handle_data_available(event.endpoint_number, event.data) case USB_EP_IN_NAK: self.usb_device.handle_nak(event.endpoint_number) Additionally, referencing the ``service_irqs`` methods of the other backend implementations can provide valuable insights into handling events specific to your implementation. ================================================ FILE: docs/source/index.rst ================================================ ======================== Facedancer Documentation ======================== .. toctree:: :maxdepth: 2 :caption: User Documentation getting_started library_overview using_facedancer using_usb_proxy facedancer_examples .. toctree:: :maxdepth: 2 :caption: Developer Documentation howto_facedancer_backend .. toctree:: :maxdepth: 1 :caption: API Documentation api_docs/modules :ref:`genindex` | :ref:`modindex` ================================================ FILE: docs/source/library_overview.rst ================================================ ================================================ Library Overview ================================================ The Facedancer library may be somewhat overwhelming at first but the modules can be broken down into a number of clearly delineated categories: Core USB Device Model ~~~~~~~~~~~~~~~~~~~~~ These packages contain the functionality used to define devices and their organisation closely mirrors the hierarchical USB device model: .. code-block:: text +--------------------------------+ | USB Device | | - Device Descriptor | | - Configuration Descriptor | | - Interface Descriptor | +----------------------------------+ | - Endpoint Descriptor | | Host | | - Request Handler | --> | - Function | | - Endpoint Descriptor | | | | - Request Handler | <-- | - Function | | - Control Interface | | | | - Request Handlers | <-> | - Enumeration, Status, Command | +--------------------------------+ +----------------------------------+ (simplified diagram for didactic purposes, not drawn to scale) * :mod:`facedancer.device` -- :class:`~facedancer.device.USBDevice` is the device root. It is responsible for managing the device's descriptors and marshalling host requests. * :mod:`facedancer.configuration` -- :class:`~facedancer.configuration.USBConfiguration` is responsible for managing the device's configuration descriptor(s). * :mod:`facedancer.interface` -- :class:`~facedancer.interface.USBInterface` is responsible for managing the device's interface descriptor(s). * :mod:`facedancer.endpoint` -- :class:`~facedancer.endpoint.USBEndpoint` is responsible for managing the device's endpoints. * :mod:`facedancer.request` -- :class:`~facedancer.request.USBControlRequest` is responsible for managing USB control transfers. In addition to the core device model there are also two modules containing support functionality: * :mod:`facedancer.descriptor` -- contains functionality for working with USB descriptors. * :mod:`facedancer.magic` -- contains functionality for Facedancer's declarative device definition syntax. Device Emulation Support ~~~~~~~~~~~~~~~~~~~~~~~~ These modules contain a small selection of example USB device classes and device emulations. * :mod:`facedancer.classes` * :mod:`facedancer.devices` USB Proxy ~~~~~~~~~ These modules contain the USB Proxy implementation. * :mod:`facedancer.proxy` -- contains the :class:`~facedancer.proxy.USBProxyDevice` implementation. * :mod:`facedancer.filters` -- contains a selection of filters to intercept, view or modify proxied USB transfers. Facedancer Board Backends ~~~~~~~~~~~~~~~~~~~~~~~~~ Contains backend implementations for the various supported Facedancer boards. * :mod:`facedancer.backends` Supporting Functionality ~~~~~~~~~~~~~~~~~~~~~~~~ * :mod:`facedancer.core` -- the Facedancer scheduler and execution core. * :mod:`facedancer.errors` -- an error type, there should probably be more. * :mod:`facedancer.types` -- various type definitions and constants. * :mod:`facedancer.logging` -- logging boilerplate. ================================================ FILE: docs/source/using_facedancer.rst ================================================ ================================================ 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: .. literalinclude:: ../../examples/minimal.py :language: python :lines: 7- :lineno-start: 7 :linenos: Device Descriptor ----------------- The entry-point for most Facedancer emulations is the :class:`~facedancer.device.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 :class:`~facedancer.device.USBBaseDevice` class if you'd like to provide your own implementation of the standard request handlers. See, for example, :class:`~facedancer.proxy.USBProxyDevice`. Starting with the initial class declaration we can define our device as: .. code-block:: python 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 :class:`~facedancer.device.USBDevice`. We also annotate our class with the :func:`@use_inner_classes_automatically ` decorator which allows us to use a declarative style when including our devices configuration, interface and endpoints. It's :mod:`~facedancer.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 :class:`~facedancer.device.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 :class:`~facedancer.configuration.USBConfiguration` class containing at least one :class:`~facedancer.interface.USBInterface` class containing zero or more :class:`~facedancer.endpoint.USBEndpoint` class. Here we define a configuration with a single interface containing two endpoints. The first endpoint has direction :class:`~facedancer.types.USBDirection.IN` and will be responsible for responding to data requests from the host. The second endpoint has direction :class:`~facedancer.types.USBDirection.OUT` and will be responsible for receiving data from the host. .. code-block:: python :emphasize-lines: 5- ... 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 :mod:`facedancer.endpoint` and :mod:`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. .. code-block:: python :emphasize-lines: 11-13, 19-21 ... 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 :class:`~facedancer.endpoint.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: * :func:`@control_request_handler ` * :func:`@standard_request_handler ` * :func:`@vendor_request_handler ` * :func:`@class_request_handler ` * :func:`@reserved_request_handler ` The second set defines the target for the control request: * :func:`@to_device ` * :func:`@to_this_endpoint ` * :func:`@to_any_endpoint ` * :func:`@to_this_interface ` * :func:`@to_any_interface ` * :func:`@to_other ` For instance, to define some vendor request handlers you can do: .. code-block:: python :emphasize-lines: 7- ... 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 :class:`~facedancer.request.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! .. literalinclude:: ../../examples/test_minimal.py :language: python :linenos: 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: .. code-block:: shell 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: .. code-block:: text 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 :example:`annotated template ` which provides an excellent reference source when building your own devices: .. literalinclude:: ../../examples/template.py :language: python :lines: 8-276 :lineno-start: 8 :linenos: ================================================ FILE: docs/source/using_usb_proxy.rst ================================================ ================================================ Using USB Proxy ================================================ Introduction ------------ A major new feature of the newer Facedancer codebase is the ability to MITM (Meddler-In-The-Middle) USB connections -- replacing the authors' original `USBProxy `__ project. This opens up a whole new realm of applications -- including protocol analysis and live manipulation of USB packets -- and is especially useful when you don't control the software running on the Target Host (e.g. on embedded systems or games consoles). .. code-block:: text +-----------------------------------------------------------------------+ +------------+ | +--------------------------------+ +---------------------------+ | +--------------+ | | | | | | | | | | | PROXIED | | | CONTROL HOST | | FACEDANCER DEVICE | | | TARGET | | USB <------> running Facedancer software <---> acts as USB-Controlled <------> HOST | | DEVICE | | | | | USB Controller | | | | | | | | | | | | | | +------------+ | +--------------------------------+ +---------------------------+ | +--------------+ | | | MITM Setup (HOST + FACEDANCER) | +-----------------------------------------------------------------------+ The Simplest USB Proxy ---------------------- .. note:: On macOS USBProxy needs to run as root in order to claim the device being proxied from the operating system. The simplest use for USB Proxy is to transparently forward USB transactions between the target computer and the proxied device while logging them to the console. .. literalinclude:: ../../examples/usbproxy.py :language: python :lines: 7- :lineno-start: 7 :linenos: Setting up a USB Proxy begins by creating an instance of the :class:`~facedancer.proxy.USBProxyDevice` with the vendor and product id's of the proxied device as arguments. The actual behaviour of USB Proxy is governed by adding :mod:`~facedancer.filters` to the proxy that can intercept, read, modify and forward USB transactions between the target computer and proxied device. The first filter is a :class:`~facedancer.filters.standard.USBProxySetupFilters` which is a simple forwarding filter that ensures all control transfers are forwarded between the target computer and the proxied device. Without the presence of this script the target computer will detect your proxied device but all attempts at enumeration would fail. The second filter is a :class:`~facedancer.filters.logging.USBProxyPrettyPrintFilter` which will intercept all transactions and then log them to the console. Writing USB Proxy Filters ------------------------- To write your own proxy filter you'd derive a new filter from :class:`~facedancer.filters.base.USBProxyFilter` and override the request handlers for the transactions you want to intercept. For example, a simple filter to intercept and modify data from a MIDI controller could look like this: .. code-block:: python from facedancer.filters import USBProxyFilter class MyFilter(USBProxyFilter): # intercept the midi controllers IN endpoint def filter_in(self, ep_num, data): # check if the data is from the correct endpoint and a midi message if ep_num == (0x82 & 0x7f) and len(data) == 4: # check if it is a midi note-on/off message if data[1] in [0x80, 0x90]: # transpose the note up by an octave - 7f data[2] += 12 # return the endpoint number and modified data return ep_num, data Which you can then add to the proxy using :class:`~facedancer.proxy.USBProxyDevice`'s :meth:`~facedancer.proxy.USBProxyDevice.add_filter` method: .. code-block:: python # add my filter to the proxy proxy.add_filter(MyFilter()) You can find more information about the supported handlers in the :class:`~facedancer.filters.base.USBProxyFilter` documentation. ================================================ FILE: examples/coroutine.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # import asyncio import sys from facedancer import * from facedancer.errors import EndEmulation from facedancer.logging import configure_default_logging, log from minimal import MyDevice async def my_exit_handler(bindkey: bytes): """A custom exit handler that will gracefully shut down the emulation when the user presses the given key combination. """ import platform if platform.system() == "Windows": import msvcrt def get_key(): key = msvcrt.getch() # check for, and propagate Control-C if key == b'\x03': raise KeyboardInterrupt return key else: import termios, tty def get_key(): fd = sys.stdin.fileno() restore = termios.tcgetattr(fd) try: tty.setcbreak(fd) key = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, restore) return str.encode(key) while True: key = get_key() if key == bindkey: raise EndEmulation("User quit the emulation.") await asyncio.sleep(0) def my_main_function(device, *coroutines): """ A custom main function for emulating a Facedancer device. """ # Set up our logging output. configure_default_logging(level=20) # Add a custom exit handler to our coroutines. coroutines = (*coroutines, my_exit_handler(b'\x05')) # Run the relevant code, along with any added coroutines. log.info("Starting emulation, press 'Control-E' to disconnect and exit.") device.emulate(*coroutines) if __name__ == "__main__": my_main_function(MyDevice()) ================================================ FILE: examples/ftdi-echo.py ================================================ #!/usr/bin/env python3 # # This file is part of Facedancer. # import logging from facedancer import main from facedancer.devices.ftdi import FTDIDevice device = FTDIDevice() async def send_hello(): """ Waits for the host to connect, and then says hello. """ logging.info("Waiting for the host to connect.") await device.wait_for_host() logging.info("Host connected!") logging.info("Telling the user hello...") device.transmit("Hello! Welcome to the FTDI demo.\n") device.transmit("Enter any text you'd like, and we'll send it back in UPPERCASE.\n") def uppercasize(data): """ Convert any received data to uppercase. """ # Convert the data to uppercase... uppercase = data.decode('utf-8').upper() # ... convert serial line endings to Python line endings... uppercase = uppercase.replace('\r', '\n') # ... and transmit our response. device.transmit(uppercase) # Override the serial data handler by adding a singleton method on our object. # This is an easy way to create one-off objects. :) device.handle_serial_data_received = uppercasize main(device, send_hello()) ================================================ FILE: examples/hackrf-info.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # from facedancer import * from facedancer import main @use_inner_classes_automatically class HackRF(USBDevice): """ Device that emulates a HackRF enough to appear in ``hackrf_info``. You can try to create this script yourself! It's relatively easy using the --suggest option and the ``template.py`` example. """ # Show up as a HackRF. product_string : str = "HackRF One (Emulated)" manufacturer_string : str = "Great Scott Gadgets" vendor_id : int = 0x1d50 product_id : int = 0x6089 # Most hosts won't accept a device unless it has a configuration # and an interface. We'll add some default/empty ones. Facedancer # provides sane defaults, so we don't need to do anything else! class DefaultConfiguration(USBConfiguration): class DefaultInterface(USBInterface): pass # # Vendor requests. # # These templates were generated using --suggest, and then modified # by the author to get the functionality she wanted. # @vendor_request_handler(number=14, direction=USBDirection.IN) @to_device def handle_control_request_14(self, request): # The --suggest command gives us the following info: # Most recent request was for 1B of data. # Theoretically, this is the point where you'd experiment # with providing one-byte responses and see what `hackrf_info` does. request.reply([2]) # # From here on out, we'll give these requests more descriptive names, # rather than using the ones from --suggest. When creating this, we'd # theoretically do our reverse engineering, and then rename the request. # # Because the decorator indicates to the backend that this is a vendor # request handler, these names can be whatever we'd like -- and we don't # have to update anything when we change them! # @vendor_request_handler(number=15, direction=USBDirection.IN) @to_device def handle_get_version_request(self, request): # Most recent request was for 255B of data. # When hackrf_info gets to this point, we can see that it's # failing with "hackrf_version_string_read() failed: Pipe error (-1000)." # # That's a pretty good hint of what it expects. request.reply(b"Sekret Facedancer Version") @vendor_request_handler(number=18, direction=USBDirection.IN) @to_device def handle_get_serial_request(self, request): # Most recent request was for 24B of data. request.reply(b'A' * 24) # # There's one last thing to do -- we'll need to implement one more # simple request. We'll leave this last one as an exercise to the reader. :) # main(HackRF) ================================================ FILE: examples/imperative.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # """ Example for using the imperative API. """ # # The other Facedancer examples tend to use the declarative API, # as it's more succinct, and typically can be created faster. # # However, the new API still supports an imperative syntax, # which may be useful in some circumstances. # import logging from facedancer import main from facedancer import * class ImperativeDevice(USBDevice): def __init__(self): # We can still implement our types imperatively, like in the old API. super().__init__( vendor_id=0x1234, product_string="Imperatively-created Device" ) # The constructor arguments to each type accept the same fields as the declarative # API -- and like the declarative API, parameters have sane defaults... configuration = USBConfiguration() self.add_configuration(configuration) # ... which means we don't really need to do much to create the various components. interface = USBInterface() configuration.add_interface(interface) # Like the declarative APIs, endpoints require a number and direction. out_endpoint = USBEndpoint(number=3, direction=USBDirection.OUT) interface.add_endpoint(out_endpoint) # # We'll still use our request decorators to declare request handlers # on the relevant objects... # @vendor_request_handler(number=13) def handle_my_request(self, request): request.acknowledge() # # ... and callbacks continue to work the same way. # def handle_data_received(self, endpoint, data): logging.info(f"New data: {data} on {endpoint}.") main(ImperativeDevice()) ================================================ FILE: examples/mass-storage.py ================================================ #!/usr/bin/env python3 # # This file is part of Facedancer. # import sys import logging from facedancer import main from facedancer.devices.umass import RawDiskImage from facedancer.devices.umass import USBMassStorageDevice # usage instructions if len(sys.argv)==1: print("Usage: mass-storage.py disk.img") sys.exit(1) # get disk image filename and clear arguments filename = sys.argv[1] sys.argv = [sys.argv[0]] # open our disk image disk_image = RawDiskImage(filename, 512, verbose=3) # create the device device = USBMassStorageDevice(disk_image) async def hello(): """ Waits for the host to connect, and then says hello. """ logging.info("Waiting for the host to connect.") await device.wait_for_host() logging.info("Host connected!") main(device, hello()) # Creating a disk image for testing: # # dd if=/dev/zero of=disk.img bs=1M count=100 # mkfs -t ext4 disk.img # mount -t auto -o loop disk.img /mnt ================================================ FILE: examples/minimal.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # import logging from facedancer import * from facedancer import main @use_inner_classes_automatically class MyDevice(USBDevice): product_string : str = "Example USB Device" manufacturer_string : str = "Facedancer" vendor_id : int = 0x1209 product_id : int = 0x0001 device_speed : DeviceSpeed = DeviceSpeed.FULL class MyConfiguration(USBConfiguration): class MyInterface(USBInterface): class MyInEndpoint(USBEndpoint): number : int = 1 direction : USBDirection = USBDirection.IN max_packet_size : int = 64 def handle_data_requested(self: USBEndpoint): logging.info("handle_data_requested") self.send(b"device on bulk endpoint") class MyOutEndpoint(USBEndpoint): number : int = 1 direction : USBDirection = USBDirection.OUT max_packet_size : int = 64 def handle_data_received(self: USBEndpoint, data): logging.info(f"device received {data} on bulk endpoint") @vendor_request_handler(number=1, direction=USBDirection.IN) @to_device def my_in_vendor_request_handler(self: USBDevice, request: USBControlRequest): logging.info("my_in_vendor_request_handler") request.reply(b"device on control endpoint") @vendor_request_handler(number=2, direction=USBDirection.OUT) @to_device def my_out_vendor_request_handler(self: USBDevice, request: USBControlRequest): logging.info(f"device received {request.index} {request.value} {bytes(request.data)} on control endpoint") request.ack() if __name__ == "__main__": main(MyDevice) ================================================ FILE: examples/rubber-ducky.py ================================================ #!/usr/bin/env python3 # # This file is part of Facedancer. # """ USB 'Rubber Ducky' example; enters some text via the keyboard module. """ import asyncio import logging from facedancer import main from facedancer.devices.keyboard import USBKeyboardDevice from facedancer.classes.hid.keyboard import KeyboardModifiers device = USBKeyboardDevice() async def type_letters(): logging.info("Beginning message typing demo...") await asyncio.sleep(2) await device.type_string("echo Hello, Facedancer!\n") logging.info("Typing complete. Idly handling USB requests.") main(device, type_letters()) ================================================ FILE: examples/template.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # """ Example template for creating new Facedancer devices. """ import logging from facedancer import main from facedancer import * from facedancer.classes import USBDeviceClass @use_inner_classes_automatically class TemplateDevice(USBDevice): """ This class is meant to act as a template to help you get acquainted with Facedancer.""" # # Core 'dataclass' definitions. # These define the basic way that a Facedancer device advertises itself to the host. # # Every one of these is optional. The defaults are relatively sane, so you can mostly # ignore these unless you want to change them! See the other examples for more minimal # data definitions. # # The USB device class, subclass, and protocol for the given device. # Often, we'll leave these all set to 0, which means the actual class is read # from the interface. # # Note that we _need_ the type annotations on these. Without them, Python doesn't # consider them valid dataclass members, and ignores them. (This is a detail of python3.7+ # dataclasses.) # device_class : int = 0 device_subclass : int = 0 protocol_revision_number : int = 0 # The maximum packet size on EP0. For most devices, the default value of 64 is fine. max_packet_size_ep0 : int = 64 # The vendor ID and product ID that we want to give our device. vendor_id : int = 0x610b product_id : int = 0x4653 # The string descriptors we'll provide for our device. # Note that these should be Python strings, and _not_ bytes. manufacturer_string : str = "Facedancer" product_string : str = "Generic USB Device" serial_number_string : str = "S/N 3420E" # This tuple is a list of languages we're choosing to support. # This gives us an opportunity to provide strings in various languages. # We don't typically use this; so we can leave this set to a language of # your choice. supported_languages : tuple = (LanguageIDs.ENGLISH_US,) # The revision of the device hardware. This doesn't matter to the USB specification, # but it's sometimes read by drivers. 0x0001 represents "0.1" in BCD. device_revision : int = 0x0001 # The revision of the USB specification that this device adheres to. # Typically, you'll leave this at 0x0200 which represents "2.0" in BCD. usb_spec_version : int = 0x0200 # # We'll define a single configuration on our device. To be compliant, # every device needs at least a configuration and an interface. # # Note that we don't need to do anything special to have this be used. # As long as we're using the @use_inner_classes_automatically decorator, # this configuration will automatically be instantiated and used. # class TemplateConfiguration(USBConfiguration): # # Configuration fields. # # Again, all of these are optional; and the default values # are sane and useful. # # Configuration number. Every configuration should have a unique # number, which should count up from one. Note that a configuration # shouldn't have a number of 0, as that's USB for "unconfigured". configuration_number : int = 1 # A simple, optional descriptive name for the configuration. If provided, # this is referenced in the configuration's descriptor. configuration_string : str = None # This setting is set to true if the device can run without bus power, # or false if it pulls its power from the USB bus. self_powered : bool = False # This setting is set to true if the device can ask that the host # wake it up from sleep. If set to true, the host may choose to # leave power on to the device when the host is suspended. supports_remote_wakeup : bool = True # The maximum power the device will use in this configuration, in mA. # Typically, most devices will request 500mA, the maximum allowed. max_power : int = 500 class TemplateInterface(USBInterface): # # Interface fields. # Again, all optional and with useful defaults. # # The interface index. Each interface should have a unique index, # starting from 0. number : int = 0 # The information about the USB class implemented by this interface. # This is the place where you'd specify if this is e.g. a HID device. class_number : int = USBDeviceClass.VENDOR_SPECIFIC subclass_number : int = 0 protocol_number : int = 0 # A short description of the interface. Optional and typically only informational. interface_string : str = None # # Here's where we define any endpoints we want to add to the device. # These behave essentially the same way as the above. # class TemplateInEndpoint(USBEndpoint): # # Endpoints are unique in that they have two _required_ # properties -- their number and direction. # # Together, these two fields form the endpoint's address. # Endpoint numbers should be > 0, since endpoint 0 is reserved as the default pipe by the spec. number : int = 1 direction : USBDirection = USBDirection.IN # # The remainder of the fields are optional and have useful defaults. # # The transfer type selects how data will be transferred over the endpoints. # The currently supported types are BULK and INTERRUPT. transfer_type : USBTransferType = USBTransferType.BULK # The maximum packet size determines how large packets are allowed to be. # For a full speed device, a max-size value of 64 is typical. max_packet_size : int = 64 # For interrupt endpoints, the interval specifies how often the host should # poll the endpoint, in milliseconds. 10ms is a typical value. interval : int = 0 # # Let's add an event handler. This one is called whenever the host # wants to read data from the device. # def handle_data_requested(self): # We can reply to this request using the .send() method on this # endpoint, like so: self.send(b"Hello!") # We can also get our parent interface using .parent; # or a reference to our device using .get_device(). class TemplateOutEndpoint(USBEndpoint): # # We'll use a more typical set of properties for our OUT endpoint. # number : int = 1 direction : USBDirection = USBDirection.OUT # # We'll also demonstrate use of another event handler. # This one is called whenever data is sent to this endpoint. # def handle_data_received(self, data): logging.info(f"Received data: {data}") # # Any of our components can use callback functions -- not just our endpoints! # The callback names are the same no matter where we use them. # def handle_data_received(self, endpoint, data): # # When using a callback on something other than an endpoint, our function's # signature is slightly different -- it takes the relevant endpoint as an # argument, as well. # # We'll delegate this back to the core handler, here, so it propagates to our subordinate # endpoints -- but we don't have to! If we wanted to, we could call functions on the # endpoint itself. This is especially useful if we're hooking handle_data_requested(), # where we can use endpoint.send() to provide the relevant data. super().handle_data_received(endpoint, data) # Note that non-endpoints have a get_endpoint() method, which you can use to get references # to endpoints by their endpoint numbers / directions. This is useful if you want to # send something on another endpoint in response to data received. # # The device also has a .send() method, which accepts an endpoint number and the data to # be sent. This is equivalent to calling .send() on the relevant endpoint. # # We can very, very easily add request handlers to our devices. # @vendor_request_handler(number=12) def handle_my_request(self, request): # # By decorating this function with "vendor_request_handler", we've ensured this # function is called to handle vendor request 12. We can also add other arguments to # the vendor_request_handler function -- it'll accept a keyword argument for every # property on the request. If you provide these, the handler will only be called # if the request matches the relevant constraint. # # For example, @vendor_request_handler(number=14, direction=USBDirection.IN, index_low=3) # means the decorated function is only called to handle vendor request 14 for IN requests # where the low byte of the index is 3. # # Other handler decorators exist -- like "class_request_handler" or "standard_request_handler" # # Replying to an IN request is easy -- you just provide the reply data using request.reply(). request.reply(b"Hello, there!") @vendor_request_handler(number=1, direction=USBDirection.OUT) @to_device def handle_another_request(self, request): # # Another set of convenience decorators exist to refine requests. # Decorators like `to_device` or `to_any_endpoint` chain with our # request decorators, and are syntax sugar for having an argument like # ``recipient=USBRequestRecipient.DEVICE`` in the handler decorator. # # For out requests, in lieu of a response, we typically want to acknowledge # the request. This can be accomplished by calling .acknowledge() or .ack() # on the request. request.ack() # Of course, if we want to let the host know we can't handle a request, we # may also choose to stall it. This is as simple as calling request.stall(). # # Note that request handlers can be used on configurations, interfaces, and # endpoints as well. For the latter two cases, the decorators `to_this_interface` # and `to_this_endpoint` are convenient -- they tell a request to run only if # it's directed at that endpoint in particular, as selected by its ``index`` parameter. # # Facedancer ships with a default main() function that you can use to set up and run # your device. It ships with some nice features -- including a ``--suggest`` function # that can suggest pieces of boilerplate code that might be useful in device emulation. # # main() will accept either the type of device to emulate, or an device instance. # It'll also accept asyncio coroutines, in case you want to run things alongside the # relevant device code. See e.g. `examples/rubber-ducky.py` for an example. # main(TemplateDevice) # # Of course, this template looks verbose as heck. # For an example that's much less verbose, check out `examples/hackrf-info.py`. # ================================================ FILE: examples/test_minimal.py ================================================ import logging def main(): import asyncio import usb1 VENDOR_REQUEST = 0x65 MAX_TRANSFER_SIZE = 64 with usb1.USBContext() as context: #logging.info("Host: waiting for device to connect") #await asyncio.sleep(1) device_handle = context.openByVendorIDAndProductID(0x1209, 0x0001) if device_handle is None: raise Exception("device not found") device_handle.claimInterface(0) # test IN endpoint logging.info("Testing bulk IN endpoint") response = device_handle.bulkRead( endpoint = 0x81, length = MAX_TRANSFER_SIZE, timeout = 1000, ) logging.info(f"[host] received '{response}' from bulk endpoint") print("") # test OUT endpoint logging.info("Testing bulk OUT endpoint") response = device_handle.bulkWrite( endpoint = 0x01, data = b"host say oh hai on bulk endpoint", timeout = 1000, ) print(f"sent {response} bytes\n") # test IN vendor request handler logging.info("Testing IN control transfer") response = device_handle.controlRead( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 1, index = 2, value = 3, length = MAX_TRANSFER_SIZE, timeout = 1000, ) logging.info(f"[host] received '{response}' from control endpoint") print("") # test OUT vendor request handler logging.info("Testing OUT control transfer") response = device_handle.controlWrite( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 2, index = 3, value = 4, data = b"host say oh hai on control endpoint", timeout = 1000, ) print(f"sent {response} bytes\n") if __name__ == "__main__": logging.getLogger().setLevel(logging.DEBUG) main() ================================================ FILE: examples/usbproxy.py ================================================ #!/usr/bin/env python3 # # This file is part of Facedancer. # """ USB Proxy example; forwards all USB transactions and logs them to the console. """ from facedancer import * from facedancer import main from facedancer.proxy import USBProxyDevice from facedancer.filters import USBProxySetupFilters, USBProxyPrettyPrintFilter # replace with the proxied device's information ID_VENDOR=0x09e8 ID_PRODUCT=0x0031 if __name__ == "__main__": # create a USB Proxy Device proxy = USBProxyDevice(idVendor=ID_VENDOR, idProduct=ID_PRODUCT) # add a filter to forward control transfers between the target host and # proxied device proxy.add_filter(USBProxySetupFilters(proxy, verbose=0)) # add a filter to log USB transactions to the console proxy.add_filter(USBProxyPrettyPrintFilter(verbose=5)) main(proxy) ================================================ FILE: facedancer/__init__.py ================================================ # Standard types. from .device import USBDevice from .configuration import USBConfiguration from .interface import USBInterface from .endpoint import USBEndpoint from .descriptor import USBDescriptor, USBClassDescriptor, USBDescriptorTypeNumber, StringRef # Control request handlers. from .request import standard_request_handler, class_request_handler, vendor_request_handler from .request import to_device, to_this_endpoint, to_this_interface, to_other from .request import to_any_endpoint, to_any_interface from .request import USBControlRequest # Raw types. from .types import USBDirection, USBTransferType, USBUsageType, USBSynchronizationType from .types import USBRequestType, USBRequestRecipient, USBStandardRequests, LanguageIDs from .types import DeviceSpeed # Decorators. from .magic import use_automatically, use_inner_classes_automatically from .descriptor import include_in_config, requestable # Alias objects to make them easier to import. from .backends import * from .core import FacedancerUSBApp, FacedancerUSBHostApp, FacedancerBasicScheduler from .devices import default_main as main # Wildcard import. __all__ = [ 'USBDevice', 'USBConfiguration', 'USBInterface', 'USBEndpoint', 'USBDescriptor', 'USBClassDescriptor', 'USBDescriptorTypeNumber', 'standard_request_handler', 'class_request_handler', 'vendor_request_handler', 'to_device', 'to_this_endpoint', 'to_any_endpoint', 'to_this_interface', 'to_any_interface', 'to_other', 'USBDirection', 'USBTransferType', 'USBUsageType', 'USBSynchronizationType', 'USBRequestType', 'USBRequestRecipient', 'USBStandardRequests', 'LanguageIDs', 'DeviceSpeed', 'use_automatically', 'use_inner_classes_automatically', 'USBControlRequest', 'include_in_config', 'requestable', 'StringRef', ] ================================================ FILE: facedancer/backends/MAXUSBApp.py ================================================ # MAXUSBApp.py # # Contains class definition for MAXUSBApp. import time from ..core import FacedancerApp from .base import FacedancerBackend class MAXUSBApp(FacedancerApp, FacedancerBackend): app_name = "MAXUSB" reg_ep0_fifo = 0x00 reg_ep1_out_fifo = 0x01 reg_ep2_in_fifo = 0x02 reg_ep3_in_fifo = 0x03 reg_setup_data_fifo = 0x04 reg_ep0_byte_count = 0x05 reg_ep1_out_byte_count = 0x06 reg_ep2_in_byte_count = 0x07 reg_ep3_in_byte_count = 0x08 reg_ep_stalls = 0x09 reg_clr_togs = 0x0a reg_endpoint_irq = 0x0b reg_endpoint_interrupt_enable = 0x0c reg_usb_irq = 0x0d reg_usb_interrupt_enable = 0x0e reg_usb_control = 0x0f reg_cpu_control = 0x10 reg_pin_control = 0x11 reg_revision = 0x12 reg_function_address = 0x13 reg_io_pins = 0x14 # bitmask values for reg_endpoint_irq = 0x0b is_setup_data_avail = 0x20 # SUDAVIRQ is_in3_buffer_avail = 0x10 # IN3BAVIRQ is_in2_buffer_avail = 0x08 # IN2BAVIRQ is_out1_data_avail = 0x04 # OUT1DAVIRQ is_out0_data_avail = 0x02 # OUT0DAVIRQ is_in0_buffer_avail = 0x01 # IN0BAVIRQ # bitmask values for reg_usb_control = 0x0f usb_control_vbgate = 0x40 usb_control_connect = 0x08 # bitmask values for reg_pin_control = 0x11 interrupt_level = 0x08 full_duplex = 0x10 ep0_in_nak = (1 << 5) ep2_in_nak = (1 << 6) ep3_in_nak = (1 << 7) # TODO: Support a generic MaxUSB interface that doesn't # depend on any GoodFET details. @staticmethod def bytes_as_hex(b, delim=" "): return delim.join(["%02x" % x for x in b]) # HACK: but given the limitations of the MAX chips, it seems necessary def send_on_endpoint(self, ep_num, data, blocking=False): if ep_num == 0: fifo_reg = self.reg_ep0_fifo bc_reg = self.reg_ep0_byte_count elif ep_num == 2: fifo_reg = self.reg_ep2_in_fifo bc_reg = self.reg_ep2_in_byte_count elif ep_num == 3: fifo_reg = self.reg_ep3_in_fifo bc_reg = self.reg_ep3_in_byte_count else: raise ValueError('endpoint ' + str(ep_num) + ' not supported') # FIFO buffer is only 64 bytes, must loop while len(data) > 64: self.write_bytes(fifo_reg, data[:64]) self.write_register(bc_reg, 64, ack=True) data = data[64:] self.write_bytes(fifo_reg, data) self.write_register(bc_reg, len(data), ack=True) if self.verbose > 1: print(self.app_name, "wrote", self.bytes_as_hex(data), "to endpoint", ep_num) # HACK: but given the limitations of the MAX chips, it seems necessary def read_from_endpoint(self, ep_num): if ep_num != 1: return b'' byte_count = self.read_register(self.reg_ep1_out_byte_count) if byte_count == 0: return b'' data = self.read_bytes(self.reg_ep1_out_fifo, byte_count) if self.verbose > 1: print(self.app_name, "read", self.bytes_as_hex(data), "from endpoint", ep_num) return data def stall_endpoint(self, ep_number, direction=0): """ Stalls an arbitrary endpoint. Args: ep_number : The endpoint number to be stalled direction : 0 for out, 1 for in """ if self.verbose > 0: print(self.app_name, "stalling endpoint {}".format(ep_number)) # TODO: Verify our behavior, here. The original facedancer code stalls # EP0 both _in_ and out, as well as uses the special STALL SETUP bit. # Is this really what we want? if ep_number == 0: self.write_register(self.reg_ep_stalls, 0x23) elif ep_number < 4: self.write_writer(self.reg_ep_stalls, 1 << (ep_num + 1)) else: raise ValueError("Invalid endpoint for MAXUSB device!") def stall_ep0(self, direction=0): return self.stall_endpoint(0, direction) def get_version(self): return self.read_register(self.reg_revision) def connect(self, usb_device, max_packet_size_ep0=64, device_speed=None): if self.read_register(self.reg_usb_control) & self.usb_control_connect: self.write_register(self.reg_usb_control, self.usb_control_vbgate) time.sleep(.1) self.write_register(self.reg_usb_control, self.usb_control_vbgate | self.usb_control_connect) self.connected_device = usb_device if self.verbose > 0: print(self.app_name, "connected device", self.connected_device.name) def disconnect(self): self.write_register(self.reg_usb_control, self.usb_control_vbgate) if self.verbose > 0: print(self.app_name, "disconnected device", self.connected_device.name) self.connected_device = None def clear_irq_bit(self, reg, bit): self.write_register(reg, bit) def service_irqs(self): irq = self.read_register(self.reg_endpoint_irq) in_nak = self.read_register(self.reg_pin_control) if self.verbose > 3: print(self.app_name, "read endpoint irq: 0x%02x" % irq) print(self.app_name, "read pin control: 0x%02x" % in_nak) if self.verbose > 2: if irq & ~ (self.is_in0_buffer_avail \ | self.is_in2_buffer_avail | self.is_in3_buffer_avail): print(self.app_name, "notable irq: 0x%02x" % irq) if irq & self.is_setup_data_avail: self.clear_irq_bit(self.reg_endpoint_irq, self.is_setup_data_avail) b = self.read_bytes(self.reg_setup_data_fifo, 8) if (irq & self.is_out0_data_avail) and (b[0] & 0x80 == 0x00): data_bytes_len = b[6] + (b[7] << 8) b += self.read_bytes(self.reg_ep0_fifo, data_bytes_len) req = self.connected_device.create_request(b) self.connected_device.handle_request(req) if irq & self.is_out1_data_avail: data = self.read_from_endpoint(1) if data: self.connected_device.handle_data_available(1, data) self.clear_irq_bit(self.reg_endpoint_irq, self.is_out1_data_avail) if irq & self.is_in2_buffer_avail: self.connected_device.handle_buffer_available(2) if irq & self.is_in3_buffer_avail: self.connected_device.handle_buffer_available(3) # Check to see if we've NAK'd on either of our IN endpoints, # and generate the relevant events. if in_nak & self.ep2_in_nak: self.connected_device.handle_nak(2) self.clear_irq_bit(self.reg_pin_control, in_nak | self.ep2_in_nak) if in_nak & self.ep3_in_nak: self.connected_device.handle_nak(3) self.clear_irq_bit(self.reg_pin_control, in_nak | self.ep3_in_nak) def set_address(self, address, defer=False): """ Sets the device address of the Facedancer. Usually only used during initial configuration. Args: address : The address that the Facedancer should assume. """ # The MAXUSB chip handles this for us, so we don't need to do anything. pass def configured(self, configuration): """ Callback that's issued when a USBDevice is configured, e.g. by the SET_CONFIGURATION request. Allows us to apply the new configuration. Args: configuration : The configuration applied by the SET_CONFIG request. """ self.validate_configuration(configuration) # For the MAXUSB case, we don't need to do anything, though it might # be nice to print a message or store the active configuration for # use by the USBDevice, etc. etc. ================================================ FILE: facedancer/backends/__init__.py ================================================ __all__ = [ "goodfet", "MAXUSBApp", "greatdancer", "raspdancer", "greathost", "libusbhost", "moondancer", "hydradancer" ] ================================================ FILE: facedancer/backends/base.py ================================================ from typing import List from .. import * class FacedancerBackend: def __init__(self, device: USBDevice=None, verbose: int=0, quirks: List[str]=[]): """ Initializes the backend. Args: device : The device that will act as our Facedancer. (Optional) verbose : The verbosity level of the given application. (Optional) quirks : List of USB platform quirks. (Optional) """ raise NotImplementedError @classmethod def appropriate_for_environment(cls, backend_name: str) -> bool: """ Determines if the current environment seems appropriate for using this backend. Args: backend_name : Backend name being requested. (Optional) """ raise NotImplementedError def get_version(self): """ Returns information about the active Facedancer version. """ raise NotImplementedError def connect(self, usb_device: USBDevice, max_packet_size_ep0: int=64, device_speed: DeviceSpeed=DeviceSpeed.FULL): """ Prepares backend to connect to the target host and emulate a given device. Args: usb_device : The USBDevice object that represents the emulated device. max_packet_size_ep0 : Max packet size for control endpoint. device_speed : Requested usb speed for the Facedancer board. """ raise NotImplementedError def disconnect(self): """ Disconnects Facedancer from the target host. """ raise NotImplementedError def reset(self): """ Triggers the Facedancer to handle its side of a bus reset. """ raise NotImplementedError def set_address(self, address: int, defer: bool=False): """ Sets the device address of the Facedancer. Usually only used during initial configuration. Args: address : The address the Facedancer should assume. defer : True iff the set_address request should wait for an active transaction to finish. """ raise NotImplementedError def configured(self, configuration: USBConfiguration): """ Callback that's issued when a USBDevice is configured, e.g. by the SET_CONFIGURATION request. Allows us to apply the new configuration. Args: configuration : The USBConfiguration object applied by the SET_CONFIG request. """ raise NotImplementedError def read_from_endpoint(self, endpoint_number: int) -> bytes: """ Reads a block of data from the given endpoint. Args: endpoint_number : The number of the OUT endpoint on which data is to be rx'd. """ raise NotImplementedError def send_on_control_endpoint(self, endpoint_number: int, in_request: USBControlRequest, data: bytes, blocking: bool=True): """ Sends a collection of USB data in response to a IN control request by the host. Args: endpoint_number : The number of the IN endpoint on which data should be sent. in_request : The control request being responded to. data : The data to be sent. blocking : If true, this function should wait for the transfer to complete. """ # Truncate data to requested length and forward to `send_on_endpoint()` for backends # that do not need to support this method. return self.send_on_endpoint(endpoint_number, data[:in_request.length], blocking) def send_on_endpoint(self, endpoint_number: int, data: bytes, blocking: bool=True): """ Sends a collection of USB data on a given endpoint. Args: endpoint_number : The number of the IN endpoint on which data should be sent. data : The data to be sent. blocking : If true, this function should wait for the transfer to complete. """ raise NotImplementedError def ack_status_stage(self, direction: USBDirection=USBDirection.OUT, endpoint_number:int =0, blocking: bool=False): """ Handles the status stage of a correctly completed control request, by priming the appropriate endpoint to handle the status phase. Args: direction : Determines if we're ACK'ing an IN or OUT vendor request. (This should match the direction of the DATA stage.) endpoint_number : The endpoint number on which the control request occurred. blocking : True if we should wait for the ACK to be fully issued before returning. """ def stall_endpoint(self, endpoint_number:int, direction: USBDirection=USBDirection.OUT): """ Stalls the provided endpoint, as defined in the USB spec. Args: endpoint_number : The number of the endpoint to be stalled. """ raise NotImplementedError def clear_halt(self, endpoint_number:int, direction: USBDirection): """ Clears a halt condition on the provided non-control endpoint. Args: endpoint_number : The endpoint number direction : The endpoint direction; or OUT if not provided. """ # FIXME do nothing as only the moondancer backend supports this for now # raise NotImplementedError pass def service_irqs(self): """ Core routine of the Facedancer execution/event loop. Continuously monitors the Facedancer's execution status, and reacts as events occur. """ raise NotImplementedError def validate_configuration(self, configuration: USBConfiguration): """ Check if this backend is able to support this configuration. Raises an exception if it is not. Args: configuration : The configuration to validate. """ if configuration is None: return # Currently, endpoints are only set up in the configured() method, and # cannot be changed on the fly by SET_INTERFACE requests. # # Therefore, no backends are able to support configurations which # re-use endpoint addresses between alternate interface settings. used_addresses = set() for interface in configuration.get_interfaces(): for endpoint in interface.get_endpoints(): address = endpoint.get_identifier() if address in used_addresses: raise Exception( f"This configuration cannot currently be supported, " f"because it re-uses endpoint address 0x{address:02X} " f"between multiple interface definitions.") used_addresses.add(address) ================================================ FILE: facedancer/backends/goodfet.py ================================================ import os import serial import sys import time from ..core import FacedancerApp from ..backends.MAXUSBApp import MAXUSBApp from ..logging import log class GoodfetMaxUSBApp(MAXUSBApp): app_name = "MAXUSB" app_num = 0x40 @classmethod def appropriate_for_environment(cls, backend_name): """ Determines if the current environment seems appropriate for using the GoodFET::MaxUSB backend. """ # Check: Only proceed if the user has specified "goodfet" as # the backend name, we don't want a false positive because the # user has another USB serial device connected. if backend_name is None or backend_name != "goodfet": return False # See if there's a connected GoodFET. try: gf = GoodFETSerialPort() gf.close() return True except ImportError: log.info("Skipping GoodFET-based devices, as pyserial isn't installed.") return False except: return False def __init__(self, device=None, verbose=0, quirks=None): if device is None: serial = GoodFETSerialPort() device = Facedancer(serial, verbose=verbose) FacedancerApp.__init__(self, device, verbose) self.connected_device = None self.enable() if verbose > 0: rev = self.read_register(self.reg_revision) print(self.app_name, "revision", rev) # set duplex and negative INT level (from GoodFEDMAXUSB.py) self.write_register(self.reg_pin_control, self.full_duplex | self.interrupt_level) def init_commands(self): self.read_register_cmd = FacedancerCommand(self.app_num, 0x00, b'') self.write_register_cmd = FacedancerCommand(self.app_num, 0x00, b'') self.enable_app_cmd = FacedancerCommand(self.app_num, 0x10, b'') self.ack_cmd = FacedancerCommand(self.app_num, 0x00, b'\x01') def enable(self): for i in range(3): self.device.writecmd(self.enable_app_cmd) self.device.readcmd() if self.verbose > 0: print(self.app_name, "enabled") def ack_status_stage(self, blocking=False): if self.verbose > 5: print(self.app_name, "sending ack!") self.device.writecmd(self.ack_cmd) self.device.readcmd() def read_register(self, reg_num, ack=False): if self.verbose > 1: print(self.app_name, "reading register 0x%02x" % reg_num) self.read_register_cmd.data = bytearray([ reg_num << 3, 0 ]) if ack: self.read_register_cmd.data[0] |= 1 self.device.writecmd(self.read_register_cmd) resp = self.device.readcmd() if self.verbose > 2: print(self.app_name, "read register 0x%02x has value 0x%02x" % (reg_num, resp.data[1])) return resp.data[1] def write_register(self, reg_num, value, ack=False): if self.verbose > 2: print(self.app_name, "writing register 0x%02x with value 0x%02x" % (reg_num, value)) self.write_register_cmd.data = bytearray([ (reg_num << 3) | 2, value ]) if ack: self.write_register_cmd.data[0] |= 1 self.device.writecmd(self.write_register_cmd) self.device.readcmd() def read_bytes(self, reg, n): if self.verbose > 2: print(self.app_name, "reading", n, "bytes from register", reg) data = bytes([ (reg << 3) ] + ([0] * n)) cmd = FacedancerCommand(self.app_num, 0x00, data) self.device.writecmd(cmd) resp = self.device.readcmd() if self.verbose > 3: print(self.app_name, "read", len(resp.data) - 1, "bytes from register", reg) return resp.data[1:] def write_bytes(self, reg, data): data = bytes([ (reg << 3) | 3 ]) + data cmd = FacedancerCommand(self.app_num, 0x00, data) self.device.writecmd(cmd) self.device.readcmd() # null response if self.verbose > 3: print(self.app_name, "wrote", len(data) - 1, "bytes to register", reg) class Facedancer: def __init__(self, serialport, verbose=0): self.serialport = serialport self.verbose = verbose self.reset() self.monitor_app = GoodFETMonitorApp(self, verbose=self.verbose) self.monitor_app.announce_connected() def halt(self): self.serialport.setRTS(1) self.serialport.setDTR(1) def reset(self): if self.verbose > 1: print("Facedancer resetting...") self.halt() self.serialport.setDTR(0) c = self.readcmd() if self.verbose > 0: print("Facedancer reset") def read(self, n): """Read raw bytes.""" b = self.serialport.read(n) if self.verbose > 3: print("Facedancer received", len(b), "bytes;", self.serialport.inWaiting(), "bytes remaining") if self.verbose > 2: print("Facedancer Rx:", MAXUSBApp.bytes_as_hex(b)) return b def readcmd(self): """Read a single command.""" b = self.read(4) app = b[0] verb = b[1] n = b[2] + (b[3] << 8) if n > 0: data = self.read(n) else: data = b'' if len(data) != n: raise ValueError('Facedancer expected ' + str(n) \ + ' bytes but received only ' + str(len(data))) cmd = FacedancerCommand(app, verb, data) if self.verbose > 1: print("Facedancer Rx command:", cmd) return cmd def write(self, b): """Write raw bytes.""" if self.verbose > 2: print("Facedancer Tx:", MAXUSBApp.bytes_as_hex(b)) self.serialport.write(b) def writecmd(self, c): """Write a single command.""" self.write(c.as_bytestring()) if self.verbose > 1: print("Facedancer Tx command:", c) class FacedancerCommand: def __init__(self, app=None, verb=None, data=None): self.app = app self.verb = verb self.data = data def __str__(self): s = "app 0x%02x, verb 0x%02x, len %d" % (self.app, self.verb, len(self.data)) if len(self.data) > 0: s += ", data " + MAXUSBApp.bytes_as_hex(self.data) return s def long_string(self): s = "app: " + str(self.app) + "\n" \ + "verb: " + str(self.verb) + "\n" \ + "len: " + str(len(self.data)) if len(self.data) > 0: try: s += "\n" + self.data.decode("utf-8") except UnicodeDecodeError: s += "\n" + MAXUSBApp.bytes_as_hex(self.data) return s def as_bytestring(self): n = len(self.data) b = bytearray(n + 4) b[0] = self.app b[1] = self.verb b[2] = n & 0xff b[3] = n >> 8 b[4:] = self.data return b class GoodFETMonitorApp(FacedancerApp): app_name = "GoodFET monitor" app_num = 0x00 def read_byte(self, addr): d = [ addr & 0xff, addr >> 8 ] cmd = FacedancerCommand(self.app_num, 2, d) self.device.writecmd(cmd) resp = self.device.readcmd() return resp.data[0] def get_infostring(self): return bytes([ self.read_byte(0xff0), self.read_byte(0xff1) ]) def get_clocking(self): return bytes([ self.read_byte(0x57), self.read_byte(0x56) ]) def print_info(self): infostring = self.get_infostring() clocking = self.get_clocking() print("MCU", MAXUSBApp.bytes_as_hex(infostring, delim="")) print("clocked at", MAXUSBApp.bytes_as_hex(clocking, delim="")) def list_apps(self): cmd = FacedancerCommand(self.app_num, 0x82, b'\x01') self.device.writecmd(cmd) resp = self.device.readcmd() print("build date:", resp.data.decode("utf-8")) print("firmware apps:") while True: resp = self.device.readcmd() if len(resp.data) == 0: break print(resp.data.decode("utf-8")) def echo(self, s): b = bytes(s, encoding="utf-8") cmd = FacedancerCommand(self.app_num, 0x81, b) self.device.writecmd(cmd) resp = self.device.readcmd() return resp.data == b def announce_connected(self): cmd = FacedancerCommand(self.app_num, 0xb1, b'') self.device.writecmd(cmd) resp = self.device.readcmd() def GoodFETSerialPort(**kwargs): "Return a Serial port using default values possibly overriden by caller" port = os.environ.get('GOODFET') or "/dev/ttyUSB0" args = dict(port=port, baudrate=115200, parity=serial.PARITY_NONE, timeout=2) args.update(kwargs) return serial.Serial(**args) ================================================ FILE: facedancer/backends/greatdancer.py ================================================ # GreatDancerApp.py import sys import time import codecs import traceback from ..core import * from ..types import * from ..logging import log from .base import FacedancerBackend class GreatDancerApp(FacedancerApp, FacedancerBackend): """ Backend for using GreatFET devices as Facedancers. """ app_name = "GreatDancer" 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_NAKI = (1 << 16) # Number of supported USB endpoints. # TODO: bump this up when we develop support using USB0 (cables flipped) SUPPORTED_ENDPOINTS = 4 # USB directions HOST_TO_DEVICE = 0 DEVICE_TO_HOST = 1 # Get status command indexes GET_USBSTS = 0 GET_ENDPTSETUPSTAT = 1 GET_ENDPTCOMPLETE = 2 GET_ENDPTSTATUS = 3 GET_ENDPTNAK = 4 # Quirk flags QUIRK_MANUAL_SET_ADDRESS = 0x01 @classmethod def appropriate_for_environment(cls, backend_name): """ Determines if the current environment seems appropriate for using the GreatDancer backend. """ # Check: if we have a backend name other than greatfet, # the user is trying to use something else. Abort! if backend_name and backend_name != "greatfet": return False # If we're not explicitly trying to use something else, # see if there's a connected GreatFET. try: import greatfet gf = greatfet.GreatFET() return gf.supports_api('greatdancer') except ImportError: log.info("Skipping GreatFET-based devices, as the greatfet python module isn't installed.") return False except: return False def __init__(self, device=None, verbose=0, quirks=None): """ Sets up a new GreatFET-backed Facedancer (GreatDancer) application. device: The GreatFET device that will act as our GreatDancer. verbose: The verbosity level of the given application. """ import greatfet if device is None: device = greatfet.GreatFET() self.device = device self.device.comms.get_exclusive_access() FacedancerApp.__init__(self, device, verbose) self.connected_device = None # Grab the raw API object from the GreatFET object. # This has the low-level RPCs used for raw USB control. self.api = self.device.apis.greatdancer # Initialize a dictionary that will store the last setup # whether each endpoint is currently stalled. self.endpoint_stalled = {} for i in range(self.SUPPORTED_ENDPOINTS): self.endpoint_stalled[i] = False # Assume a max packet size of 64 until configured otherwise. self.max_packet_size_ep0 = 64 # Start off by assuming we're not waiting for an OUT control transfer's # data stage. # See _handle_setup_complete_on_endpoint for details. self.pending_control_request = None # Store a reference to the device's active configuration, # which we'll use to know which endpoints we'll need to check # for data transfer readiness. self.configuration = None # # Store our list of quirks to handle. # if quirks: self.quirks = quirks else: self.quirks = [] def init_commands(self): """ API compatibility function; not necessary for GreatDancer. """ pass def get_version(self): """ Returns information about the active GreatDancer version. """ # TODO: Return the GreatFET software version, or something indicating # the GreatFET API number? raise NotImplementedError() def ack_status_stage(self, direction=HOST_TO_DEVICE, endpoint_number=0, blocking=False): """ Handles the status stage of a correctly completed control request, by priming the appropriate endpoint to handle the status phase. Args: direction : Determines if we're ACK'ing an IN or OUT vendor request. (This should match the direction of the DATA stage.) endpoint_number : The endpoint number on which the control request occurred. blocking : True if we should wait for the ACK to be fully issued before returning. """ if direction == self.HOST_TO_DEVICE: # If this was an OUT request, we'll prime the output buffer to # respond with the ZLP expected during the status stage. self.send_on_endpoint(endpoint_number, data=[], blocking=blocking) else: # If this was an IN request, we'll need to set up a transfer descriptor # so the status phase can operate correctly. This effectively reads the # zero length packet from the STATUS phase. self.read_from_endpoint(endpoint_number) def _generate_endpoint_config_arguments(self, config): """ Generates the data content for an Endpoint Configuration command that will set up the GreatDancer's endpoints to match the active configuration. Args: config : A USBConfiguration object that represents the configuration being applied to the GreatDancer. """ arguments = [] # If our configuration is None, there's nothing to configure; bail out. if config is None: return arguments for interface in config.get_interfaces(): for endpoint in interface.get_endpoints(): log.info(f"Configuring {endpoint}.") triple = (endpoint.get_address(), endpoint.max_packet_size, endpoint.transfer_type,) arguments.append(triple) return arguments def connect(self, usb_device, max_packet_size_ep0=64, device_speed=DeviceSpeed.FULL): """ Prepares the GreatDancer to connect to the target host and emulate a given device. Args: usb_device : The USBDevice object that represents the device to be emulated. """ if device_speed != DeviceSpeed.FULL: log.warning(f"GreatFET only supports USB Full Speed. Ignoring requested speed: {device_speed.name}") self.max_packet_size_ep0 = max_packet_size_ep0 quirks = 0 # Compute our quirk flags. if 'manual_set_address' in self.quirks: log.info("Handling SET_ADDRESS on the host side!") quirks |= self.QUIRK_MANUAL_SET_ADDRESS self.api.connect(self.max_packet_size_ep0, quirks) self.connected_device = usb_device log.info("Connecting to host.") def disconnect(self): """ Disconnects the GreatDancer from its target host. """ log.info("Disconnecting from host.") self.device.comms.release_exclusive_access() self.api.disconnect() def _wait_until_ready_to_send(self, ep_num): # If we're already ready, we don't need to do anything. Abort. if self._is_ready_for_priming(ep_num, self.DEVICE_TO_HOST): return # Otherwise, wait until we're ready to send... while not self._is_ready_for_priming(ep_num, self.DEVICE_TO_HOST): pass # ... and since we've blocked the app from cleaning up any transfer # descriptors automatically by spinning in this thread, we'll clean up # the relevant transfers here. self._clean_up_transfers_for_endpoint(ep_num, self.DEVICE_TO_HOST) def send_on_endpoint(self, ep_num, data, blocking=True): """ Sends a collection of USB data on a given endpoint. Args: ep_num : The number of the IN endpoint on which data should be sent. data : The data to be sent. blocking : If true, this function will wait for the transfer to complete. """ log.trace(f"EP{ep_num}/IN: <- {bytes(data)}") self._wait_until_ready_to_send(ep_num) self.api.send_on_endpoint(ep_num, bytes(data)) # If we're blocking, wait until the transfer completes. if blocking: while not self._transfer_is_complete(ep_num, self.DEVICE_TO_HOST): pass self._clean_up_transfers_for_endpoint(ep_num, self.DEVICE_TO_HOST) def read_from_endpoint(self, ep_num): """ Reads a block of data from the given endpoint. Args: ep_num : The number of the OUT endpoint on which data is to be rx'd. """ # Start a nonblocking read from the given endpoint... self._prime_out_endpoint(ep_num) # ... and wait for the transfer to complete. while not self._transfer_is_complete(ep_num, self.HOST_TO_DEVICE): pass # Finally, return the result. return self._finish_primed_read_on_endpoint(ep_num) @staticmethod def _endpoint_address(ep_num, direction): """ Returns the endpoint number that corresponds to a given direction and address. """ if direction: return ep_num | 0x80 else: return ep_num def stall_endpoint(self, ep_num, direction=0): """ Stalls the provided endpoint, as defined in the USB spec. Args: ep_num : The number of the endpoint to be stalled. """ in_vs_out = "IN" if direction else "OUT" log.trace("Stalling EP{} {}".format(ep_num, in_vs_out)) self.endpoint_stalled[ep_num] = True self.api.stall_endpoint(self._endpoint_address(ep_num, direction)) def stall_ep0(self, direction=0): """ Convenience function that stalls the control endpoint zero. """ self.stall_endpoint(0, direction) def set_address(self, address, defer=False): """ Sets the device address of the GreatDancer. Usually only used during initial configuration. Args: address : The address that the GreatDancer should assume. defer : True iff the set_address request should wait for an active transaction to finish. """ self.api.set_address(address, 1 if defer else 0) @staticmethod def _decode_usb_register(transfer_result): """ Decodes a raw 32-bit register value from a form encoded for transit as a USB control request. Args: transfer_result : The value returned by the vendor request. Returns: The raw integer value of the given register. """ status_hex = codecs.encode(transfer_result[::-1], 'hex') return int(status_hex, 16) def _fetch_irq_status(self): """ Fetch the USB controller's pending-IRQ bitmask, which indicates which interrupts need to be serviced. Returns: A raw integer bitmap. """ return self.api.get_status(self.GET_USBSTS) def _fetch_setup_status(self): """ Fetch the USB controller's "pending setup packet" bitmask, which indicates which endpoints have setup packets to be read. Returns: A raw integer bitmap. """ return self.api.get_status(self.GET_ENDPTSETUPSTAT) def _handle_setup_events(self): """ Handles any outstanding setup events on the USB controller. """ # Determine if we have setup packets on any of our endpoints. status = self._fetch_setup_status() # If we don't, abort. if not status: return # Otherwise, figure out which endpoints have outstanding setup events, # and handle them. for i in range(self.SUPPORTED_ENDPOINTS): if status & (1 << i): self._handle_setup_event_on_endpoint(i) def _handle_setup_event_on_endpoint(self, endpoint_number): """ Handles a known outstanding setup event on a given endpoint. Args: endpoint_number : The endpoint number for which a setup event should be serviced. """ # HACK: to maintain API compatibility with the existing facedancer API, # we need to know if a stall happens at any point during our handler. self.endpoint_stalled[endpoint_number] = False # Read the data from the SETUP stage... data = bytearray(self.api.read_setup(endpoint_number)) request = self.connected_device.create_request(data) # If this is an OUT request, handle the data stage, # and add it to the request. is_out = request.get_direction() == self.HOST_TO_DEVICE has_data = (request.length > 0) # Special case: if this is an OUT request with a data stage, we won't # handle the request until the data stage has been completed. Instead, # we'll stash away the data received in the setup stage, prime the # endpoint for the data stage, and then wait for the data stage to # complete, triggering a corresponding code path in # in _handle_transfer_complete_on_endpoint. if is_out and has_data: self._prime_out_endpoint(endpoint_number) self.pending_control_request = request return self.connected_device.handle_request(request) if not is_out and not self.endpoint_stalled[endpoint_number]: self.ack_status_stage(direction=self.DEVICE_TO_HOST) def _fetch_transfer_status(self): """ Fetch the USB controller's "completed transfer" bitmask, which indicates which endpoints have recently completed transactions. Returns : A raw integer bitmap. """ return self.api.get_status(self.GET_ENDPTCOMPLETE) def _transfer_is_complete(self, endpoint_number, direction): """ Returns true iff a given endpoint has just completed a transfer. Can be used to check for completion of a non-blocking transfer. Args: endpoint_number : The endpoint number to be queried. direction : The direction of the transfer. Should be self.HOST_TO_DEVICE or self.DEVICE_TO_HOST. """ status = self._fetch_transfer_status() # From the LPC43xx manual: out endpoint completions start at bit zero, # while in endpoint completions start at bit 16. out_is_ready = (status & (1 << endpoint_number)) in_is_ready = (status & (1 << (endpoint_number + 16))) if direction == self.HOST_TO_DEVICE: return out_is_ready else: return in_is_ready def _handle_transfer_events(self): """ Handles any outstanding setup events on the USB controller. """ # Determine if we have ready packets on any of our endpoints. status = self._fetch_transfer_status() # If we don't, abort. if not status: return # Figure out which endpoints have recently completed transfers, # and clean up any transactions on those endpoints. It's important # that this be done /before/ the _handle_transfer_complete... section # below, as those can generate further events which will need the freed # transfer descriptors. # [Note that it's safe to clean up the transfer descriptors before reading, # here-- the GreatFET's USB controller has transparently moved any data # from OUT transactions into a holding buffer for us. Nice of it!] for i in range(self.SUPPORTED_ENDPOINTS): if status & (1 << i): self._clean_up_transfers_for_endpoint(i, self.HOST_TO_DEVICE) if status & (1 << (i + 16)): self._clean_up_transfers_for_endpoint(i, self.DEVICE_TO_HOST) # Now that we've cleaned up all relevant transfer descriptors, trigger # any events that should occur due to the completed transaction. for i in range(self.SUPPORTED_ENDPOINTS): if status & (1 << i): self._handle_transfer_complete_on_endpoint(i, self.HOST_TO_DEVICE) if status & (1 << (i + 16)): self._handle_transfer_complete_on_endpoint(i, self.DEVICE_TO_HOST) # Finally, after completing all of the above, we may now have idle # (unprimed) endpoints. For OUT endpoints, we'll need to re-prime them # so we're ready for receipt; for IN endpoints, we'll want to give the # emulated device a chance to provide new data. self._handle_transfer_readiness() def _finish_primed_read_on_endpoint(self, endpoint_number): """ Completes a non-blocking (primed) read on an OUT endpoint by reading any data received since the endpoint was primed. See read_from_endpoint for an example of proper use. Args: endpoint_number : The endpoint to read from. """ return self.api.finish_nonblocking_read(endpoint_number) def _clean_up_transfers_for_endpoint(self, endpoint_number, direction): """ Cleans up any outstanding transfers on the given endpoint. This must be called for each completed transaction so the relevant transfer descriptors can be re-used. There's no harm in calling this if a transaction isn't complete, but it _must_ be called at least once for each completed transaction. Args: endpoint_number : The endpoint number whose transfer descriptors should be cleaned up. direction : The endpoint direction for which TD's should be cleaned. """ # Ask the device to clean up any transaction descriptors related to the transfer. self.api.clean_up_transfer(self._endpoint_address(endpoint_number, direction)) def _is_control_endpoint(self, endpoint_number): """ Returns true iff the given endpoint number corresponds to a control endpoint. """ # FIXME: Support control endpoints other than EP0. return endpoint_number == 0 def _handle_transfer_complete_on_endpoint(self, endpoint_number, direction): """ Handles a known-completed transfer on a given endpoint. Args: endpoint_number : The endpoint number for which a setup event should be serviced. """ # If a transfer has just completed on an OUT endpoint, we've just received data # that we need to handle. if direction == self.HOST_TO_DEVICE: # Special case: if we've just received data on a control endpoint, # we're completing a control request. if self._is_control_endpoint(endpoint_number): # If we received a setup packet to handle, handle it. if self.pending_control_request: # Read the rest of the data from the endpoint, completing # the control request. new_data = self._finish_primed_read_on_endpoint(endpoint_number) # Append our new data to the pending control request. self.pending_control_request.data.extend(new_data) all_data_received = len(self.pending_control_request.data) == self.pending_control_request.length is_short_packet = len(new_data) < self.max_packet_size_ep0 if all_data_received or is_short_packet: # Handle the completed setup request... self.connected_device.handle_request(self.pending_control_request) # And clear our pending setup data. self.pending_control_request = None else: # Otherwise, re-prime the endpoint to grab the next packet. self._prime_out_endpoint(endpoint_number) # Typical case: this isn't a control endpoint, so we don't have a # defined packet format. Read the data and issue the corresponding # callback. else: data = self._finish_primed_read_on_endpoint(endpoint_number) log.trace(f"EP{endpoint_number}/OUT -> {data}") self.connected_device.handle_data_available(endpoint_number, data) def _fetch_transfer_readiness(self): """ Queries the GreatFET for a bitmap describing the endpoints that are not currently primed, and thus ready to be primed again. """ return self.api.get_status(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.api.get_status(self.GET_ENDPTNAK) def _prime_out_endpoint(self, endpoint_number): """ Primes an out endpoint, allowing it to receive data the next time the host chooses to send it. Args: endpoint_number : The endpoint that should be primed. """ self.api.start_nonblocking_read(endpoint_number) def _handle_transfer_readiness(self): """ Check to see if any non-control IN endpoints are ready to accept data from our device, and handle if they are. """ # If we haven't been configured yet, we can't have any # endpoints other than the control endpoint, and we don't n if not self.configuration: return # Fetch the endpoint status. status = self._fetch_transfer_readiness() # Check the status of every endpoint /except/ endpoint zero, # which is always a control endpoint and set handled by our # control transfer handler. for interface in self.configuration.get_interfaces(): for endpoint in interface.get_endpoints(): # Check to see if the endpoint is ready to be primed. if self._is_ready_for_priming(endpoint.number, endpoint.direction): # If this is an IN endpoint, we're ready to accept data to be # presented on the next IN token. if endpoint.direction == USBDirection.IN: self.connected_device.handle_buffer_available(endpoint.number) # If this is an OUT endpoint, we'll need to prime the endpoint to # accept new data. This provides a place for data to go once the # host sends an OUT token. else: self._prime_out_endpoint(endpoint.number) def _is_ready_for_priming(self, ep_num, direction): """ Returns true iff the endpoint is ready to be primed. Args: ep_num : The endpoint number in question. direction : The endpoint direction in question. """ # Fetch the endpoint status. status = self._fetch_transfer_readiness() ready_for_in = (not status & (1 << (ep_num + 16))) ready_for_out = (not status & (1 << (ep_num))) if direction == self.HOST_TO_DEVICE: return ready_for_out else: 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. Args: 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. """ log.debug("Host issued bus reset.") if self.connected_device: self.connected_device.handle_bus_reset() else: self.api.bus_reset() def reset(self): """ Triggers the GreatFET to handle its side of a bus reset. """ self.api.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.get_interfaces(): for endpoint in interface.get_endpoints(): # Skip OUT endpoints if endpoint.direction == self.HOST_TO_DEVICE: continue # 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. Args: configuration : The USBConfiguration object that describes the endpoints provided. """ endpoint_triplets = self._generate_endpoint_config_arguments(configuration) # If we need to issue a configuration command, issue one. # (If there are no endpoints other than control, this command will be # empty, and we can skip this.) if endpoint_triplets: self.api.set_up_endpoints(*endpoint_triplets) def configured(self, configuration): """ Callback that's issued when a USBDevice is configured, e.g. by the SET_CONFIGURATION request. Allows us to apply the new configuration. Args: configuration: The configuration applied by the SET_CONFIG request. """ self.validate_configuration(configuration) self._configure_endpoints(configuration) self.configuration = configuration # If we've just set up endpoints, check to see if any of them # need to be primed, or have NAKs waiting. self._handle_transfer_readiness() self._handle_nak_events() def service_irqs(self): """ Core routine of the Facedancer execution/event loop. Continuously monitors the GreatDancer's execution status, and reacts as events occur. """ status = self._fetch_irq_status() # Other bits that may be of interest: # D_SRI = start of frame received # D_PCI = port change detect (switched between low, full, high speed state) # D_SLI = device controller suspend # D_UEI = USB error; completion of transaction caused error, see usb1_isr in firmware # D_NAKI = both the tx/rx NAK bit and corresponding endpoint NAK enable are set if status & self.USBSTS_D_UI: self._handle_setup_events() self._handle_transfer_events() if status & self.USBSTS_D_URI: self._bus_reset() if status & self.USBSTS_D_NAKI: self._handle_nak_events() ================================================ FILE: facedancer/backends/greathost.py ================================================ # # This file is part of Facedancer. # """ Host support for GreatFET-base devices. """ import sys import time import codecs import struct from ..core import * from ..endpoint import USBEndpoint from ..types import * from ..logging import log class GreatDancerHostApp(FacedancerUSBHost): """ Class that represents a GreatFET-based USB host. """ app_name = "GreatDancer Host" PORT_STATUS_REG = 0 READ_STATUS_REG = 1 WRITE_STATUS_REG = 2 PORT_STATUS_REGISTER_CONNECTED_MASK = (1 << 0) PORT_STATUS_REGISTER_ENABLED_MASK = (1 << 2) PORT_STATUS_REGISTER_POWERED_MASK = (1 << 12) PORT_STATUS_REGISTER_SPEED_SHIFT = 26 PORT_STATUS_REGISTER_SPEED_MASK = 0b11 PORT_STATUS_REGISTER_LINE_STATE_SHIFT = 10 PORT_STATUS_REGISTER_LINE_STATE_MASK = 0b11 LINE_STATE_NAMES = { 0: "SE0", 1: "J", 2: "K", 3: "No device / SE1" } LINE_STATE_SE0 = 0 LINE_STATE_J = 1 LINE_STATE_K = 2 LINE_STATE_SE1 = 3 DEVICE_SPEED_LOW = 0 DEVICE_SPEED_FULL = 1 DEVICE_SPEED_HIGH = 2 DEVICE_SPEED_NONE = 3 STATUS_REG_SPEED_VALUES = { 0: DEVICE_SPEED_FULL, 1: DEVICE_SPEED_LOW, 2: DEVICE_SPEED_HIGH, 3: DEVICE_SPEED_NONE } DEVICE_SPEED_NAMES = { DEVICE_SPEED_FULL: "Full speed", DEVICE_SPEED_LOW: "Low speed", DEVICE_SPEED_HIGH: "High speed", DEVICE_SPEED_NONE: "Disconnected" } SPEED_REQUESTS = { 0: 1, 1: 0, 2: 2, 3: 3 } # Endpoint directions DIRECTION_IN = 0x00 DIRECTION_OUT = 0x80 # Endpoint types ENDPOINT_TYPE_CONTROL = 0 # Packet IDs PID_SETUP = 2 PID_OUT = 0 PID_IN = 1 @classmethod def appropriate_for_environment(cls, backend_name): """ Determines if the current environment seems appropriate for using the GreatDancer backend. """ # Check: if we have a backend name other than greatfet, # the user is trying to use something else. Abort! if backend_name and backend_name != "greatfet": return False # If we're not explicitly trying to use something else, # see if there's a connected GreatFET. try: import greatfet greatfet.GreatFET() return True except ImportError: log.info("Skipping GreatFET-based devices, as the greatfet python module isn't installed.") return False except: return False def __init__(self, verbose=0, quirks=[], autoconnect=True, device=None): """ Sets up a GreatFET-based host connection. """ import greatfet if device is None: device = greatfet.GreatFET() # Store our input args. # TODO: pull into base class self.device = device self.verbose = verbose # Grab a reference to our protocol definitions. self.vendor_requests = greatfet.protocol.vendor_requests if autoconnect: self.connect() def connect(self, device_speed=None): """ Sets up our host to talk to the device, including turning on VBUS. """ self.device.comms._vendor_request_out(self.vendor_requests.USBHOST_CONNECT) def bus_reset(self, delay=0.500): """ Issues a "bus reset", requesting that the downstream device reset itself. Args: delay : The amount of time, in seconds, to wait before or after the reset request. To be compliant, this should be omitted, or set to 0.1s. """ # Note: we need to wait a reset delay before and after the bus reset. # This allows the host to initialize _and_ then allows the device to settle. time.sleep(delay) self.device.comms._vendor_request_out(self.vendor_requests.USBHOST_BUS_RESET) time.sleep(delay) @staticmethod def _decode_usb_register(transfer_result): """ Decodes a raw 32-bit register value from a form encoded for transit as a USB control request. Args: transfer_result : The value returned by the vendor request. returns : The raw integer value of the given register. """ status_hex = codecs.encode(transfer_result[::-1], 'hex') return int(status_hex, 16) def _fetch_status_register(self, register_number): """ Fetches a status register from the GreatDacner, and returns it as an integer. """ raw_status = self.device.comms._vendor_request_in(self.vendor_requests.USBHOST_GET_STATUS, index=register_number, length=4) return self._decode_usb_register(raw_status) def _port_status(self): """ Returns the raw state of the port status register. """ return self._fetch_status_register(self.PORT_STATUS_REG) def _get_read_status(self): """ Returns the raw state of the read status word. """ return self._fetch_status_register(self.READ_STATUS_REG) def _get_write_status(self): """ Returns the raw state of the read status word. """ return self._fetch_status_register(self.WRITE_STATUS_REG) def device_is_connected(self): """ Returns true iff a given device is connected. """ status = self._port_status() return bool(status & self.PORT_STATUS_REGISTER_CONNECTED_MASK) def port_is_enabled(self): """ Returns true iff the Facedancer host port's enabled. """ status = self._port_status() return bool(status & self.PORT_STATUS_REGISTER_ENABLED_MASK) def port_is_powered(self): """ Returns true iff the Facedancer host port's enabled. """ status = self._port_status() return bool(status & self.PORT_STATUS_REGISTER_POWERED_MASK) def current_device_speed(self, as_string=False): """ Returns the speed of the connected device Args: as_string : If true, returns the speed as a string for printing; otherwise returns a DEVICE_SPEED_* constant. """ port_speed_raw = \ (self._port_status() >> self.PORT_STATUS_REGISTER_SPEED_SHIFT) & \ self.PORT_STATUS_REGISTER_SPEED_MASK # Translate from a GreatFET format device speed to a Facedancer one. port_speed = self.STATUS_REG_SPEED_VALUES[port_speed_raw] if as_string: port_speed = self.DEVICE_SPEED_NAMES[port_speed] return port_speed def current_line_state(self, as_string=False): """ Returns the current state of the USB differential pair Args: as_string : If true, returns the speed as a string for printing; otherwise returns a LINE_STATE_* constant. """ line_state = \ (self._port_status() >> self.PORT_STATUS_REGISTER_LINE_STATE_SHIFT) & \ self.PORT_STATUS_REGISTER_LINE_STATE_MASK if as_string: line_state = self.LINE_STATE_NAMES[line_state] return line_state def set_up_endpoint(self, endpoint_address_or_object, endpoint_type=None, max_packet_size=None, device_address=None, endpoint_speed=None, handle_data_toggle=None, is_control_endpoint=None): """ Sets up an endpoint for use. Can be used to initialize an endpoint or to update its parameters. Two forms exist: Args: endpoint_object : a USBEndpoint object with the parameters to be populated or Args: endpoint_address : the address of the endpoint to be setup; including the direction bit endpoint_type : one of the ENDPOINT_TYPE constants that specifies the transfer mode on the endpoint_address max_packet_size : the maximum packet size to be communicated on the given endpoint device_address : the address of the device to be communicated with; if not provided, the last address will be used. endpoint_speed : the speed of the packets to be communicated on the endpoint; should be a DEVICE_SPEED_* constant; if not provided, the last device's speed will be used. handle_data_toggle : true iff the hardware should automatically handle selection of data packet PIDs is_control_endpoint : true iff the given packet is a for a control endpoint """ if isinstance(endpoint_address_or_object, USBEndpoint): endpoint = endpoint_address_or_object # Figure out the endpoint address from its direction and number. endpoint_address = endpoint.number if endpoint.direction == USBDirection.IN: endpoint_address |= self.DIRECTION_IN self.set_up_endpoint(endpoint_address, endpoint.transfer_type, endpoint.max_packet_size) return endpoint_address = endpoint_address_or_object endpoint_number = endpoint_address & 0x7f if endpoint_number > 15: raise ValueError("cannot have an endpoint with a number > 15!") # Figure out defaults for any arguments not provided. if device_address is None: device_address = self.last_device_address if endpoint_speed is None: endpoint_speed = self.last_device_speed if is_control_endpoint is None: is_control_endpoint = (endpoint_number == 0) if handle_data_toggle is None: handle_data_toggle = True if not is_control_endpoint else False # Figure out which endpoint schedule to use. # FIXME: support more than the asynchronous schedule endpoint_schedule = 0 # TODO: do we translate speed requests, here? # Issue the configuration packet. packet = struct.pack("> endpoint_number) & 0x1 complete = (status >> (endpoint_number + 16)) & 0x1 if stalled: raise IOError("Stalled!") def read_from_endpoint(self, endpoint_number, expected_read_size=64, data_packet_pid=0): """ Sends a block of data on the provided endpoints. Args: endpoint_number : The endpoint number on which to send. expected_read_size : The expected amount of data to be read. data_packet_pid : The data packet PID to use (1 or 0). Ignored if the endpoint is set to automatically alternate data PIDs. Raises an IOError on a communications error or stall. """ # Start the request... self.device.comms._vendor_request_out(self.vendor_requests.USBHOST_START_NONBLOCKING_READ, index=(data_packet_pid << 8) | endpoint_number, value=expected_read_size) # ... and if we're blocking, also finish it. complete = False stalled = False # Wait until we get a complete flag in the status register. # XXX: This isn't entirely correct-- it'll clear too much status. while not complete: status = self._get_read_status() stalled = (status >> endpoint_number) & 0x1 complete = (status >> (endpoint_number + 16)) & 0x1 if stalled: raise IOError("Stalled!") # Figure out how muhc to read. raw_length = self.device.comms._vendor_request_in(self.vendor_requests.USBHOST_GET_NONBLOCKING_LENGTH, index=endpoint_number, length=4) length = self._decode_usb_register(raw_length) if self.verbose > 4: print("Supposedly, we've got {} bytes of data to read".format(length)) # If there's no data available, we don't need to waste time reading anything. if length == 0: return b'' # Otherwise, read the data from the endpoint and return it. data = self.device.comms._vendor_request_in(self.vendor_requests.USBHOST_FINISH_NONBLOCKING_READ, index=endpoint_number, length=length) return data.tobytes() ================================================ FILE: facedancer/backends/hydradancer.py ================================================ """ Backend for the Hydradancer boards. Supports 5 endpoints, with addresses between 0 and 7. Supports low, full and high-speed. """ import sys import logging import time from array import array from time import time_ns from dataclasses import dataclass from typing import List, Dict, Any import usb from usb.util import CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE, CTRL_IN, CTRL_OUT from ..core import * from ..device import USBDevice, USBConfiguration, USBDirection, USBEndpoint from ..types import DeviceSpeed from ..logging import log from .base import FacedancerBackend @dataclass class HydradancerEvent: # Events EVENT_BUS_RESET = 0x0 EVENT_IN_BUFFER_AVAILABLE = 0x1 EVENT_OUT_BUFFER_AVAILABLE = 0x2 EVENT_NAK = 0x3 event_type : int = -1 value : int = -1 @staticmethod def from_bytes(data : bytes): return HydradancerEvent(event_type = data[0], value = data[1]) def __repr__(self): return f"event_type {self.event_type} value {self.value}" class HydradancerHostApp(FacedancerApp, FacedancerBackend): """ Backend for the HydraUSB3 boards. """ app_name = "Hydradancer Host" MANUFACTURER_STRING = "Quarkslab https://www.quarkslab.com/ & HydraBus https://hydrabus.com/" # USB directions HOST_TO_DEVICE = 0 DEVICE_TO_HOST = 1 USB2_MAX_EP_IN = 16 current_setup_req = None def __init__(self, device: USBDevice=None, verbose: int=0, quirks: List[str]=[]): """ Initializes the backend. Args: device : The device that will act as our Facedancer. (Optional) verbose : The verbosity level of the given application. (Optional) quirks : List of USB platform quirks. (Optional) """ super().__init__(self) self.configuration = None self.pending_control_out_request = None self.connected_device = None self.max_ep0_packet_size = None self.ep_transfer_queue : List[List[Any]] = [[]] * self.USB2_MAX_EP_IN self.ep_in : Dict[int, USBEndpoint] = {} self.ep_out : Dict[int, USBEndpoint] = {} self.api = HydradancerBoard() self.verbose = verbose self.api.wait_board_ready() @classmethod def appropriate_for_environment(cls, backend_name: str) -> bool: """ Determines if the current environment seems appropriate for using this backend. Args: backend_name : Backend name being requested. (Optional) """ logging.info("this is hydradancer hi") # Open a connection to the target device... device = usb.core.find(idVendor=0x16c0, idProduct=0x27d8) if device is not None and device.manufacturer == cls.MANUFACTURER_STRING and backend_name == "hydradancer": return True return False def get_version(self): """ Returns information about the active Facedancer version. """ raise NotImplementedError def connect(self, usb_device: USBDevice, max_packet_size_ep0: int=64, device_speed: DeviceSpeed=DeviceSpeed.FULL): """ Prepares backend to connect to the target host and emulate a given device. Args: usb_device : The USBDevice object that represents the emulated device. max_packet_size_ep0 : Max packet size for control endpoint. device_speed : Requested usb speed for the Facedancer board. """ self.api.set_endpoint_mapping(0) if device_speed not in [DeviceSpeed.LOW, DeviceSpeed.FULL, DeviceSpeed.HIGH]: log.warning(f"Hydradancer only supports USB Low, Full and High Speed. Ignoring requested speed: {device_speed.name}") self.api.set_usb2_speed(device_speed) logging.info("connect ...") self.api.connect() self.connected_device = usb_device self.max_ep0_packet_size = max_packet_size_ep0 def disconnect(self): """ Disconnects Facedancer from the target host. """ logging.info("disconnect") self.configuration = None self.pending_control_out_request = None self.connected_device = None self.max_ep0_packet_size = 0 self.ep_transfer_queue = [[]] * self.USB2_MAX_EP_IN self.api.disconnect() def reset(self): """ Triggers the Facedancer to handle its side of a bus reset. """ logging.info("bus reset") def set_address(self, address: int, defer: bool=False): """ Sets the device address of the Facedancer. Usually only used during initial configuration. Args: address : The address the Facedancer should assume. defer : True iff the set_address request should wait for an active transaction to finish. """ logging.info("set address") self.api.set_address(address, defer) def configured(self, configuration: USBConfiguration): """ Callback that's issued when a USBDevice is configured, e.g. by the SET_CONFIGURATION request. Allows us to apply the new configuration. Args: configuration : The USBConfiguration object applied by the SET_CONFIG request. """ self.validate_configuration(configuration) if configuration is None: self.configuration = None self.api.configured = False logging.debug("unconfigured") return self.api.reinit(keep_ep0=True) endpoint_numbers = [] for interface in configuration.get_interfaces(): for endpoint in interface.get_endpoints(): ep_num = endpoint.number is_ep_in = endpoint.direction == 1 if ep_num not in endpoint_numbers: endpoint_numbers.append(ep_num) if is_ep_in: self.ep_in[ep_num] = endpoint else: self.ep_out[ep_num] = endpoint self.api.configure(endpoint_numbers) self.configuration = configuration logging.debug("configured") def read_from_endpoint(self, endpoint_number: int) -> bytes: """ Reads a block of data from the given endpoint. Args: endpoint_number : The number of the OUT endpoint on which data is to be rx'd. """ return self.api.read(endpoint_number, blocking=True) def send_on_endpoint(self, endpoint_number: int, data: bytes, blocking: bool=True): """ Sends a collection of USB data on a given endpoint. Args: endpoint_number : The number of the IN endpoint on which data should be sent. data : The data to be sent. blocking : If true, this function should wait for the transfer to complete. """ if endpoint_number != 0 and not blocking and not self.api.in_buffer_empty(endpoint_number): logging.debug(f"Storing {len(data)} on ep {endpoint_number} for later") self.ep_transfer_queue[endpoint_number].append(data) return backup_len = len(data) max_packet_size = self.max_ep0_packet_size if endpoint_number == 0 else self.ep_in[endpoint_number].max_packet_size if not data: self.api.send(endpoint_number, data) while data: packet = data[0:max_packet_size] data = data[len(packet):] logging.debug(f"Sending {len(packet)} on ep {endpoint_number}") self.api.send(endpoint_number, packet) # Many things to take into account here ... # first, if the len we are sending is a multiple of the max_packet_size, the host will request a ZLP (otherwise, it can't know when the transfer ends) # however, if the endpoint is endpoint 0, the host knows the size of the transfer in advance so it might not request the ZLP # this could be solved by using NAKs for EP0 as well (answering by a ZLP if a NAK is received but we already sent everything) # however, this could add too much latency and make enumeration fail if endpoint_number == 0 and (backup_len % max_packet_size) == 0 and backup_len > 0 and backup_len != self.current_setup_req.length: logging.debug(f"Sending ZLP") self.api.send(endpoint_number, b"") # Sending ZLP def ack_status_stage(self, direction: USBDirection=USBDirection.OUT, endpoint_number:int =0, blocking: bool=False): """ Handles the status stage of a correctly completed control request, by priming the appropriate endpoint to handle the status phase. Args: direction : Determines if we're ACK'ing an IN or OUT vendor request. (This should match the direction of the DATA stage.) endpoint_number : The endpoint number on which the control request occurred. blocking : True if we should wait for the ACK to be fully issued before returning. """ if direction == USBDirection.OUT: # If this was an OUT request, we'll prime the output buffer to # respond with the ZLP expected during the status stage. self.send_on_endpoint(endpoint_number, data=b"", blocking=blocking) else: # If this was an IN request, we'll need to set up a transfer descriptor # so the status phase can operate correctly. This effectively reads the # zero length packet from the STATUS phase. self.read_from_endpoint(endpoint_number) def stall_endpoint(self, endpoint_number:int, direction: USBDirection=USBDirection.OUT): """ Stalls the provided endpoint, as defined in the USB spec. Args: endpoint_number : The number of the endpoint to be stalled. """ in_vs_out = "IN" if direction else "OUT" logging.info(f"Stalling EP {endpoint_number} {in_vs_out}") self.api.stall_endpoint(endpoint_number, direction) def clear_halt(self, endpoint_number:int, direction: USBDirection): """ Clears a halt condition on the provided non-control endpoint. Args: endpoint_number : The endpoint number direction : The endpoint direction; or OUT if not provided. """ logging.debug(f"Clearing halt for endpoint {endpoint_number}") self.api.clear_halt(endpoint_number, direction) def service_irqs(self): """ Core routine of the Facedancer execution/event loop. Continuously monitors the Facedancer's execution status, and reacts as events occur. """ events = self.api.fetch_events() if events is not None: for event in events: if event is None: continue if event.event_type == HydradancerEvent.EVENT_BUS_RESET: self.handle_bus_reset() if event.event_type == HydradancerEvent.EVENT_IN_BUFFER_AVAILABLE and event.value != 0 and (event.value in self.ep_in.keys()): self.connected_device.handle_buffer_empty(self.ep_in[event.value]) self.handle_control_request() self.handle_data_endpoints() def handle_bus_reset(self): """ Triggers Hydradancer to perform its side of a bus reset. """ if self.connected_device: self.connected_device.handle_bus_reset() else: self.reset() def handle_data_endpoints(self): """ Handle IN or OUT requests on non-control endpoints. """ # process ep OUT firsts, transfer is dictated by the host, if there is data available on an ep OUT, # it should be processed before setting new IN data for ep_num in self.ep_out: if self.api.out_buffer_available(ep_num): data = self.api.read(ep_num) if data is not None: self.connected_device.handle_data_available( ep_num, data.tobytes()) for ep_num, ep in self.ep_in.items(): if self.api.in_buffer_empty(ep_num) and self.api.nak_on_endpoint(ep_num): if len(self.ep_transfer_queue[ep_num]) != 0: max_packet_size = ep.max_packet_size packet = self.ep_transfer_queue[ep_num][0][0:max_packet_size] self.ep_transfer_queue[ep_num][0] = self.ep_transfer_queue[ep_num][0][len(packet):] self.api.send(ep_num, packet) if len(self.ep_transfer_queue[ep_num][0]) == 0: self.ep_transfer_queue[ep_num].pop(0) else: self.connected_device.handle_nak(ep_num) def handle_control_request(self): if not self.api.control_buffer_available(): return data = self.api.read(0) if data is None: return logging.debug( f"CONTROL EP/OUT: -> size {len(data)} {bytes(data)}") #  inspired from moondancer and greatdancer backends if self.pending_control_out_request is not None: self.pending_control_out_request.data.extend(data) all_data_received = len( self.pending_control_out_request.data) == self.pending_control_out_request.length is_short_packet = len(data) < self.max_ep0_packet_size if all_data_received or is_short_packet: self.connected_device.handle_request( self.pending_control_out_request) self.pending_control_out_request = None elif len(data) > 0: request = self.connected_device.create_request(data) is_out = request.get_direction() == self.HOST_TO_DEVICE has_data = (request.length > 0) self.current_setup_req = request if is_out and has_data: logging.debug("queuing Control OUT req, waiting for more data") self.pending_control_out_request = request return self.connected_device.handle_request(request) # handle status stage of IN transfer elif len(data) == 0: logging.debug("Received ACK for IN Ctrl req") class HydradancerBoardFatalError(Exception): pass class HydradancerBoard(): """ Handles the communication with the Hydradancer control board and manages the events it sends. """ MAX_PACKET_SIZE = 1024 # USB Vendor Requests codes ENABLE_USB_CONNECTION = 50 SET_ADDRESS = 51 GET_EVENT = 52 SET_ENDPOINT_MAPPING = 53 DISABLE_USB = 54 SET_SPEED = 55 SET_EP_RESPONSE = 56 CHECK_HYDRADANCER_READY = 57 DO_BUS_RESET = 58 CONFIGURED = 59 CLEAR_HALT = 60 # Facedancer USB2 speed to Hydradancer USB2 speed facedancer_to_hydradancer_speed = { DeviceSpeed.LOW : 0, DeviceSpeed.FULL : 1, DeviceSpeed.HIGH : 2 } # Max number of events that can be sent by the board # This must not be less than what is defined in the firmware EVENT_QUEUE_SIZE = 100 # Endpoint states on the emulation board ENDP_STATE_ACK = 0x00 ENDP_STATE_NAK = 0x02 ENDP_STATE_STALL = 0x03 # USB endpoints direction HOST_TO_DEVICE = 0 DEVICE_TO_HOST = 1 EP_POLL_NUMBER = 1 SUPPORTED_EP_NUM = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] INCOMPATIBLE_EP = [[], [9], [10], [11], [8, 12], [13], [14], [15], [4], [1], [2], [3], [4], [5], [6], [7]] timeout_ms_poll = 1 def reinit(self, keep_ep0:bool = False): if keep_ep0 and 0 in self.endpoints_mapping: old_control_ep = self.endpoints_mapping[0] self.endpoints_mapping = {0: self.endpoints_mapping[0]} self.reverse_endpoints_mapping = {old_control_ep:0} else: self.endpoints_mapping = {} # emulated endpoint -> control board endpoint self.reverse_endpoints_mapping = {} # control_board_endpoint -> emulated_endpoint self.events = array('B', [0] * 2 * self.EVENT_QUEUE_SIZE) # True when SET_CONFIGURATION has been received and the Hydradancer boards are configured self.configured = False # 0x00ff (IN status mask, 1 = emulated ep ready for priming), 0xff00 (OUT mask, data received on emulated ep) self._hydradancer_status_bytes = array('B', [0] * 4) self.hydradancer_status = {} self.hydradancer_status["ep_in_status"] = (1 << 0) & 0xff self.hydradancer_status["ep_out_status"] = 0x00 self.hydradancer_status["ep_in_nak"] = 0x00 def __init__(self): """ Get handles on the USB control board, and wait for Hydradancer to be ready """ self.configured = False self.endpoints_mapping : Dict[int,int] = {} self.reinit() # Open a connection to the target device... self.device = usb.core.find(idVendor=0x16c0, idProduct=0x27d8) if self.device is None: raise HydradancerBoardFatalError("Hydradancer board not found") if self.device.speed != usb.util.SPEED_SUPER: raise HydradancerBoardFatalError( "Hydradancer not detected as USB3 Superspeed") cfg = self.device.get_active_configuration() intf = cfg[(0, 0)] # Detach the device from any kernel driver for intf in cfg: if self.device.is_kernel_driver_active(intf.bInterfaceNumber): try: self.device.detach_kernel_driver(intf.bInterfaceNumber) except usb.core.USBError as e: sys.exit("Could not detach kernel driver from interface({0}): {1}".format( intf.bInterfaceNumber, str(e))) # store the different endpoints handles we need self.ep_in = list(usb.util.find_descriptor( intf, find_all=True, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN and usb.util.endpoint_address(e.bEndpointAddress) != self.EP_POLL_NUMBER)) self.ep_in = {usb.util.endpoint_address( e.bEndpointAddress): e for e in self.ep_in} self.ep_out = list(usb.util.find_descriptor( intf, find_all=True, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT and usb.util.endpoint_address(e.bEndpointAddress) != self.EP_POLL_NUMBER)) self.ep_out = {usb.util.endpoint_address( ep.bEndpointAddress): ep for ep in self.ep_out} # the endpoint on which status information is received self.ep_poll = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN and usb.util.endpoint_address(e.bEndpointAddress) == self.EP_POLL_NUMBER) if len(self.ep_in.keys()) == 0 and len(self.ep_out.keys()) == 0: logging.info("Dumping device configuration \r\n" + str(cfg)) raise HydradancerBoardFatalError( "Could not fetch Hydradancer IN and OUT endpoints list") if len(self.ep_in.keys()) == 0: logging.info("Dumping device configuration \r\n" + str(cfg)) raise HydradancerBoardFatalError( "Could not fetch Hydradancer IN endpoints list") if len(self.ep_out.keys()) == 0: logging.info("Dumping device configuration \r\n" + str(cfg)) raise HydradancerBoardFatalError( "Could not fetch Hydradancer OUT endpoints list") if self.ep_out.keys() != self.ep_in.keys(): logging.info("Dumping device configuration \r\n" + str(cfg)) raise HydradancerBoardFatalError( f"Hydradancer IN/OUT endpoints pair incomplete \r\nep_in {self.ep_in} \r\nep_out {self.ep_out}") if self.ep_poll is None: logging.info("Dumping device configuration \r\n" + str(cfg)) raise HydradancerBoardFatalError( f"Could not get handle on Hydradancer events endpoint (EP {self.EP_POLL_NUMBER})") self.endpoints_pool = set(self.ep_in.keys()) # wait until the board is ready, for instance if a disconnect was previously issued self.wait_board_ready() def connect(self): """ Enable the USB2 connection on the emulation board """ try: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.ENABLE_USB_CONNECTION) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError("Error, unable to connect") from exception def disconnect(self): """ Disable the USB2 connection on the emulation board, and reset internal states on both control and emulation boards. """ try: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.DISABLE_USB) usb.util.dispose_resources(self.device) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError("Error, unable to disconnect") from exception def wait_board_ready(self): """ Wait until the Hydradancer boards are ready, try to disconnect at some point to reset the internal states, hoping it will be ready next time. """ #  num of checks before trying to disconnect max_num_status_ready_before_disconnect = 100 count_status_ready = 0 max_disconnect = 2 count_disconnect = 2 time_after_disconnect_sec = 1 time_between_checks_sec = 0.01 try: # check if the board is ready a first time hydradancer_ready = self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_IN, self.CHECK_HYDRADANCER_READY, data_or_wLength=1, timeout=5) # repeat max_num_status_ready_before_disconnect times while (hydradancer_ready is None or hydradancer_ready == 0) and count_disconnect < max_disconnect: count_status_ready += 1 if count_status_ready % max_num_status_ready_before_disconnect == 0 and \ count_disconnect < max_disconnect: logging.info( "This is taking too long, disconnecting again ...") self.disconnect() time.sleep(time_after_disconnect_sec) count_disconnect += 1 hydradancer_ready = self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_IN, self.CHECK_HYDRADANCER_READY, data_or_wLength=1, timeout=5) time.sleep(time_between_checks_sec) # if hydradancer is still not ready if hydradancer_ready == 0: raise HydradancerBoardFatalError( "Hydradancer is not ready, please reset the board") except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError( "USB Error while waiting for Hydradancer go-ahead") from exception def set_endpoint_mapping(self, ep_num): """ Maps emulated endpoints (endpoints facing the target) to Facedancer's host endpoints (control board endpoints) """ if ep_num not in self.SUPPORTED_EP_NUM: raise HydradancerBoardFatalError( f"Endpoint number {ep_num} not supported, supported numbers : {self.SUPPORTED_EP_NUM}") if len(self.endpoints_mapping.values()) >= len(self.endpoints_pool): raise HydradancerBoardFatalError( f"All {len(self.endpoints_pool)} endpoints are already in use (for EP0 included)") if ep_num not in self.endpoints_mapping: self.endpoints_mapping[ep_num] = list( self.endpoints_pool - set(self.endpoints_mapping.values()))[0] self.reverse_endpoints_mapping[self.endpoints_mapping[ep_num]] = ep_num try: self.device.ctrl_transfer(CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_ENDPOINT_MAPPING, wValue=( ep_num & 0x00ff) | ((self.endpoints_mapping[ep_num] << 8) & 0xff00)) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError( f"Could not set mapping for ep {ep_num}") from exception def set_usb2_speed(self, device_speed: DeviceSpeed=DeviceSpeed.FULL): """ Set the speed of the USB2 device. Speed is physically determined by the host, so the emulation board must be configured. """ try: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_SPEED, wValue=self.facedancer_to_hydradancer_speed[device_speed] & 0x00ff) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError("Error, unable to set speed") from exception def clear_halt(self, endpoint_number:int, direction: USBDirection): try: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.CLEAR_HALT, wValue=((endpoint_number & 0xff) | ((direction & 0xff) << 8))) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError("Error, unable to clear halt on endpoint {endpoint_number} direction {direction}") from exception def set_address(self, address, defer=False): """ Set the USB address on the emulation board """ try: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_ADDRESS, address) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError( "Error, unable to set address on emulated device") from exception def stall_endpoint(self, ep_num, direction=0): """ Stall the ep_num endpoint on the emulation board. STALL will be cleared automatically after next SETUP packet received. """ # Stall EP try: if ep_num == 0: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_EP_RESPONSE, wValue=(ep_num | 0 << 7) | (self.ENDP_STATE_STALL << 8) & 0xff00) self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_EP_RESPONSE, wValue=(ep_num | 1 << 7) | (self.ENDP_STATE_STALL << 8) & 0xff00) else: self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.SET_EP_RESPONSE, wValue=(ep_num | direction << 7) | (self.ENDP_STATE_STALL << 8) & 0xff00) except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError(f"Could not stall ep {ep_num}") from exception def send(self, ep_num, data): """ Prime target endpoint ep_num. """ try: while not self.in_buffer_empty(ep_num): events = self.fetch_events() logging.debug(f"Sending len {len(data)} {data} on ep {ep_num}") self.ep_out[self.endpoints_mapping[ep_num]].write( data) self.hydradancer_status["ep_in_status"] &= ~(0x01 << ep_num) self.hydradancer_status["ep_in_nak"] &= ~(0x01 << ep_num) except (usb.core.USBTimeoutError, usb.core.USBError): logging.error(f"could not send data on ep {ep_num}") def read(self, ep_num, blocking=False): """ Read from target endpoint ep_num. If blocking=True, wait until the endpoint's buffer is full. """ logging.debug(f"reading from ep {ep_num}") try: if blocking: while not self.out_buffer_available(ep_num): self.fetch_events() if self.out_buffer_available(ep_num): read = self.ep_in[self.endpoints_mapping[ep_num]].read( self.MAX_PACKET_SIZE) logging.debug( f"EP{ep_num}/OUT: <- size {len(read)} {bytes(read)}") self.hydradancer_status["ep_out_status"] &= ~(0x01 << ep_num) return read return None except (usb.core.USBTimeoutError, usb.core.USBError): logging.error(f"could not read data from ep {ep_num}") return None def configure(self, endpoint_numbers): if len(endpoint_numbers) > len(self.endpoints_pool): raise HydradancerBoardFatalError( f"Hydradancer cannot handle {len(endpoint_numbers)} endpoints, only {len(self.endpoints_pool)}") try: for number in endpoint_numbers: if self.INCOMPATIBLE_EP[number] in endpoint_numbers: raise HydradancerBoardFatalError( f"EP {number} can't be used at the same time as EPs {','.join([endpoint_numbers])}") from exception self.set_endpoint_mapping(number) self.device.ctrl_transfer(CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT, self.CONFIGURED) logging.info(f"Endpoints mapping {self.endpoints_mapping}") self.configured = True except (usb.core.USBTimeoutError, usb.core.USBError) as exception: logging.error(exception) raise HydradancerBoardFatalError( "Could not pass configured step on board") from exception def fetch_events(self): """ Poll the status of the endpoints. The state are accumulated (like on the boards), and cleared when sending or reading data (which will trigger a similar clear on the boards). Thus, self.ep_status should always be in sync with the endpoint's status on the boards. """ try: # Use the endpoint type that best fits the type of request : # -> for control requests, polling using ctrl transfers garanties the fastest status update. # Latency is key in the enumeration phase # -> for bulk requests, polling using bulk transfers allows for more status updates to be sent, # thus increasing the speed # TODO : what about interrupt or isochronous transfers ? if not self.configured: read = self.device.ctrl_transfer( CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_IN, self.GET_EVENT, data_or_wLength=self.events, timeout=self.timeout_ms_poll) else: read = self.ep_poll.read( self.events, timeout=self.timeout_ms_poll*10) if read >= 2: events = [] for i in range(0, read, 2): event = HydradancerEvent.from_bytes(self.events[i:i+2]) events.append(event) logging.debug(event) if event.event_type == HydradancerEvent.EVENT_IN_BUFFER_AVAILABLE: self.hydradancer_status["ep_in_status"] |= (0x1 << event.value) & 0xff elif event.event_type == HydradancerEvent.EVENT_OUT_BUFFER_AVAILABLE: self.hydradancer_status["ep_out_status"] |= (0x1 << event.value) & 0xff elif event.event_type == HydradancerEvent.EVENT_NAK: self.hydradancer_status["ep_in_nak"] |= (0x1 << event.value) & 0xff logging.debug(f"Hydradancer status {self.hydradancer_status}") return events return None except usb.core.USBTimeoutError: return None except usb.core.USBError as exception: logging.error(exception) raise HydradancerBoardFatalError("USB Error while fetching events") from exception def in_buffer_empty(self, ep_num): """ Returns True if the IN buffer for target endpoint ep_num is ready for priming """ return self.hydradancer_status["ep_in_status"] & (0x1 << ep_num) def nak_on_endpoint(self, ep_num): """ Returns True if the IN Endpoint has sent a NAK (meaning a host has sent an IN request) """ return self.hydradancer_status["ep_in_nak"] & (0x1 << ep_num) def out_buffer_available(self, ep_num): """ Returns True if the OUT buffer for target endpoint ep_num is full """ return self.hydradancer_status["ep_out_status"] & (0x1 << ep_num) def control_buffer_available(self): """ Returns True if the control buffer is available. Since this buffer is shared between EP0 IN/EP0 OUT, only the OUT status is used for both. """ return self.out_buffer_available(0) ================================================ FILE: facedancer/backends/libusbhost.py ================================================ # # This file is part of Facedancer. # """ Host support for accessing libusb with a Facedancer-like syntax. """ import sys import time import codecs import struct import usb from ..core import * class LibUSBHostApp(FacedancerUSBHost): """ Class that represents a libusb-based USB host. """ app_name = "LibUSB Host" @classmethod def appropriate_for_environment(cls, backend_name): """ Determines if the current environment seems appropriate for using the libusb backend. """ # For this to work, we need to somehow select a single port. # The best way to do this is with BUS and PORT, so allow the user # to select those using the environment. # TODO: accept these via quirks? if os.environ.get('LIBUSB_BUS') and os.environ.get('LIBUSB_PORT'): return True # As a stand-in, allow use if the user specifies a device's address. if os.environ.get('LIBUSB_ADDRESS'): return True # Never automatically instantiate the libusb backend, # as it's not a full implementation and requires host-OS oddities. return False def __init__(self, verbose=0, quirks=[], index=0, **kwargs): """ Creates a new libusb backend for communicating with a target device. """ self.verbose = verbose # If we have a specified bus/port, accept them. # TODO: accept these via quirks? desired_bus = os.environ.get('LIBUSB_BUS') desired_port = os.environ.get('LIBUSB_PORT') if desired_bus and desired_port: kwargs['bus'] = int(desired_bus) kwargs['port_number'] = int(desired_port) # If the user's searching by address, use that. # TODO: accept these via quirks? desired_address = os.environ.get('LIBUSB_ADDRESS') if desired_address: kwargs['address'] = int(desired_address) # Open a connection to the target device... usb_devices = list(usb.core.find(find_all=True, **kwargs)) if len(usb_devices) <= index: raise DeviceNotFoundError("Could not find a device to connect to via libusb!") self.device = usb_devices[index] # Detach any existing drivers, where possible. try: index = self.device.get_active_configuration().index self.device.detach_kernel_driver(index) except: # FIXME: note this here, with a warning? pass def connect(self, device_speed=None): """ Sets up our host to talk to the device, including turning on VBUS. """ pass def bus_reset(self, delay=0): """ Issues a "bus reset", requesting that the downstream device reset itself. Args: delay : The amount of time, in seconds, to wait before or after the reset request. To be compliant, this should be omitted, or set to 0.1s. """ # Note: we need to wait a reset delay before and after the bus reset. # This allows the host to initialize _and_ then allows the device to settle. time.sleep(delay) self.device.reset() time.sleep(delay) def current_device_speed(self, as_string=False): """ Returns the speed of the connected device Args: as_string : If true, returns the speed as a string for printing; otherwise returns a DEVICE_SPEED_* constant. """ return self.device.speed def current_line_state(self, as_string=False): """ Returns the current state of the USB differential pair as_string : If true, returns the speed as a string for printing; otherwise returns a LINE_STATE_* constant. """ return None def device_is_connected(self): """ Returns true iff a given device is connected. """ return True def port_is_enabled(self): """ Returns true iff a given device is connected. """ return True def port_is_powered(self): """ Returns true iff a given device is connected. """ return True def set_up_endpoint(self, endpoint_address_or_object, endpoint_type=None, max_packet_size=None, device_address=None, endpoint_speed=None, handle_data_toggle=None, is_control_endpoint=None): """ Sets up an endpoint for use. Can be used to initialize an endpoint or to update its parameters. Two forms exist: Args: endpoint_object : a USBEndpoint object with the parameters to be populated or Args: endpoint_address : the address of the endpoint to be setup; including the direction bit endpoint_type : one of the ENDPOINT_TYPE constants that specifies the transfer mode on the endpoint_address max_packet_size : the maximum packet size to be communicated on the given endpoint device_address : the address of the device to be communicated with; if not provided, the last address will be used endpoint_speed : the speed of the packets to be communicated on the endpoint; should be a DEVICE_SPEED_* constant; if not provided, the last device's speed will be used. handle_data_toggle : true iff the hardware should automatically handle selection of data packet PIDs is_control_endpoint : true iff the given packet is a for a control endpoint """ # TODO: eventually support hubs / more than one device? pass def initialize_control_endpoint(self, device_address=None, device_speed=None, max_packet_size=None): """ Set up the device's control endpoint, so we can use it for e.g. enumeration. """ pass def send_on_endpoint(self, endpoint_number, data, is_setup=False, blocking=True, data_packet_pid=0): """ Sends a block of data on the provided endpoints. Args: endpoint_number : The endpoint number on which to send. data : The data to be transmitted. is_setup : True iff this transfer should begin with a SETUP token. blocking : True iff this transaction should wait for the transaction to complete. data_packet_pid : The data packet PID to use (1 or 0). Ignored if the endpoint is set to automatically alternate data PIDs. raises an IOError on a communications error or stall """ self.device.write(endpoint_number, data) def read_from_endpoint(self, endpoint_number, expected_read_size=64, data_packet_pid=0): """ Sends a block of data on the provided endpoints. Args: endpoint_number : The endpoint number on which to send. expected_read_size : The expected amount of data to be read. data_packet_pid : The data packet PID to use (1 or 0). Ignored if the endpoint is set to automatically alternate data PIDs. raises an IOError on a communications error or stall """ data = self.device.read(endpoint_number, expected_read_size) return data.tobytes() def control_request_in(self, request_type, recipient, request, value=0, index=0, length=0): """ Performs an IN control request. Args: request_type : Determines if this is a standard, class, or vendor request. Accepts a REQUEST_TYPE_* constant. recipient : Determines the context in which this command is interpreted. Accepts a REQUEST_RECIPIENT_* constant. request : The request number to be performed. value, index : The standard USB request arguments, to be included in the setup packet. Their meaning varies depending on the request. length : The maximum length of data expected in response, or 0 if we don't expect any data back. """ request_type = self._build_request_type(True, request_type, recipient) data = self.device.ctrl_transfer(request_type, request, value, index, length) return data.tobytes() def control_request_out(self, request_type, recipient, request, value=0, index=0, data=[]): """ Performs an OUT control request. Args: request_type : Determines if this is a standard, class, or vendor request. Accepts a REQUEST_TYPE_* constant. recipient : Determines the context in which this command is interpreted. Accepts a REQUEST_RECIPIENT_* constant. request : The request number to be performed. value, index : The standard USB request arguments, to be included in the setup packet. Their meaning varies depending on the request. data : The data to be transmitted with this control request. """ request_type = self._build_request_type(True, request_type, recipient) self.device.ctrl_transfer(request_type, request, value, index, data) ================================================ FILE: facedancer/backends/moondancer.py ================================================ # MoondancerApp.py import sys import time import codecs import enum import traceback from typing import List, Tuple from ..core import * from ..device import USBDevice from ..configuration import USBConfiguration from ..request import USBControlRequest from ..types import DeviceSpeed, USBDirection from ..logging import log from .base import FacedancerBackend # Quirk flags class QuirkFlag(enum.IntFlag): MANUAL_SET_ADDRESS: int = 0x01 # Cynthion interrupt events class InterruptEvent: USB_BUS_RESET: int = 10 USB_RECEIVE_CONTROL: int = 11 USB_RECEIVE_PACKET: int = 12 USB_SEND_COMPLETE: int = 13 def __init__(self, data: Tuple[int, int]): """ Parses a tuple of two bytes representing an interrupt event into an InterruptEvent. Args: data : A tuple of two bytes. The first byte is the interrupt code, the second is the endpoint number. """ if len(data) != 2: log.error(f"Invalid length for InterruptEvent: {len(data)}") raise ValueError(f"Invalid length for InterruptEvent: {len(data)}") event = data[0] endpoint_number = data[1] if event not in [ InterruptEvent.USB_BUS_RESET, InterruptEvent.USB_RECEIVE_CONTROL, InterruptEvent.USB_RECEIVE_PACKET, InterruptEvent.USB_SEND_COMPLETE ]: raise ValueError(f"Unknown InterruptEvent id: {event}") self.event = event self.endpoint_number = endpoint_number def __eq__(self, rhs): return self.event == rhs def __repr__(self): name = "UNKNOWN" if self.event == InterruptEvent.USB_BUS_RESET: name = "USB_BUS_RESET" elif self.event == InterruptEvent.USB_RECEIVE_CONTROL: name = "USB_RECEIVE_CONTROL" elif self.event == InterruptEvent.USB_RECEIVE_PACKET: name = "USB_RECEIVE_PACKET" elif self.event == InterruptEvent.USB_SEND_COMPLETE: name = "USB_SEND_COMPLETE" return f"{name} {self.endpoint_number}" # # Moondancer backend implementation # class MoondancerApp(FacedancerApp, FacedancerBackend): """ Backend for using Cynthion devices as Facedancers. """ app_name = "Moondancer" # Number of supported USB endpoints. SUPPORTED_ENDPOINTS = 16 def __init__(self, device: USBDevice=None, verbose: int=0, quirks: List[str]=[]): """ Sets up a new Cynthion-backed Facedancer (Moondancer) application. Args: device : The Cynthion device that will act as our Moondancer. verbose : The verbosity level of the given application. """ log.info("Using the Moondancer backend.") import cynthion if device is None: device = cynthion.Cynthion() self.device = device self.device.comms.get_exclusive_access() FacedancerApp.__init__(self, device, verbose) self.connected_device = None # Grab the raw API object from the Cynthion object. # This has the low-level RPCs used for raw USB control. self.api = self.device.apis.moondancer # Initialize a dictionary that will store the last setup # whether each endpoint is currently stalled. self.endpoint_stalled = {} for i in range(self.SUPPORTED_ENDPOINTS): self.endpoint_stalled[i] = False # Assume a max packet size of 64 until configured otherwise. self.max_packet_size_ep0 = 64 # Start off by assuming we're not waiting for an OUT control transfer's # data stage. # See handle_setup_complete_on_endpoint for details. self.pending_control_request = None # Store a reference to the device's active configuration, # which we'll use to know which endpoints we'll need to check # for data transfer readiness. self.configuration = None # # Store our list of quirks to handle. # if quirks: self.quirks = quirks else: self.quirks = [] # Maintain a list of configured endpoints with form: (address, max_packet_size, USBTransferType) self.configured_endpoints = dict() # - Facedancer backend methods -------------------------------------------- @classmethod def appropriate_for_environment(cls, backend_name: str) -> bool: """ Determines if the current environment seems appropriate for using the Moondancer backend. """ # Check: if we have a backend name other than moondancer, # the user is trying to use something else. Abort! if backend_name and backend_name != "cynthion": return False # If we're not explicitly trying to use something else, # see if there's a connected Cynthion. try: import cynthion device = cynthion.Cynthion() return device.supports_api('moondancer') except ImportError: log.info("Skipping Cynthion-based devices, as the cynthion python module isn't installed.") return False except IOError: log.warning("Found Cynthion-based device, but could not access it. (Check permissions?)") return False except: return False def get_version(self): """ Returns information about the active Moondancer version. """ # TODO: Return the Cynthion software version, or something indicating # the Cynthion API number? raise NotImplementedError() def connect(self, usb_device: USBDevice, max_packet_size_ep0: int=64, device_speed: DeviceSpeed=DeviceSpeed.FULL): """ Prepares Cynthion to connect to the target host and emulate a given device. Args: usb_device : The USBDevice object that represents the device to be emulated. """ if device_speed not in [DeviceSpeed.FULL, DeviceSpeed.HIGH]: log.warning(f"Moondancer only supports USB Full and High Speed. Ignoring requested speed: {device_speed.name}") log.debug(f"moondancer.connect(max_packet_size_ep0:{max_packet_size_ep0}, device_speed:{device_speed}, quirks:{self.quirks})") self.max_packet_size_ep0 = max_packet_size_ep0 # compute our quirk flags quirks = 0 if 'manual_set_address' in self.quirks: log.warning("Handling SET_ADDRESS on the target host side!") quirks |= QuirkFlag.MANUAL_SET_ADDRESS # connect to target host self.api.connect(self.max_packet_size_ep0, device_speed, quirks) self.connected_device = usb_device # get device name device_name = f"{type(self.connected_device).__module__}.{type(self.connected_device).__qualname__}" log.info(f"Connected {device_speed.name} speed device '{device_name}' to target host.") def disconnect(self): """ Disconnects Cynthion from the target host. """ log.info("Disconnecting from target host.") self.device.comms.release_exclusive_access() # disconnect from target host self.api.disconnect() self.connected_device = None def reset(self): """ Triggers the Cynthion to handle its side of a bus reset. """ log.debug(f"moondancer.bus_reset()") self.api.bus_reset() def set_address(self, address: int, defer: bool=False): """ Sets the device address of Moondancer. Usually only used during initial configuration. Args: address : The address that Moondancer should assume. defer : True iff the set_address request should wait for an active transaction to finish. """ log.debug(f"moondancer.set_address({address}, {defer})") self.api.set_address(address, 1 if defer else 0) def configured(self, configuration: USBConfiguration): """ Callback that's issued when a USBDevice is configured, e.g. by the SET_CONFIGURATION request. Allows us to apply the new configuration. Args: configuration : The USBConfiguration object applied by the SET_CONFIG request. """ self.validate_configuration(configuration) log.debug(f"moondancer.configured({configuration})") if configuration is None: log.error("Target host configuration could not be applied.") return # If we need to issue a configuration command, issue one. # (If there are no endpoints other than control, this command will be # empty, and we can skip this.) endpoint_triplets = [] for interface in configuration.get_interfaces(): for endpoint in interface.get_endpoints(): log.debug(f"Configuring endpoint: {endpoint}.") triple = (endpoint.get_address(), endpoint.max_packet_size, endpoint.transfer_type,) endpoint_triplets.append(triple) if len(endpoint_triplets): self.api.configure_endpoints(*endpoint_triplets) for triplet in endpoint_triplets: self.configured_endpoints[triplet[0]] = triplet # save configuration self.configuration = configuration # If we've just set up endpoints, check to see if any of them # have NAKs waiting. nak_status = self.api.get_nak_status() self.handle_ep_in_nak_status(nak_status) log.info("Target host configuration complete.") def read_from_endpoint(self, endpoint_number: int) -> bytes: """ Reads a block of data from the given endpoint. Args: endpoint_number : The number of the OUT endpoint on which data is to be rx'd. """ log.debug(f"moondancer.read_from_endpoint({endpoint_number})") # Read from the given endpoint... data = self.api.read_endpoint(endpoint_number) # Re-enable OUT interface to receive data again... self.api.ep_out_interface_enable() log.trace(f" moondancer.api.read_endpoint({endpoint_number}) -> {len(data)} '{data}'") # Finally, return the result. return data def send_on_control_endpoint(self, endpoint_number: int, in_request: USBControlRequest, data: bytes, blocking: bool=True): """ Sends a collection of USB data in response to a IN control request by the host. Args: endpoint_number : The number of the IN endpoint on which data should be sent. requested_length : The number of bytes requested by the host. data : The data to be sent. blocking : If true, this function should wait for the transfer to complete. """ requested_length = in_request.length self.api.write_control_endpoint(endpoint_number, requested_length, blocking, bytes(data)) log.debug(f"moondancer.send_on_control_endpoint({endpoint_number}, {requested_length}, {len(data)}, {blocking})") log.trace(f" moondancer.api.write_control_endpoint({endpoint_number}, {requested_length}, {blocking}, {len(data)})") def send_on_endpoint(self, endpoint_number: int, data: bytes, blocking: bool=True): """ Sends a collection of USB data on a given endpoint. Args: endpoint_number : The number of the IN endpoint on which data should be sent. data : The data to be sent. blocking : If true, this function will wait for the transfer to complete. """ self.api.write_endpoint(endpoint_number, blocking, bytes(data)) log.debug(f"moondancer.send_on_endpoint({endpoint_number}, {len(data)}, {blocking})") log.trace(f" moondancer.api.write_endpoint({endpoint_number}, {blocking}, {len(data)})") # TODO this is only used by USBProxy - replace with "backend.ep_prime_for_receive" and "backend.send_zlp" def ack_status_stage(self, direction: USBDirection=USBDirection.OUT, endpoint_number:int =0, blocking: bool=False): """ Handles the status stage of a correctly completed control request, by priming the appropriate endpoint to handle the status phase. Args: direction : Determines if we're ACK'ing an IN or OUT vendor request. (This should match the direction of the DATA stage.) endpoint_number : The endpoint number on which the control request occurred. blocking : True if we should wait for the ACK to be fully issued before returning. """ log.debug(f"moondancer.ack_status_stage({direction.name}, {endpoint_number}, {blocking})") if direction == USBDirection.OUT: # HOST_TO_DEVICE # If this was an OUT request, we'll prime the output buffer to # respond with the ZLP expected during the status stage. self.api.write_endpoint(endpoint_number, blocking, bytes([])) log.trace(f" moondancer.api.write_endpoint({endpoint_number}, {blocking}, [])") else: # DEVICE_TO_HOST (IN) # If this was an IN request, we'll need to set up a transfer descriptor # so the status phase can operate correctly. This effectively reads the # zero length packet from the STATUS phase. self.api.ep_out_prime_receive(endpoint_number) log.trace(f" moondancer.api.ep_out_prime_receive({endpoint_number})") def stall_endpoint(self, endpoint_number:int, direction: USBDirection=USBDirection.OUT): """ Stalls the provided endpoint, as defined in the USB spec. Args: endpoint_number : The number of the endpoint to be stalled. """ endpoint_address = (endpoint_number | 0x80) if direction else endpoint_number log.debug(f"Stalling EP{endpoint_number} {USBDirection(direction).name} (0x{endpoint_address:x})") # Mark endpoint number as stalled. self.endpoint_stalled[endpoint_number] = True # Stall endpoint address. if direction: self.api.stall_endpoint_in(endpoint_number) log.debug(f" moondancer.api.stall_endpoint_in({endpoint_number})") else: self.api.stall_endpoint_out(endpoint_number) log.debug(f" moondancer.api.stall_endpoint_out({endpoint_number})") def clear_halt(self, endpoint_number: int, direction: USBDirection): """ Clears a halt condition on the provided non-control endpoint. Args: endpoint_number : The endpoint number direction : The endpoint direction; or OUT if not provided. """ endpoint_address = (endpoint_number | 0x80) if direction else endpoint_number log.debug(f"Clearing halt EP{endpoint_number} {USBDirection(direction).name} (0x{endpoint_address:x})") self.api.clear_feature_endpoint_halt(endpoint_number, direction) log.debug(f" moondancer.api.clear_feature_endpoint_halt({endpoint_number}, {direction})") def service_irqs(self): """ Core routine of the Facedancer execution/event loop. Continuously monitors the Moondancer's execution status, and reacts as events occur. """ # Get latest interrupt events events: List[Tuple[int, int]] = self.api.get_interrupt_events() # Handle interrupt events. if len(events) > 0: # gcp doesn't seem to return a nested tuple if it's only one event if isinstance(events[0], int): events = [ events ] parsed_events = [InterruptEvent(event) for event in events] for event in parsed_events: log.debug(f"MD IRQ => {event}") if event == InterruptEvent.USB_BUS_RESET: self.handle_bus_reset() elif event == InterruptEvent.USB_RECEIVE_CONTROL: self.handle_receive_control(event.endpoint_number) elif event == InterruptEvent.USB_RECEIVE_PACKET and event.endpoint_number == 0: # TODO support endpoints other than EP0 self.handle_receive_control_packet(event.endpoint_number) elif event == InterruptEvent.USB_RECEIVE_PACKET: self.handle_receive_packet(event.endpoint_number) elif event == InterruptEvent.USB_SEND_COMPLETE: self.handle_send_complete(event.endpoint_number) else: log.error(f"Unhandled interrupt event: {event}") # Check EP_IN NAK status for pending data requests else: nak_status = self.api.get_nak_status() if nak_status != 0: self.handle_ep_in_nak_status(nak_status) # - Interrupt event handlers ---------------------------------------------- # USB0_BUS_RESET def handle_bus_reset(self): """ Triggers Moondancer to perform its side of a bus reset. """ if self.connected_device: self.connected_device.handle_bus_reset() else: self.api.bus_reset() # USB0_RECEIVE_CONTROL def handle_receive_control(self, endpoint_number: int): """ Handles a known outstanding control event on a given endpoint. endpoint_number: The endpoint number for which a control event should be serviced. """ log.debug(f"handle_receive_control({endpoint_number})") # HACK: to maintain API compatibility with the existing facedancer API, # we need to know if a stall happens at any point during our handler. self.endpoint_stalled[endpoint_number] = False # Read the data from the SETUP stage... data = bytearray(self.api.read_control()) request = self.connected_device.create_request(data) log.debug(f" moondancer.api.read_control({endpoint_number}) -> {len(data)} '{request}'") is_out = request.get_direction() == USBDirection.OUT # HOST_TO_DEVICE has_data = (request.length > 0) log.trace(f" is_out:{is_out} has_data:{has_data}") # Special case: if this is an OUT request with a data stage, we won't # handle the request until the data stage has been completed. Instead, # we'll stash away the data received in the setup stage, prime the # endpoint for the data stage, and then wait for the data stage to # complete, triggering a corresponding code path in # in handle_transfer_complete_on_endpoint. if is_out and has_data: log.debug(f" setup packet has data - queueing read") self.pending_control_request = request self.api.ep_out_prime_receive(endpoint_number) return # Pass the request to the emulated device for handling. log.trace(f" connected_device.handle_request({request})") self.connected_device.handle_request(request) # If it was an IN request with a data stage we now need to # prime the endpoint to receive a ZLP from the host # acknowledging receipt of our response. if has_data and not is_out and not self.endpoint_stalled[endpoint_number]: log.debug(f" CONTROL IN -> prime ep to receive zlp") self.api.ep_out_prime_receive(endpoint_number) # USB0_RECEIVE_PACKET(0) def handle_receive_control_packet(self, endpoint_number: int): log.debug(f"moondancer.handle_receive_control_packet({endpoint_number}) pending:{self.pending_control_request}") # Handle packet if we don't have a pending control request if not self.pending_control_request: data = self.api.read_endpoint(endpoint_number) if len(data) == 0: # It's a zlp following an IN control transfer, re-enable interface for reception on other endpoints. self.api.ep_out_interface_enable() else: log.error(f"Discarding {len(data)} bytes on control endpoint with no pending control request") return # We have a pending control request with a data stage... # Read the rest of the data from the endpoint, completing the control request. new_data = self.api.read_endpoint(endpoint_number) log.debug(f" handling control data stage: {len(new_data)} bytes") log.trace(f" moondancer.api.read_endpoint({endpoint_number}) -> {len(new_data)}") if len(new_data) == 0: # It's a zlp following a control IN transfer, re-enable interface for reception on other endpoints. self.api.ep_out_interface_enable() log.debug(f"ZLP ending Control IN transfer on ep: {endpoint_number}") return # Append our new data to the pending control request. self.pending_control_request.data.extend(new_data) all_data_received = len(self.pending_control_request.data) == self.pending_control_request.length is_short_packet = len(new_data) < self.max_packet_size_ep0 if all_data_received or is_short_packet: # Handle the completed setup request... self.connected_device.handle_request(self.pending_control_request) # And clear our pending setup data. self.pending_control_request = None # Finally, re-enable interface for reception on other endpoints. self.api.ep_out_interface_enable() return # Finally, re-prime our control endpoint to receive the rest of the control data. self.api.ep_out_prime_receive(endpoint_number) # USB0_RECEIVE_PACKET(1...15) def handle_receive_packet(self, endpoint_number: int): """ Handles a known-completed transfer on a given endpoint. Args: endpoint_number : The endpoint number for which the transfer should be serviced. """ log.debug(f"moondancer.handle_receive_packet({endpoint_number})") # Read the data from the endpoint data = self.api.read_endpoint(endpoint_number) log.trace(f" moondancer.api.read_endpoint({endpoint_number}) -> {len(data)}") # Ignore it if it's a ZLP ack as Facedancer devices don't handle it. if len(data) == 0: # Finally, Prime endpoint to receive again. self.api.ep_out_interface_enable() log.debug(f" ZLP ending Bulk IN transfer on ep: {endpoint_number}") return # Pass it to the device's handler self.connected_device.handle_data_available(endpoint_number, data) # Finally, re-enable other OUT endpoints so we can receive on them again. self.api.ep_out_interface_enable() # USB0_SEND_COMPLETE def handle_send_complete(self, endpoint_number: int): log.debug(f"handle_send_complete({endpoint_number})") pass # Handle pending data requests on EP_IN def handle_ep_in_nak_status(self, nak_status: int): nakked_endpoints = [epno for epno in range(self.SUPPORTED_ENDPOINTS) if (nak_status >> epno) & 1] for endpoint_number in nakked_endpoints: if endpoint_number != 0: log.trace(f"Received IN NAK on ep{endpoint_number}") self.connected_device.handle_nak(endpoint_number) ================================================ FILE: facedancer/backends/raspdancer.py ================================================ # pylint: disable=import-error # # Raspdancer # # Implementation of the Facedancer API that supports direct access to the MAX324x # chip via a RasPi's SoC SPI bus. Emulates talking to a Facedancer, but ignores # the details of the GreatFET protocol. # import os import sys import time from ..core import FacedancerApp from ..backends.MAXUSBApp import MAXUSBApp from ..logging import log class RaspdancerMaxUSBApp(MAXUSBApp): app_name = "MAXUSB" app_num = 0x00 # Not meaningful for us. TODO: Remove! @classmethod def appropriate_for_environment(cls, backend_name): """ Determines if the current environment seems appropriate for using the GoodFET::MaxUSB backend. """ # Only ever try Raspdancer backends if the backend is set to raspdancer; # we don't want to start randomly spamming a system's SPI bus. if backend_name is None or backend_name != "raspdancer": return False # If we're not explicitly trying to use something else, # see if there's a connected Raspdancer. try: rd = Raspdancer() return True except ImportError as e: log.info("Skipping Raspdancer devices, as perquisites aren't installed ({}).".format(e)) return False except: return False def __init__(self, device=None, verbose=0, quirks=None): if device is None: device = Raspdancer(verbose=verbose) FacedancerApp.__init__(self, device, verbose) self.connected_device = None self.enable() if verbose > 0: rev = self.read_register(self.reg_revision) print(self.app_name, "revision", rev) # set duplex and negative INT level (from GoodFEDMAXUSB.py) self.write_register(self.reg_pin_control, self.full_duplex | self.interrupt_level) def init_commands(self): pass def enable(self): for i in range(3): self.device.set_up_comms() if self.verbose > 0: print(self.app_name, "enabled") def ack_status_stage(self, blocking=False): if self.verbose > 5: print(self.app_name, "sending ack!") self.device.transfer(b'\x01') def read_register(self, reg_num, ack=False): if self.verbose > 1: print(self.app_name, "reading register 0x%02x" % reg_num) data = bytearray([ reg_num << 3, 0 ]) if ack: data[0] |= 1 resp = self.device.transfer(data) if self.verbose > 2: print(self.app_name, "read register 0x%02x has value 0x%02x" % (reg_num, resp[1])) return resp[1] def write_register(self, reg_num, value, ack=False): if self.verbose > 2: print(self.app_name, "writing register 0x%02x with value 0x%02x" % (reg_num, value)) data = bytearray([ (reg_num << 3) | 2, value ]) if ack: data[0] |= 1 self.device.transfer(data) def read_bytes(self, reg, n): if self.verbose > 2: print(self.app_name, "reading", n, "bytes from register", reg) data = bytes([ (reg << 3) ] + ([0] * n)) resp = self.device.transfer(data) if self.verbose > 3: print(self.app_name, "read", len(resp) - 1, "bytes from register", reg) return resp[1:] def write_bytes(self, reg, data): data = bytes([ (reg << 3) | 3 ]) + data self.device.transfer(data) if self.verbose > 3: print(self.app_name, "wrote", len(data) - 1, "bytes to register", reg) class Raspdancer(object): """ Extended version of the Facedancer class that accepts a direct SPI connection to the MAX324x chip, as used by the Raspdancer. """ def __init__(self, verbose=0): """ Initializes our connection to the MAXUSB device. """ import spi import RPi.GPIO as GPIO self.verbose = verbose self.buffered_result = b'' self.last_verb = -1 self.spi = spi self.gpio = GPIO self.gpio.setwarnings(False) self.gpio.setmode(self.gpio.BOARD) self.reset() def reset(self): """ Resets the connected MAXUSB chip. """ self.gpio.setup(15, self.gpio.OUT) self.gpio.output(15, self.gpio.LOW) self.gpio.output(15, self.gpio.HIGH) def set_up_comms(self): """ Sets up the Raspdancer to communicate with the MAX324x. """ # pin15=GPIO22 is linked to MAX3420 -RST self.gpio.setup(15, self.gpio.OUT) self.gpio.output(15,self.gpio.LOW) self.gpio.output(15,self.gpio.HIGH) self.spi.openSPI(speed=26000000) def transfer(self, data): """ Emulate the facedancer's write command, which blasts data directly over to the SPI bus. """ if isinstance(data,str): data = [ord(x) for x in data] data = tuple(data) data = self.spi.transfer(data) return bytearray(data) ================================================ FILE: facedancer/classes/__init__.py ================================================ # # This file is part of Facedancer. # """ Support code for USB classes. """ from enum import IntEnum class USBDeviceClass(IntEnum): """ Class representing known USB class numbers. """ COMPOSITE = 0x00 AUDIO = 0x01 COMMUNICATIONS = 0x02 HID = 0x03 PHYSICAL = 0x05 IMAGE = 0x06 PRINTER = 0x07 MASS_STORAGE = 0x08 HUB = 0x09 CDC_DATA = 0x0A SMART_CARD = 0x0B CONTENT_SECURITY = 0x0D VIDEO = 0x0E PERSONAL_HEALTHCARE = 0x0F AUDIO_VIDEO = 0x10 BILLBOARD = 0x11 TYPE_C_BRIDGE = 0x12 DIAGNOSTIC = 0xDC WIRELESS_CONTROLLER = 0xE0 MISCELLANEOUS = 0xEF APPLICATION_SPECIFIC = 0xFE VENDOR_SPECIFIC = 0xFF ================================================ FILE: facedancer/classes/hid/__init__.py ================================================ # # This file is part of Facedancer. # """ Code for implementing HID classes. """ ================================================ FILE: facedancer/classes/hid/descriptor.py ================================================ # # This file is part of Facedancer. # """ Code for implementing HID classes. """ # Support annotations on Python < 3.9 from __future__ import annotations from enum import IntEnum from typing import Tuple, Iterable from ...descriptor import USBDescriptor, USBDescriptorTypeNumber # # Global items. # def _hid_item_generator(constant) -> Tuple[int]: """ Generates a HID descriptor global item entry. """ # See See HID1.1 [6.2.2.1 Items Types and Tags] size_code_map = { 0: 0b00, # No data 1: 0b01, # 1 byte 2: 0b10, # 2 bytes 4: 0b11, # 4 bytes } # Generate a function that creates a item with # the relevant type... def hid_item(*octets): size = len(octets) if size not in size_code_map: raise ValueError( f"HID short item can only have 0, 1, 2 or 4 data bytes, got {size}" ) size_code = size_code_map[size] prefix = constant | size_code return (prefix, *octets) # ... and return it. return hid_item def _io_item_generator(type_constant) -> Tuple[int]: # Generate a function that creates a item with # the relevant type... def hid_io_item( constant=False, variable=False, relative=False, wrap=False, nonlinear=False, preferred_state=True, nullable=False, buffered_bytes=False ): # If we have a buffered bytes byte, include it. item_length = 2 if buffered_bytes else 1 # Build the relevant item. # See HID1.1 [6.2.2.4] item = (1 << 0) if constant else 0 item |= (1 << 1) if variable else 0 item |= (1 << 2) if relative else 0 item |= (1 << 3) if wrap else 0 item |= (1 << 4) if nonlinear else 0 item |= 0 if preferred_state else (1 << 5) item |= (1 << 6) if nullable else 0 # Build the item, and return it. extra = (1,) if buffered_bytes else () return (type_constant | item_length, item, *extra) # ... and return our function. return hid_io_item # # Main items. # INPUT = _io_item_generator(0b1000_00_00) OUTPUT = _io_item_generator(0b1001_00_00) FEATURE = _io_item_generator(0b1011_00_00) COLLECTION = _hid_item_generator(0b1010_00_00) END_COLLECTION = lambda : (0b1100_00_00,) # Note: the odd separation of the last two bits here is due to # the formatting of the USB specification (and due to the fact) # that those bits are overridden, and thus always should be zero. USAGE_PAGE = _hid_item_generator(0b0000_01_00) LOGICAL_MINIMUM = _hid_item_generator(0b0001_01_00) LOGICAL_MAXIMUM = _hid_item_generator(0b0010_01_00) PHYSICAL_MINIMUM = _hid_item_generator(0b0011_01_00) PHYSICAL_MAXIMUM = _hid_item_generator(0b0100_01_00) UNIT_EXPONENT = _hid_item_generator(0b0101_01_00) UNIT = _hid_item_generator(0b0110_01_00) REPORT_SIZE = _hid_item_generator(0b0111_01_00) REPORT_ID = _hid_item_generator(0b1000_01_00) REPORT_COUNT = _hid_item_generator(0b1001_01_00) PUSH = _hid_item_generator(0b1010_01_00) POP = _hid_item_generator(0b1011_01_00) # # Local items. # USAGE = _hid_item_generator(0b0000_10_00) USAGE_MINIMUM = _hid_item_generator(0b0001_10_00) USAGE_MAXIMUM = _hid_item_generator(0b0010_10_00) DESGINATOR_INDEX = _hid_item_generator(0b0011_10_00) DESGINATOR_MINIMUM = _hid_item_generator(0b0100_10_00) DESGINATOR_MAXIMUM = _hid_item_generator(0b0101_10_00) STRING_INDEX = _hid_item_generator(0b0111_10_00) STRING_MINIMUM = _hid_item_generator(0b1000_10_00) STRING_MAXIMUM = _hid_item_generator(0b1001_10_00) DELIMITER = _hid_item_generator(0b1010_10_00) class HIDCollection(IntEnum): """ HID collections; from HID1.1 [6.2.2.4]. """ PHYSICAL = 0x00 APPLICATION = 0x01 LOGICAL = 0x02 REPORT = 0x03 NAMED_ARRAY = 0x04 USAGE_SWITCH = 0x05 USAGE_MODIFIER = 0x06 VENDOR = 0xFF class HIDReportDescriptor(USBDescriptor): """ Descriptor class representing a HID report descriptor. """ # Parameter where the user defines the descriptor's fields. fields: Iterable[bytes] = () # Mark this as a HID report descriptor. type_number : int = USBDescriptorTypeNumber.REPORT raw : None | bytes = None def __call__(self, index=0): """ Converts the descriptor object into raw bytes. """ if self.raw is not None: return self.raw raw = bytearray() # Squish together all of our fields to make a descriptor. for field in self.fields: raw.extend(field) return bytes(raw) ================================================ FILE: facedancer/classes/hid/keyboard.py ================================================ # # This file is part of Facedancer. # """ Helpers for HID keyboards. """ import string from enum import IntEnum, IntFlag # Table mapping ASCII codes to their equivalent HID keycodes. # From the Adafruit HID library; https://github.com/adafruit/Adafruit_CircuitPython_HID/. # Used under copyright exemption (as these are facts, rather than implementation). # # Like their table; the most significant bit is used to indicate whether we should press shift. # _ASCII_TO_KEYCODE = ( b'\x00' # NUL b'\x00' # SOH b'\x00' # STX b'\x00' # ETX b'\x00' # EOT b'\x00' # ENQ b'\x00' # ACK b'\x00' # BEL \a b'\x2a' # BS BACKSPACE \b (called DELETE in the usb.org document) b'\x2b' # TAB \t b'\x28' # LF \n (called Return or ENTER in the usb.org document) b'\x00' # VT \v b'\x00' # FF \f b'\x00' # CR \r b'\x00' # SO b'\x00' # SI b'\x00' # DLE b'\x00' # DC1 b'\x00' # DC2 b'\x00' # DC3 b'\x00' # DC4 b'\x00' # NAK b'\x00' # SYN b'\x00' # ETB b'\x00' # CAN b'\x00' # EM b'\x00' # SUB b'\x29' # ESC b'\x00' # FS b'\x00' # GS b'\x00' # RS b'\x00' # US b'\x2c' # SPACE b'\x9e' # ! x1e|SHIFT_FLAG (shift 1) b'\xb4' # " x34|SHIFT_FLAG (shift ') b'\xa0' # # x20|SHIFT_FLAG (shift 3) b'\xa1' # $ x21|SHIFT_FLAG (shift 4) b'\xa2' # % x22|SHIFT_FLAG (shift 5) b'\xa4' # & x24|SHIFT_FLAG (shift 7) b'\x34' # ' b'\xa6' # ( x26|SHIFT_FLAG (shift 9) b'\xa7' # ) x27|SHIFT_FLAG (shift 0) b'\xa5' # * x25|SHIFT_FLAG (shift 8) b'\xae' # + x2e|SHIFT_FLAG (shift =) b'\x36' # , b'\x2d' # - b'\x37' # . b'\x38' # / b'\x27' # 0 b'\x1e' # 1 b'\x1f' # 2 b'\x20' # 3 b'\x21' # 4 b'\x22' # 5 b'\x23' # 6 b'\x24' # 7 b'\x25' # 8 b'\x26' # 9 b'\xb3' # : x33|SHIFT_FLAG (shift ;) b'\x33' # ; b'\xb6' # < x36|SHIFT_FLAG (shift ,) b'\x2e' # = b'\xb7' # > x37|SHIFT_FLAG (shift .) b'\xb8' # ? x38|SHIFT_FLAG (shift /) b'\x9f' # @ x1f|SHIFT_FLAG (shift 2) b'\x84' # A x04|SHIFT_FLAG (shift a) b'\x85' # B x05|SHIFT_FLAG (etc.) b'\x86' # C x06|SHIFT_FLAG b'\x87' # D x07|SHIFT_FLAG b'\x88' # E x08|SHIFT_FLAG b'\x89' # F x09|SHIFT_FLAG b'\x8a' # G x0a|SHIFT_FLAG b'\x8b' # H x0b|SHIFT_FLAG b'\x8c' # I x0c|SHIFT_FLAG b'\x8d' # J x0d|SHIFT_FLAG b'\x8e' # K x0e|SHIFT_FLAG b'\x8f' # L x0f|SHIFT_FLAG b'\x90' # M x10|SHIFT_FLAG b'\x91' # N x11|SHIFT_FLAG b'\x92' # O x12|SHIFT_FLAG b'\x93' # P x13|SHIFT_FLAG b'\x94' # Q x14|SHIFT_FLAG b'\x95' # R x15|SHIFT_FLAG b'\x96' # S x16|SHIFT_FLAG b'\x97' # T x17|SHIFT_FLAG b'\x98' # U x18|SHIFT_FLAG b'\x99' # V x19|SHIFT_FLAG b'\x9a' # W x1a|SHIFT_FLAG b'\x9b' # X x1b|SHIFT_FLAG b'\x9c' # Y x1c|SHIFT_FLAG b'\x9d' # Z x1d|SHIFT_FLAG b'\x2f' # [ b'\x31' # \ backslash b'\x30' # ] b'\xa3' # ^ x23|SHIFT_FLAG (shift 6) b'\xad' # _ x2d|SHIFT_FLAG (shift -) b'\x35' # ` b'\x04' # a b'\x05' # b b'\x06' # c b'\x07' # d b'\x08' # e b'\x09' # f b'\x0a' # g b'\x0b' # h b'\x0c' # i b'\x0d' # j b'\x0e' # k b'\x0f' # l b'\x10' # m b'\x11' # n b'\x12' # o b'\x13' # p b'\x14' # q b'\x15' # r b'\x16' # s b'\x17' # t b'\x18' # u b'\x19' # v b'\x1a' # w b'\x1b' # x b'\x1c' # y b'\x1d' # z b'\xaf' # { x2f|SHIFT_FLAG (shift [) b'\xb1' # | x31|SHIFT_FLAG (shift \) b'\xb0' # } x30|SHIFT_FLAG (shift ]) b'\xb5' # ~ x35|SHIFT_FLAG (shift `) b'\x4c' # DEL DELETE (called Forward Delete in usb.org document) ) class KeyboardModifiers(IntFlag): MOD_LEFT_CTRL = 0x01 MOD_LEFT_SHIFT = 0x02 MOD_LEFT_ALT = 0x04 MOD_LEFT_META = 0x08 MOD_RIGHT_CTRL = 0x10 MOD_RIGHT_SHIFT = 0x20 MOD_RIGHT_ALT = 0x40 MOD_RIGHT_META = 0x80 class KeyboardKeys(IntEnum): NONE = 0x00 # No key pressed ERR_OVF = 0x01 # Keyboard Error Roll Over - used for all slots if too many keys are pressed ("Phantom key") A = 0x04 # Keyboard a and A B = 0x05 # Keyboard b and B C = 0x06 # Keyboard c and C D = 0x07 # Keyboard d and D E = 0x08 # Keyboard e and E F = 0x09 # Keyboard f and F G = 0x0a # Keyboard g and G H = 0x0b # Keyboard h and H I = 0x0c # Keyboard i and I J = 0x0d # Keyboard j and J K = 0x0e # Keyboard k and K L = 0x0f # Keyboard l and L M = 0x10 # Keyboard m and M N = 0x11 # Keyboard n and N O = 0x12 # Keyboard o and O P = 0x13 # Keyboard p and P Q = 0x14 # Keyboard q and Q R = 0x15 # Keyboard r and R S = 0x16 # Keyboard s and S T = 0x17 # Keyboard t and T U = 0x18 # Keyboard u and U V = 0x19 # Keyboard v and V W = 0x1a # Keyboard w and W X = 0x1b # Keyboard x and X Y = 0x1c # Keyboard y and Y Z = 0x1d # Keyboard z and Z NUM_1 = 0x1e # Keyboard 1 and ! NUM_2 = 0x1f # Keyboard 2 and @ NUM_3 = 0x20 # Keyboard 3 and # NUM_4 = 0x21 # Keyboard 4 and $ NUM_5 = 0x22 # Keyboard 5 and % NUM_6 = 0x23 # Keyboard 6 and ^ NUM_7 = 0x24 # Keyboard 7 and & NUM_8 = 0x25 # Keyboard 8 and * NUM_9 = 0x26 # Keyboard 9 and ( NUM_0 = 0x27 # Keyboard 0 and ) ENTER = 0x28 # Keyboard Return (ENTER) ESC = 0x29 # Keyboard ESCAPE BACKSPACE = 0x2a # Keyboard DELETE (Backspace) TAB = 0x2b # Keyboard Tab SPACE = 0x2c # Keyboard Spacebar MINUS = 0x2d # Keyboard - and _ EQUAL = 0x2e # Keyboard = and + LEFTBRACE = 0x2f # Keyboard [ and { RIGHTBRACE = 0x30 # Keyboard ] and } BACKSLASH = 0x31 # Keyboard \ and | HASHTILDE = 0x32 # Keyboard Non-US # and ~ SEMICOLON = 0x33 # Keyboard ; and : APOSTROPHE = 0x34 # Keyboard ' and " GRAVE = 0x35 # Keyboard ` and ~ COMMA = 0x36 # Keyboard, and < DOT = 0x37 # Keyboard . and > SLASH = 0x38 # Keyboard / and ? CAPSLOCK = 0x39 # Keyboard Caps Lock F1 = 0x3a # Keyboard F1 F2 = 0x3b # Keyboard F2 F3 = 0x3c # Keyboard F3 F4 = 0x3d # Keyboard F4 F5 = 0x3e # Keyboard F5 F6 = 0x3f # Keyboard F6 F7 = 0x40 # Keyboard F7 F8 = 0x41 # Keyboard F8 F9 = 0x42 # Keyboard F9 F10 = 0x43 # Keyboard F10 F11 = 0x44 # Keyboard F11 F12 = 0x45 # Keyboard F12 SYSRQ = 0x46 # Keyboard Print Screen SCROLLLOCK = 0x47 # Keyboard Scroll Lock PAUSE = 0x48 # Keyboard Pause INSERT = 0x49 # Keyboard Insert HOME = 0x4a # Keyboard Home PAGEUP = 0x4b # Keyboard Page Up DELETE = 0x4c # Keyboard Delete Forward END = 0x4d # Keyboard End PAGEDOWN = 0x4e # Keyboard Page Down RIGHT = 0x4f # Keyboard Right Arrow LEFT = 0x50 # Keyboard Left Arrow DOWN = 0x51 # Keyboard Down Arrow UP = 0x52 # Keyboard Up Arrow NUMLOCK = 0x53 # Keyboard Num Lock and Clear KPSLASH = 0x54 # Keypad / KPASTERISK = 0x55 # Keypad * KPMINUS = 0x56 # Keypad - KPPLUS = 0x57 # Keypad + KPENTER = 0x58 # Keypad ENTER KP1 = 0x59 # Keypad 1 and End KP2 = 0x5a # Keypad 2 and Down Arrow KP3 = 0x5b # Keypad 3 and PageDn KP4 = 0x5c # Keypad 4 and Left Arrow KP5 = 0x5d # Keypad 5 KP6 = 0x5e # Keypad 6 and Right Arrow KP7 = 0x5f # Keypad 7 and Home KP8 = 0x60 # Keypad 8 and Up Arrow KP9 = 0x61 # Keypad 9 and Page Up KP0 = 0x62 # Keypad 0 and Insert KPDOT = 0x63 # Keypad . and Delete COMPOSE = 0x65 # Keyboard Application POWER = 0x66 # Keyboard Power KPEQUAL = 0x67 # Keypad = F13 = 0x68 # Keyboard F13 F14 = 0x69 # Keyboard F14 F15 = 0x6a # Keyboard F15 F16 = 0x6b # Keyboard F16 F17 = 0x6c # Keyboard F17 F18 = 0x6d # Keyboard F18 F19 = 0x6e # Keyboard F19 F20 = 0x6f # Keyboard F20 F21 = 0x70 # Keyboard F21 F22 = 0x71 # Keyboard F22 F23 = 0x72 # Keyboard F23 F24 = 0x73 # Keyboard F24 OPEN = 0x74 # Keyboard Execute HELP = 0x75 # Keyboard Help PROPS = 0x76 # Keyboard Menu FRONT = 0x77 # Keyboard Select STOP = 0x78 # Keyboard Stop AGAIN = 0x79 # Keyboard Again UNDO = 0x7a # Keyboard Undo CUT = 0x7b # Keyboard Cut COPY = 0x7c # Keyboard Copy PASTE = 0x7d # Keyboard Paste FIND = 0x7e # Keyboard Find MUTE = 0x7f # Keyboard Mute VOLUMEUP = 0x80 # Keyboard Volume Up VOLUMEDOWN = 0x81 # Keyboard Volume Down KPCOMMA = 0x85 # Keypad Comma RO = 0x87 # Keyboard International1 KATAKANAHIRAGANA = 0x88 # Keyboard International2 YEN = 0x89 # Keyboard International3 HENKAN = 0x8a # Keyboard International4 MUHENKAN = 0x8b # Keyboard International5 KPJPCOMMA = 0x8c # Keyboard International6 HANGEUL = 0x90 # Keyboard LANG1 HANJA = 0x91 # Keyboard LANG2 KATAKANA = 0x92 # Keyboard LANG3 HIRAGANA = 0x93 # Keyboard LANG4 ZENKAKUHANKAKU = 0x94 # Keyboard LANG5 #SYSRQ = 0x9a # Keyboard SysReq/Attention KEYPAD_00 = 0xb0 # Keypad 00 KEYPAD_000 = 0xb1 # Keypad 000 KPLEFTPAREN = 0xb6 # Keypad ( KPRIGHTPAREN = 0xb7 # Keypad ) LEFTCTRL = 0xe0 # Keyboard Left Control LEFTSHIFT = 0xe1 # Keyboard Left Shift LEFTALT = 0xe2 # Keyboard Left Alt LEFTMETA = 0xe3 # Keyboard Left GUI RIGHTCTRL = 0xe4 # Keyboard Right Control RIGHTSHIFT = 0xe5 # Keyboard Right Shift RIGHTALT = 0xe6 # Keyboard Right Alt RIGHTMETA = 0xe7 # Keyboard Right GUI MEDIA_PLAYPAUSE = 0xe8 MEDIA_STOPCD = 0xe9 MEDIA_PREVIOUSSONG = 0xea MEDIA_NEXTSONG = 0xeb MEDIA_EJECTCD = 0xec MEDIA_VOLUMEUP = 0xed MEDIA_VOLUMEDOWN = 0xee MEDIA_MUTE = 0xef MEDIA_WWW = 0xf0 MEDIA_BACK = 0xf1 MEDIA_FORWARD = 0xf2 MEDIA_STOP = 0xf3 MEDIA_FIND = 0xf4 MEDIA_SCROLLUP = 0xf5 MEDIA_SCROLLDOWN = 0xf6 MEDIA_EDIT = 0xf7 MEDIA_SLEEP = 0xf8 MEDIA_COFFEE = 0xf9 MEDIA_REFRESH = 0xfa MEDIA_CALC = 0xfb @classmethod def get_scancode_for_ascii(cls, letter_or_code): """ Returns the (modifiers, scancode) used to type a given ASCII letter. """ # Look up the relevant ASCII code in our table. ascii_code = letter_or_code if isinstance(letter_or_code, int) else ord(letter_or_code) composite = _ASCII_TO_KEYCODE[ascii_code] # The Adafruit table uses bits [6:0] to indicate keycode; and bit [7] to indicate # if shift is necessary. modifiers = KeyboardModifiers.MOD_LEFT_SHIFT if (composite & 0x80) else 0 scancode = composite & 0x7F return (modifiers, scancode) ================================================ FILE: facedancer/classes/hid/usage.py ================================================ # # This file is part of Facedancer. # """ Code for working with HID usages. """ from enum import IntEnum class HIDUsagePage(IntEnum): """ HID Usage Page numbers; from USB HID Usage Tables [Table 1]. """ GENERIC_DESKTOP = 0x01 SIMULATION = 0x02 VR = 0x03 SPORT = 0x04 GAME = 0x05 GENERIC = 0x06 KEYBOARD = 0x07 LEDS = 0x08 BUTTONS = 0x09 ORDINAL = 0x0A TELEPHONY = 0x0B CONSUMER = 0x0C DIGITIZER = 0x0D PID = 0x0F UNICODE = 0x10 ALPHANUMERIC_DISPLAY = 0x14 MEDICAL_INSTRUMENTS = 0x40 BARCODE_SCANNER = 0x8C SCALE = 0x8D MAGNETIC_STRIPE = 0x8E CAMERA_CONTROL = 0x90 ARCADE = 0x91 VENDOR_DEFINED = 0xFFFF class HIDGenericDesktopUsage(IntEnum): """ HID Usages for Generic Desktop Control; from [Table 6]. """ POINTER = 0x01 MOUSE = 0x02 JOYSTICK = 0x04 GAMEPAD = 0x05 KEYBOARD = 0x06 KEYPAD = 0x07 MULTIAXIS_CONTROLLER = 0x08 TABLET_PC_SYSTEM_CONTROLS = 0x09 X = 0x30 Y = 0x31 Z = 0x32 RX = 0x33 RY = 0x34 RZ = 0x35 SLIDER = 0x36 DIAL = 0x37 WHEEL = 0x38 HAT_SWITCH = 0x39 COUNTED_BUFFER = 0x3A BYTE_COUNT = 0x3B MOTION_WAKEUP = 0x3C START = 0x3D SELECT = 0x3E VX = 0x40 VY = 0x41 VZ = 0x42 VBRX = 0x43 VBRY = 0x44 VBRZ = 0x45 VNO = 0x46 FEATURE_NOTIFICATION = 0x47 RESOLUTION_MULTIPLIER = 0x48 SYSTEM_CONTROL = 0x80 SYSTEM_POWER_DOWN = 0x81 SYSTEM_SLEEP = 0x82 SYSTEM_WAKE_UP = 0x83 SYSTEM_CONTEXT_MENU = 0x84 SYSTEM_MAIN_MENU = 0x85 SYSTEM_APP_MENU = 0x86 SYSTEM_MENU_HELP = 0x87 SYSTEM_MENU_EXIT = 0x88 SYSTEM_MENU_SELECT = 0x89 SYSTEM_MENU_RIGHT = 0x8A SYSTEM_MENU_LEFT = 0x8B SYSTEM_MENU_UP = 0x8C SYSTEM_MENU_DOWN = 0x8D SYSTEM_COLD_RESTART = 0x8E SYSTEM_WARM_UP = 0x8F DPAD_UP = 0x90 DPAD_DOWN = 0x91 DPAD_RIGHT = 0x92 DPAD_LEFT = 0x93 SYSTEM_DOCK = 0xA0 SYSTEM_UNDOCK = 0xA1 SYSTEM_SETUP = 0xA2 SYSTEM_BREAK = 0xA3 SYSTEM_DEBUGGER_BREAK = 0xA4 APPLICATION_BREAK = 0xA5 APPLICATION_DEBUGGER_BREAK = 0xA6 SYSTEM_SPEAKER_MUTE = 0xA7 SYSTEM_HIBERNATE = 0xA8 SYSTEM_DISPLAY_INVERT = 0xB0 SYSTEM_DISPLAY_INTERNAL = 0xB1 SYSTEM_DISPLAY_EXTERNAL = 0xB2 SYSTEM_DISPLAY_BOTH = 0xB3 SYSTEM_DISPLAY_DUAL = 0xB4 SYSTEM_DISPLAY_TOGGLE = 0xB5 SYSTEM_DISPLAY_SWAP = 0xB6 SYSTEM_DISPLAY_AUTOSCALE = 0xB7 ================================================ FILE: facedancer/configuration.py ================================================ # # This file is part of Facedancer. # """ Functionality for describing USB device configurations. """ import struct import textwrap from dataclasses import field from typing import Iterable from .types import USBDirection from .magic import instantiate_subordinates, AutoInstantiable from .request import USBRequestHandler from .interface import USBInterface from .descriptor import USBDescribable, USBDescriptor, StringRef from .endpoint import USBEndpoint class USBConfiguration(USBDescribable, AutoInstantiable, USBRequestHandler): """ Class representing a USBDevice's configuration. Fields: number: The configuration's number; one-indexed. configuration_string A string describing the configuration; or None if not provided. max_power: The maximum power expected to be drawn by the device when using this interface, in mA. Typically 500mA, for maximum possible. supports_remote_wakeup: True iff this device should be able to wake the host from suspend. """ DESCRIPTOR_TYPE_NUMBER = 0x02 DESCRIPTOR_SIZE_BYTES = 9 number : int = 1 configuration_string : StringRef = None max_power : int = 500 self_powered : bool = True supports_remote_wakeup : bool = True parent : USBDescribable = None interfaces : USBInterface = field(default_factory=dict) @classmethod def from_binary_descriptor(cls, data, strings={}): """ Generates a new USBConfiguration object from a configuration descriptor, handling any attached subordinate descriptors. Args: data: The raw bytes for the descriptor to be parsed. """ length = data[0] # Unpack the main collection of data into the descriptor itself. descriptor_type, total_length, num_interfaces, index, string_index, \ attributes, half_max_power = struct.unpack_from('> 6) & 1), supports_remote_wakeup=bool((attributes >> 5) & 1), ) data = data[length:total_length] last_interface = None last_endpoint = None # Continue parsing until we run out of descriptors. while data: # Determine the length and type of the next descriptor. length = data[0] descriptor = USBDescribable.from_binary_descriptor(data[:length], strings=strings) # If we have an interface descriptor, add it to our list of interfaces. if isinstance(descriptor, USBInterface): configuration.add_interface(descriptor) last_interface = descriptor last_endpoint = None elif isinstance(descriptor, USBEndpoint): last_interface.add_endpoint(descriptor) last_endpoint = descriptor elif isinstance(descriptor, USBDescriptor): descriptor.include_in_config = True if len(last_interface.endpoints) == 0: last_interface.add_descriptor(descriptor) else: last_endpoint.add_descriptor(descriptor) # Move on to the next descriptor. data = data[length:] return configuration def __post_init__(self): self.configuration_string = StringRef.ensure(self.configuration_string) # Gather any interfaces attached to the configuration. for interface in instantiate_subordinates(self, USBInterface): self.add_interface(interface) @property def attributes(self): """ Retrives the "attributes" composite word. """ # Start off with the required bits set to one... attributes = 0b10000000 # ... and then add in our attributes. attributes |= (1 << 6) if self.self_powered else 0 attributes |= (1 << 5) if self.supports_remote_wakeup else 0 return attributes # # User API. # def get_device(self): """ Returns a reference to the associated device.""" return self.parent def add_interface(self, interface: USBInterface): """ Adds an interface to the configuration. """ identifier = interface.get_identifier() num, alt = identifier if identifier in self.interfaces: other = self.interfaces[identifier] iface_name = type(interface).__name__ other_name = type(other).__name__ raise Exception( f"Interface of type {iface_name} cannot be added to this " f"configuration because there is already an interface of " f"type {other_name} with the same interface number {num} " f"and alternate setting {alt}") else: self.interfaces[identifier] = interface interface.parent = self def get_endpoint(self, number: int, direction: USBDirection) -> USBEndpoint: """ Attempts to find an endpoint with the given number + direction. Args: number : The endpoint number to look for. direction : Whether to look for an IN or OUT endpoint. """ # Search each of our interfaces for the relevant endpoint. for interface in self.active_interfaces.values(): endpoint = interface.get_endpoint(number, direction) if endpoint is not None: return endpoint # If none have one, return None. return None # # Event handlers. # def handle_data_received(self, endpoint: USBEndpoint, data: bytes): """ Handler for receipt of non-control request data. Typically, this method will delegate any data received to the appropriate configuration/interface/endpoint. If overridden, the overriding function will receive all data; and can delegate it by calling the `.handle_data_received` method on `self.configuration`. Args: endpoint : The endpoint on which the data was received. data : The raw bytes received on the relevant endpoint. """ for interface in self.active_interfaces.values(): if interface.has_endpoint(endpoint.number, direction=USBDirection.OUT): interface.handle_data_received(endpoint, data) return # If no interface owned the targeted endpoint, consider the data unexpected. self.get_device().handle_unexpected_data_received(endpoint.number, data) def handle_data_requested(self, endpoint: USBEndpoint): """ Handler called when the host requests data on a non-control endpoint. Typically, this method will delegate the request to the appropriate interface+endpoint. If overridden, the overriding function will receive all data. Args: endpoint : The endpoint on which the host requested data. """ for interface in self.active_interfaces.values(): if interface.has_endpoint(endpoint.number, direction=USBDirection.IN): interface.handle_data_requested(endpoint) return # If no one interface owned the targeted endpoint, consider the data unexpected. self.get_device().handle_unexpected_data_requested(endpoint.number) def handle_buffer_empty(self, endpoint: USBEndpoint): """ Handler called when a given endpoint first has an empty buffer. Often, an empty buffer indicates an opportunity to queue data for sending ('prime an endpoint'), but doesn't necessarily mean that the host is planning on reading the data. This function is called only once per buffer. """ for interface in self.active_interfaces.values(): if interface.has_endpoint(endpoint.number, direction=USBDirection.IN): interface.handle_buffer_empty(endpoint) return # # Backend interface functions. # def get_interfaces(self) -> Iterable[USBInterface]: """ Returns an iterable over all interfaces on the provided device. """ return self.interfaces.values() def get_descriptor(self) -> bytes: """ Returns this configuration's configuration descriptor, including subordinates. """ interface_descriptors = bytearray() # FIXME: use construct # All all subordinate descriptors together to create a big subordinate descriptor. for interface in self.interfaces.values(): interface_descriptors += interface.get_descriptor() total_len = len(interface_descriptors) + 9 string_manager = self.get_device().strings # Build the core interface descriptor. d = bytes([ 9, # length of descriptor in bytes 2, # descriptor type 2 == configuration total_len & 0xff, (total_len >> 8) & 0xff, len(set(interface.number for interface in self.interfaces.values())), self.number, string_manager.get_index(self.configuration_string), self.attributes, self.max_power // 2 ]) return d + interface_descriptors # # Interfacing functions for AutoInstantiable. # def get_identifier(self) -> int: return self.number # # Backend functions for our RequestHandler class. # def _request_handlers(self) -> Iterable[callable]: return () def _get_subordinate_handlers(self) -> Iterable[USBInterface]: return self.interfaces.values() def generate_code(self, name=None, indent=0): if name is None: name = f"Configuration_{self.number}" code = f""" class {name}(USBConfiguration): number = {self.number} configuration_string = {self.configuration_string.generate_code()} max_power = {self.max_power} self_powered = {repr(self.self_powered)} supports_remote_wakeup = {repr(self.supports_remote_wakeup)} """ for interface in self.interfaces.values(): code += interface.generate_code(indent=4) return textwrap.indent(code, indent * ' ') ================================================ FILE: facedancer/core.py ================================================ # Facedancer.py # # Contains the core methods for working with a facedancer, inclduing methods # necessary for autodetection. # and GoodFETMonitorApp. import os from .errors import * from .logging import log def FacedancerUSBApp(verbose=0, quirks=None): """ Convenience function that automatically creates a FacedancerApp based on the BOARD environment variable and some crude internal automagic. Args: verbose : Sets the verbosity level of the relevant app. Increasing this from zero yields progressively more output. """ return FacedancerApp.autodetect(verbose, quirks) class FacedancerApp: app_name = "override this" app_num = 0x00 @classmethod def autodetect(cls, verbose=0, quirks=None): """ Convenience function that automatically creates the appropriate subclass based on the BOARD environment variable and some crude internal automagic. Args: verbose: Sets the verbosity level of the relevant app. Increasing this from zero yields progressively more output. """ if 'BACKEND' in os.environ: backend_name = os.environ['BACKEND'].lower() else: backend_name = None # Iterate over each subclass of FacedancerApp until we find one # that seems appropriate. subclass = cls._find_appropriate_subclass(backend_name) if subclass: if verbose > 0: log.info("Using {} backend.".format(subclass.app_name)) return subclass(verbose=verbose, quirks=quirks) else: log.error("FacedancerApp failed to autodetect any Facedancer devices.") log.error("Try specifying a backend with: BACKEND=\"\" ") raise DeviceNotFoundError("FacedancerApp failed to autodetect any Facedancer devices.") @classmethod def _find_appropriate_subclass(cls, backend_name): # Recursive case: if we have any subnodes, see if they are # feed them to this function. for subclass in cls.__subclasses__(): # Check to see if the subnode has any appropriate children. appropriate_class = subclass._find_appropriate_subclass(backend_name) # If it does, that's our answer! if appropriate_class: return appropriate_class # Base case: check the current node. if cls.appropriate_for_environment(backend_name): return cls else: return None @classmethod def appropriate_for_environment(cls, backend_name=None): """ Returns true if the current class is likely to be the appropriate class to connect to a facedancer given the board_name and other environmental factors. Args: backend_name : The name of the backend, as typically retrieved from the BACKEND environment variable, or None to try figuring things out based on other environmental factors. """ return False def __init__(self, device, verbose=0): self.device = device self.verbose = verbose self.init_commands() if self.verbose > 0: log.info(self.app_name, "initialized") def init_commands(self): pass def enable(self): pass def FacedancerUSBHostApp(verbose=0, quirks=None): """ Convenience function that automatically creates a FacedancerApp based on the BOARD environment variable and some crude internal automagic. verbose: Sets the verbosity level of the relevant app. Increasing this from zero yields progressively more output. """ return FacedancerUSBHost.autodetect(verbose, quirks) class FacedancerUSBHost: """ Base class for Facedancer host connections-- extended to provide actual connections to each host. """ # TODO: remove this redundancy; these should be somewhere common # Endpoint directions ENDPOINT_DIRECTION_OUT = 0x00 ENDPOINT_DIRECTION_IN = 0x80 # Endpoint types ENDPOINT_TYPE_CONTROL = 0 # Packet IDs PID_SETUP = 2 PID_OUT = 0 PID_IN = 1 # USB Request Types REQUEST_TYPE_STANDARD = 0 REQUEST_TYPE_CLASS = 1 REQUEST_TYPE_VENDOR = 2 REQUEST_TYPE_RESERVED = 3 # USB Request Recipients REQUEST_RECIPIENT_DEVICE = 0 REQUEST_RECIPIENT_INTERFACE = 1 REQUEST_RECIPIENT_ENDPOINT = 2 REQUEST_RECIPIENT_OTHER = 3 # USB Standard Requests STANDARD_REQUEST_GET_STATUS = 0 STANDARD_REQUEST_SET_ADDRESS = 5 STANDARD_REQUEST_GET_DESCRIPTOR = 6 STANDARD_REQUEST_SET_CONFIGURATION = 9 @classmethod def autodetect(cls, verbose=0, quirks=None): """ Convenience function that automatically creates the appropriate subclass based on the BOARD environment variable and some crude internal automagic. Args: verbose: Sets the verbosity level of the relevant app. Increasing this from zero yields progressively more output. """ # TODO: Filter this out into some kind of autodetecting base class... if 'BACKEND' in os.environ: backend_name = os.environ['BACKEND'].lower() else: backend_name = None # Iterate over each subclass of FacedancerApp until we find one # that seems appropriate. subclass = cls._find_appropriate_subclass(backend_name) if subclass: if verbose > 0: log.info("Using {} backend.".format(subclass.app_name)) return subclass(verbose=verbose, quirks=quirks) else: log.error("FacedancerUSBHost failed to autodetect any Facedancer devices.") log.error("Try specifying a backend with: BACKEND=\"\" ") raise DeviceNotFoundError("FacedancerUSBHost failed to autodetect any Facedancer devices.") @classmethod def _find_appropriate_subclass(cls, backend_name): # TODO: Filter this out into some kind of autodetecting base class... # Recursive case: if we have any subnodes, see if they are # feed them to this function. for subclass in cls.__subclasses__(): # Check to see if the subnode has any appropriate children. appropriate_class = subclass._find_appropriate_subclass(backend_name) # If it does, that's our answer! if appropriate_class: return appropriate_class # Base case: check the current node. if cls.appropriate_for_environment(backend_name): return cls else: return None @classmethod def appropriate_for_environment(cls, backend_name=None): """ Returns true if the current class is likely to be the appropriate class to connect to a facedancer given the board_name and other environmental factors. Args: backend_name : The name of the backend, as typically retrieved from the BACKEND environment variable, or None to try figuring things out based on other environmental factors. """ return False @classmethod def _build_request_type(cls, is_in, req_type, recipient): """ Builds the request type field for a USB request. Args: is_in : True iff this is a DEVICE-to-HOST request. req_type : The type of request to be used. recipient : The context in which this request should be interpreted. Returns : a request_type byte """ request_type = 0 if is_in: request_type |= cls.ENDPOINT_DIRECTION_IN request_type |= (req_type << 5) request_type |= (recipient) return request_type @classmethod def _build_setup_request(cls, is_in, request_type, recipient, request, value, index, length): """ Builds a setup request packet from the standard USB request fields. """ # Fields: # uint8_t request_type; # uint8_t request; # uint16_t value; # uint16_t index; # uint16_t length; def split(value): value_high = value >> 8 value_low = value & 0xFF return [value_low, value_high] setup_request = [cls._build_request_type(is_in, request_type, recipient), request] setup_request.extend(split(value)) setup_request.extend(split(index)) setup_request.extend(split(length)) return setup_request def control_request_in(self, request_type, recipient, request, value=0, index=0, length=0): """ Performs an IN control request. Args: request_type : Determines if this is a standard, class, or vendor request. Accepts a REQUEST_TYPE_* constant. recipient : Determines the context in which this command is interpreted. Accepts a REQUEST_RECIPIENT_* constant. request : The request number to be performed. value, index : The standard USB request arguments, to be included in the setup packet. Their meaning varies depending on the request. length : The maximum length of data expected in response, or 0 if we don't expect any data back. """ # Create the raw setup request, and send it. setup_request = self._build_setup_request(True, request_type, recipient, request, value, index, length) if self.verbose > 4: log.info("Issuing setup packet: {}".format(setup_request)) self.send_on_endpoint(0, setup_request, True, data_packet_pid=0) if self.verbose > 4: log.info("Done.") # If we have a data stage, issue it: if length: if self.verbose > 4: log.info("Reading response... ") data = self.read_from_endpoint(0, length, data_packet_pid=1) if self.verbose > 4: log.info("Got response: {}".format(data)) # and give the host an opportunity to ACK by sending a ZLP. self.send_on_endpoint(0, [], data_packet_pid=1) return data else: self.read_from_endpoint(0, 0, data_packet_pid=1) def control_request_out(self, request_type, recipient, request, value=0, index=0, data=[]): """ Performs an OUT control request. Args: request_type : Determines if this is a standard, class, or vendor request. Accepts a REQUEST_TYPE_* constant. recipient : Determines the context in which this command is interpreted. Accepts a REQUEST_RECIPIENT_* constant. request : The request number to be performed. value, index : The standard USB request arguments, to be included in the setup packet. Their meaning varies depending on the request. data : The data to be transmitted with this control request. """ # Create the raw setup request, and send it. setup_request = self._build_setup_request(False, request_type, recipient, request, value, index, len(data)) self.send_on_endpoint(0, setup_request, True) # If we have a data stage, issue it: if data: self.send_on_endpoint(0, data) # And try to read a ZLP from the host for ACK'ing purposes. self.read_from_endpoint(0, 0, data_packet_pid=1) def initialize_device(self, apply_configuration=0, assign_address=0): """ Sets up a connection to a directly-attached USB device. Args: apply_configuration : If non-zero, the configuration with the given index will be applied to the relevant device. assign_address : If non-zero, the device will be assigned the given address as part of the enumeration/initialization process. """ # TODO: support timeouts in waiting for a connection # Repeatedly attempt to connect to any connected devices. while not self.device_is_connected(): self.bus_reset() # Assume the default device addresses, and read the device's speed. self.last_device_address = 0 self.last_device_speed = self.current_device_speed() # Set up the device to work. if self.verbose > 3: log.info("Initializing control endpoint...") self.initialize_control_endpoint() # Try to ask the device for its maximum packet size on EP0. self.last_ep0_max_packet_size = self.read_ep0_max_packet_size() # If we've been asked to assign an address, # set the device's address, and reinitialize the control endpoint # with the updated address. if assign_address: self.set_address(assign_address) self.initialize_control_endpoint(max_packet_size=self.last_ep0_max_packet_size) # If we're auto-configuring the device, read the full configuration descriptor, # assign the first configuration, and then set up endpoints accordingly if apply_configuration: self.apply_configuration(apply_configuration) def get_descriptor(self, descriptor_type, descriptor_index, language_id, max_length): """ Reads up to max_length bytes of a device's descriptors. """ return self.control_request_in( self.REQUEST_TYPE_STANDARD, self.REQUEST_RECIPIENT_DEVICE, self.STANDARD_REQUEST_GET_DESCRIPTOR, (descriptor_type << 8) | descriptor_index, language_id, max_length) def get_device_descriptor(self, max_length=18): """ Returns the device's device descriptor. """ from .device import USBDevice raw_descriptor = self.get_descriptor(USBDevice.DESCRIPTOR_TYPE_NUMBER, 0, 0, max_length) return USBDevice.from_binary_descriptor(raw_descriptor) def read_ep0_max_packet_size(self): """ Returns the device's reported maximum packet size on EP0, in a way appropriate for an barely-configured endpoint. """ device_descriptor = self.get_device_descriptor(max_length=8) return device_descriptor.max_packet_size_ep0 def get_configuration_descriptor(self, index=0, include_subordinates=True): """ Returns the device's configuration descriptor. Args: include_subordinate : if true, subordinate descriptors will also be returned """ from .configuration import USBConfiguration # Read just the raw configuration descriptor. raw_descriptor = self.get_descriptor(USBConfiguration.DESCRIPTOR_TYPE_NUMBER, index, 0, USBConfiguration.DESCRIPTOR_SIZE_BYTES) # If we want to include the subordinate descriptors, read-read the configuration descriptor with an updated length. if include_subordinates: from struct import unpack total_descriptor_lengths = unpack(' string descriptors. self.descriptors = {} # Maps python strings => indexes. self.indexes = {} def add_string(self, string, index=None): """Add a Python string as a new string descriptor, and return an index. The specified index is used for the new string descriptor, overwriting any previous descriptor with the same index. If an index is not specified, a new, unique, incrementing index is allocated. """ if isinstance(string, StringRef): index = string.index string = string.string if index is None: index = self.next_index if index in self.descriptors: old_string = self.descriptors[index].python_string self.indexes.pop(old_string) self.descriptors[index] = USBStringDescriptor.from_string(string, index=index) self.indexes[string] = index while self.next_index in self.descriptors: self.next_index += 1 return index def get_index(self, string): """ Returns the index of the given string; creating it if the string isn't already known. """ # If we already have an index, leave it alone... if isinstance(string, StringRef): if string.index is not None: return string.index else: string = string.string elif isinstance(string, int): return string # Special case: return 0 for None, allowing null strings to be represented. if string is None: return 0 if string in self.indexes: return self.indexes[string] return self.add_string(string) def __getitem__(self, index): """ Gets the relevant string descriptor. """ if isinstance(index, str): index = self.get_index(index) return self.descriptors.get(index, None) class USBDescriptorTypeNumber(IntEnum): DEVICE = 1 CONFIGURATION = 2 STRING = 3 INTERFACE = 4 ENDPOINT = 5 DEVICE_QUALIFIER = 6 OTHER_SPEED_CONFIGURATION = 7 INTERFACE_POWER = 8 HID = 33 REPORT = 34 def include_in_config(cls): """ Decorator that marks a descriptor to be included in configuration data. """ return adjust_defaults(cls, include_in_config=True) def requestable(type_number, number): """ Decorator that marks a descriptor as requestable. """ return lambda cls: adjust_defaults(cls, type_number=type_number, number=number) ================================================ FILE: facedancer/device.py ================================================ # # This file is part of Facedancer. # """ Functionality for defining USB devices. """ # Support annotations on Python < 3.9 from __future__ import annotations import sys import asyncio import struct import warnings import itertools from typing import Coroutine, Dict, Iterable, Union from dataclasses import field from prompt_toolkit import HTML, print_formatted_text from .core import FacedancerUSBApp from .errors import EndEmulation from .types import DescriptorTypes, LanguageIDs, USBStandardRequests from .types import USBDirection, USBRequestType, USBRequestRecipient from .types import DeviceSpeed from .magic import instantiate_subordinates from .descriptor import USBDescribable, USBDescriptor, StringDescriptorManager, StringRef from .configuration import USBConfiguration from .interface import USBInterface from .endpoint import USBEndpoint from .request import USBControlRequest, USBRequestHandler from .request import standard_request_handler, to_device, get_request_handler_methods from .logging import log class USBBaseDevice(USBDescribable, USBRequestHandler): """ Base-most class for Facedancer USB devices. This version is very similar to the USBDevice type, except that it does not define _any_ standard handlers. This allows you the freedom to declare whatever standard requests you'd like. Fields: vendor_id, product_id : The USB vendor and product ID for this device. manufacturer_string, product_string, serial_number_string : Python strings identifying the device to the USB host. device_class, device_subclass, protocol_revision_number : The USB descriptor fields that select the class, subclass, and protocol. supported_languages : A tuple containing all of the language IDs supported by the device. device_revision : Number indicating the hardware revision of this device. Typically BCD. usb_spec_revision : Number indicating the version of the USB specification we adhere to. Typically 0x0200. device_speed : Specify the device speed for boards that support multiple interface speeds. """ DESCRIPTOR_TYPE_NUMBER = 0x01 DESCRIPTOR_LENGTH = 0x12 name : str = "generic device" device_class : int = 0 device_subclass : int = 0 protocol_revision_number : int = 0 max_packet_size_ep0 : int = 64 vendor_id : int = 0x610b product_id : int = 0x4653 manufacturer_string : StringRef = StringRef.field(string="Facedancer") product_string : StringRef = StringRef.field(string="Generic USB Device") serial_number_string : StringRef = StringRef.field(string="S/N 3420E") # I feel bad for putting this as the default language ID / propagating anglocentrism, # but this appears to be the only language ID supported by some systems, so here it is. supported_languages : tuple = (LanguageIDs.ENGLISH_US,) device_revision : int = 0 usb_spec_version : int = 0x0200 device_speed : DeviceSpeed = None # Descriptors that can be requested with the GET_DESCRIPTOR request. requestable_descriptors : Dict[tuple[int, int], Union[bytes, callable]] = field(default_factory=dict) configurations : Dict[int, USBConfiguration] = field(default_factory=dict) backend : FacedancerUSBApp = None @classmethod def from_binary_descriptor(cls, data, strings={}): """ Creates a USBBaseDevice object from its descriptor. """ data = bytes(data) # Pad the descriptor out with zeroes to the full length of a configuration descriptor. if len(data) < cls.DESCRIPTOR_LENGTH: padding_necessary = cls.DESCRIPTOR_LENGTH - len(data) data += b"\0" * padding_necessary # Parse the core descriptor into its components... spec_version, device_class, device_subclass, device_protocol, \ max_packet_size_ep0, vendor_id, product_id, device_rev, \ manufacturer_string_index, product_string_index, \ serial_number_string_index, num_configurations = struct.unpack_from(" USBEndpoint: """ Attempts to find a subordinate endpoint matching the given number/direction. Args: endpoint_number : The endpoint number to search for. direction : The endpoint direction to be matched. Returns: The matching endpoint; or None if no matching endpoint existed. """ if self.configuration: endpoint = self.configuration.get_endpoint(endpoint_number, direction) if endpoint is None: log.error(f"Requested non-existent endpoint EP{endpoint_number}/{direction.name} for configured device!") return endpoint else: log.error(f"Requested endpoint EP{endpoint_number}/{direction.name} for unconfigured device!") return None # # Backend interface helpers. # def create_request(self, raw_data: bytes) -> USBControlRequest: return USBControlRequest.from_raw_bytes(raw_data, device=self) # # Backend / low-level event receivers. # def handle_nak(self, ep_num: int): """ Backend data-requested handler; for legacy compatibility. Prefer overriding handle_data_requested() and handle_unexpected_data_Requested """ endpoint = self.get_endpoint(ep_num, USBDirection.IN) if endpoint: self.handle_data_requested(endpoint) else: self.handle_unexpected_data_requested(ep_num) def handle_buffer_available(self, ep_num): """ Backend data-buffer-empty handler; for legacy compatibility. Prefer overriding handle_buffer_available(). """ endpoint = self.get_endpoint(ep_num, USBDirection.IN) if endpoint: self.handle_buffer_empty(endpoint) def handle_data_available(self, ep_num, data): """ Backend data-available handler; for legacy compatibility. Prefer overriding handle_data_received(). """ endpoint = self.get_endpoint(ep_num, USBDirection.OUT) if endpoint: self.handle_data_received(endpoint, data) else: self.handle_unexpected_data_received(ep_num, data) # # Event handlers. # def handle_bus_reset(self): """ Event handler for a bus reset. """ log.info("Host issued a bus reset; resetting our connection.") # Clear our state back to address zero and no configuration. self.configuration = None self.address = 0 self.backend.reset() def handle_request(self, request: USBControlRequest): """ Core control request handler. This function can be overridden by a subclass if desired; but the typical way to handle a specific control request is to the the ``@control_request_handler`` decorators. Args: request : the USBControlRequest object representing the relevant request """ log.debug(f"{self.name} received request: {request}") # Call our base USBRequestHandler method. handled = super().handle_request(request) # As the top-most handle_request function, we have an extra responsibility: # we'll need to stall the endpoint if no handler was found. if not handled: log.warning(f"Stalling unhandled {request}.") self._add_request_suggestion(request) self.stall(direction=USBDirection.IN) return handled def handle_data_received(self, endpoint: USBEndpoint, data: bytes): """ Handler for receipt of non-control request data. Typically, this method will delegate any data received to the appropriate configuration/interface/endpoint. If overridden, the overriding function will receive all data. Args: endpoint_number : The endpoint number on which the data was received. data : The raw bytes received on the relevant endpoint. """ # If we have a configuration, delegate to it. if self.configuration: self.configuration.handle_data_received(endpoint, data) # If we're un-configured, we don't expect to receive # anything other than control data; defer to our "unexpected data". else: log.error(f"Received non-control data when unconfigured!" "This is invalid host behavior.") self.handle_unexpected_data_received(endpoint.number, data) def handle_unexpected_data_received(self, endpoint_number: int, data: bytes): """ Handler for unexpected data. Handles any data directed at an unexpected target; e.g. an endpoint that doesn't exist. Note that even if `handle_data_received` is overridden, this method can still be called e.g. by configuration.handle_data_received. Args: endpoint_number : The endpoint number on which the data was received. data : The raw bytes received on the relevant endpoint. """ log.error(f"Received {len(data)} bytes of data on invalid EP{endpoint_number}/OUT.") def handle_data_requested(self, endpoint: USBEndpoint): """ Handler called when the host requests data on a non-control endpoint. Typically, this method will delegate the request to the appropriate configuration+interface+endpoint. If overridden, the overriding function will receive all events. Args: endpoint_number : The endpoint number on which the host requested data. """ # If we have a configuration, delegate to it. if self.configuration: self.configuration.handle_data_requested(endpoint) # If we're un-configured, we don't expect to receive # anything other than control data; defer to our "unexpected data". else: log.error(f"Received non-control data when unconfigured!" "This is invalid host behavior.") self.handle_unexpected_data_requested(endpoint.number) def handle_unexpected_data_requested(self, endpoint_number: int): """ Handler for unexpected data requests. Handles any requests directed at an unexpected target; e.g. an endpoint that doesn't exist. Note that even if `handle_data_requested` is overridden, this method can still be called e.g. by configuration.handle_data_received. Args: endpoint_number : The endpoint number on which the data was received. """ log.error(f"Host requested data on invalid EP{endpoint_number}/IN.") def handle_buffer_empty(self, endpoint: USBEndpoint): """ Handler called when a given endpoint first has an empty buffer. Often, an empty buffer indicates an opportunity to queue data for sending ('prime an endpoint'), but doesn't necessarily mean that the host is planning on reading the data. This function is called only once per buffer. """ # If we have a configuration, delegate to it. if self.configuration: self.configuration.handle_buffer_empty(endpoint) # # Methods for USBRequestHandler. # def _request_handlers(self) -> Iterable[callable]: return self._request_handler_methods def _get_subordinate_handlers(self) -> Iterable[callable]: # As a device, our subordinates are our configurations. return self.configurations.values() # # Suggestion engine. # def _add_request_suggestion(self, request: USBControlRequest): """ Adds a 'suggestion' to the list of requests that may need implementing. Args: request : The unhandled request on which the suggestion should be based. """ # Build a tuple of the relevant immutable parts of the request, # and store it as a suggestion. if request.recipient in (USBRequestRecipient.INTERFACE, USBRequestRecipient.ENDPOINT): recipient_id = request.index & 0xFF else: recipient_id = None suggestion_summary = (request.direction, request.type, request.recipient, recipient_id, request.number) self._suggested_requests.add(suggestion_summary) self._suggested_request_metadata[suggestion_summary] = { 'length': request.length, 'data': request.data } def _print_suggested_requests(self): """ Prints a collection of suggested additions to the stdout. """ # Create a quick printing shortcut. print_html = lambda data : print_formatted_text(HTML(data)) # Look-ups for the function's decorators / etc. request_type_decorator = { USBRequestType.STANDARD: '@standard_request_handler', USBRequestType.VENDOR: '@vendor_request_handler', USBRequestType.CLASS: '@class_request_handler', USBRequestRecipient.OTHER: '@reserved_request_handler' } target_decorator = { USBRequestRecipient.DEVICE: '@to_device', USBRequestRecipient.INTERFACE: '@to_this_interface', USBRequestRecipient.ENDPOINT: '@to_this_endpoint', USBRequestRecipient.OTHER: '@to_other', } # Helper function used to group requests by recipients. def grouper(suggestion): direction, request_type, recipient, recipient_id, number = suggestion return (recipient, recipient_id) print_html("\nRequest handler code:") if not self._suggested_requests: print_html("\t No suggestions.") return # Sort the suggested requests by recipient, then group them. all_suggestions = sorted(self._suggested_requests, key=grouper) groups = itertools.groupby(all_suggestions, key=grouper) # Print each suggestion, grouped by recipient. for group, suggestions in groups: recipient, recipient_id = group if recipient == USBRequestRecipient.INTERFACE: print_html(f"\nOn interface {recipient_id}:") elif recipient == USBRequestRecipient.ENDPOINT: print_html(f"\nOn endpoint {recipient_id}:") else: print_html(f"\nOn the device:") for suggestion in suggestions: direction, request_type, recipient, recipient_id, number = suggestion metadata = self._suggested_request_metadata[suggestion] # Find the associated text descriptions for the relevant field. decorator = request_type_decorator[request_type] direction_name = USBDirection(direction).name # Generate basic metadata for our function. request_number = f"{number}" function_name = f"handle_control_request_{number}" # Figure out if we want to use a cleaner request number. if request_type == USBRequestType.STANDARD: try: request_number = f"USBStandardRequests.{USBStandardRequests(number).name}" function_name = f"handle_{USBStandardRequests(number).name.lower()}_request" except ValueError: pass # Figure out if we should include a target decorator. if recipient in target_decorator: recipient_decorator = target_decorator[recipient] specific_recipient = "" else: recipient_decorator = None specific_recipient = f"recipient={recipient}, " # # Print the code block. # print_html("") # Primary request decorator, e.g. "@standard_request_handler". print_html(f" {decorator}(" f"number={request_number}, " f"{specific_recipient}" f"direction=USBDirection.{direction_name}" f")") # Recipient specifier; e.g. "@to_device" if recipient_decorator: print_html(f" {recipient_decorator}") # Function definition. print_html(f" def " f"{function_name}" "(self, request):" ) # Note about the requested length, if applicable. if direction == USBDirection.IN: print_html(f" # Most recent request was for {metadata['length']}B of data.") else: print_html(f" # Most recent request data: {metadata['data']}.") # Default function body. print_html(f" # Replace me with your handler.") print_html(f" request.stall()") def print_suggested_additions(self): """ Prints a collection of suggested additions to the stdout. """ sys.stdout.flush() sys.stderr.flush() # Create a quick printing shortcut. print_html = lambda data : print_formatted_text(HTML(data)) # Header. print_html("") print_html("Automatic Suggestions") print_html("These suggestions are based on simple observed behavior;") print_html("not all of these suggestions may be useful / desirable.") print_html("") self._print_suggested_requests() print_html("") # # Backend helpers. # def set_address(self, address: int, defer: bool = False): """ Updates the device's knowledge of its own address. Args: address : The address to apply. defer : If true, the address change should be deferred until the next time a control request ends. Should be set if we're changing the address before we ack the relevant transaction. """ self.address = address self.backend.set_address(address, defer) def get_descriptor(self) -> bytes: """ Returns a complete descriptor for this device. """ d = bytearray([ 18, # length of descriptor in bytes 1, # descriptor type 1 == device self.usb_spec_version & 0xff, (self.usb_spec_version >> 8) & 0xff, self.device_class, self.device_subclass, self.protocol_revision_number, self.max_packet_size_ep0, self.vendor_id & 0xff, (self.vendor_id >> 8) & 0xff, self.product_id & 0xff, (self.product_id >> 8) & 0xff, self.device_revision & 0xff, (self.device_revision >> 8) & 0xff, self.strings.get_index(self.manufacturer_string), self.strings.get_index(self.product_string), self.strings.get_index(self.serial_number_string), len(self.configurations) ]) return d def get_configuration_descriptor(self, index: int) -> bytes: """ Returns the configuration descriptor with the given configuration number. """ # The index argument is zero-indexed; here, but configuration numbers # are one-indexed (as 0 is unconfigured). Adjust accordingly. return self.configurations[index + 1].get_descriptor() def handle_get_supported_languages_descriptor(self) -> bytes: """Return the special string-descriptor-zero that indicates which languages are supported.""" if self.supported_languages is None: return None # Our string descriptor is going to have two header bytes, plus two bytes # for each language. total_length = (len(self.supported_languages) * 2) + 2 packet = bytearray([total_length, DescriptorTypes.STRING]) for language in self.supported_languages: packet.extend(language.to_bytes(2, byteorder='little')) return bytes(packet) def get_string_descriptor(self, index:int) -> bytes: """ Returns the string descriptor associated with a given index. """ if index == 0: return self.handle_get_supported_languages_descriptor() else: return self.strings[index] @staticmethod def handle_generic_get_descriptor_request( self: Union['USBDevice', USBInterface], request: USBControlRequest): """ Handle GET_DESCRIPTOR requests; per USB2 [9.4.3] """ log.debug(f"received GET_DESCRIPTOR request {request}") # Extract the core parameters from the request. descriptor_type = request.value_high descriptor_index = request.value_low identifier = (descriptor_type, descriptor_index) # Try to find the specific descriptor for the request. response = self.requestable_descriptors.get(identifier, None) # If that fails, try to find a function covering this type. if response is None: response = self.requestable_descriptors.get(descriptor_type, None) # If we have a callable, we need to evaluate it to figure # out what the actual descriptor should be. while callable(response): response = response(descriptor_index) # If we wound up with a valid response, reply with it. if response: response_length = min(request.length, len(response)) request.reply(response[:response_length]) log.trace(f"sending {response_length} bytes in response") else: log.trace(f"stalling descriptor request") request.stall() class USBDevice(USBBaseDevice): """ Class representing the behavior of a USB device. This default implementation provides standard request handlers in order to facilitate creating a host-compatible USB device. These functions can be overloaded to change their behavior. If you want to dramatically change the behavior of these requests, you can opt to use USBBaseDevice, which lacks standard request handling. Fields: device_class/device_subclass/protocol_revision_number -- The USB descriptor fields that select the class, subclass, and protcol. vendor_id, product_id -- The USB vendor and product ID for this device. manufacturer_string, product_string, serial_number_string -- Python strings identifying the device to the USB host. supported_languages -- A tuple containing all of the language IDs supported by the device. device_revision -- Number indicating the hardware revision of this device. Typically BCD. usb_spec_revision -- Number indicating the version of the USB specification we adhere to. Typically 0x0200. """ @standard_request_handler(number=USBStandardRequests.GET_STATUS) @to_device def handle_get_status_request(self, request): """ Handles GET_STATUS requests; per USB2 [9.4.5].""" log.debug("received GET_STATUS request") # self-powered and remote-wakeup (USB 2.0 Spec section 9.4.5) request.reply(b'\x03\x00') @standard_request_handler(number=USBStandardRequests.CLEAR_FEATURE) @to_device def handle_clear_feature_request(self, request): """ Handle CLEAR_FEATURE requests; per USB2 [9.4.1] """ log.debug(f"Received CLEAR_FEATURE request with type {request.number} and value {request.value}.") request.acknowledge() @standard_request_handler(number=USBStandardRequests.SET_FEATURE) @to_device def handle_set_feature_request(self, request): """ Handle SET_FEATURE requests; per USB2 [9.4.9] """ log.debug("received SET_FEATURE request") request.stall() @standard_request_handler(number=USBStandardRequests.SET_ADDRESS) @to_device def handle_set_address_request(self, request): """ Handle SET_ADDRESS requests; per USB2 [9.4.6] """ request.acknowledge(blocking=True) self.set_address(request.value) @standard_request_handler(number=USBStandardRequests.GET_DESCRIPTOR) @to_device def handle_get_descriptor_request(self, request): """ Handle GET_DESCRIPTOR requests; per USB2 [9.4.3] """ # Defer to our generic get_descriptor handler. self.handle_generic_get_descriptor_request(self, request) @standard_request_handler(number=USBStandardRequests.SET_DESCRIPTOR) @to_device def handle_set_descriptor_request(self, request): """ Handle SET_DESCRIPTOr requests; per USB2 [9.4.8] """ log.debug("received SET_DESCRIPTOR request") request.stall() @standard_request_handler(number=USBStandardRequests.GET_CONFIGURATION) @to_device def handle_get_configuration_request(self, request): """ Handle GET_CONFIGURATION requests; per USB2 [9.4.2] """ log.debug(f"received GET_CONFIGURATION request for configuration {request.value}") # If we haven't yet been configured, send back a zero configuration value. if self.configuration is None: request.reply(b"\x00") # Otherwise, return the index for our configuration. else: config_index = self.configuration.number request.reply(config_index.to_bytes(1, byteorder='little')) @standard_request_handler(number=USBStandardRequests.SET_CONFIGURATION) @to_device def handle_set_configuration_request(self, request): """ Handle SET_CONFIGURATION requests; per USB2 [9.4.7] """ log.debug("received SET_CONFIGURATION request") # If the host is requesting configuration zero, they're asking # us to drop our configuration. if request.value == 0: self.configuration = None request.acknowledge() # Otherwise, we'll find a given configuration and apply it. else: try: self.configuration = self.configurations[request.value] # On a configuration change, all interfaces revert # to alternate setting 0. self.configuration.active_interfaces = { interface.number: interface for interface in self.configuration.get_interfaces() if interface.alternate == 0 } request.acknowledge() except KeyError: request.stall() # Notify the backend of the reconfiguration, in case # it needs to e.g. set up endpoints accordingly self.backend.configured(self.configuration) # USB 2.0 specification, section 9.4.11 (p 288 of pdf) @standard_request_handler(number=USBStandardRequests.SYNCH_FRAME) @to_device def handle_synch_frame_request(self, request): """ Handle SYNC_FRAME requests; per USB2 [9.4.10] """ log.debug(f"f{self.name} received SYNCH_FRAME request") request.acknowledge() def generate_code(self, name="Device"): languages = [f"LanguageIDs.{l.name}" for l in self.supported_languages] if len(languages) == 1: languages = f"({languages[0]},)" else: languages = f"({str.join(', '), languages})" if self.device_speed is None: speed = "None" else: speed = f"DeviceSpeed.{self.device_speed.name}" code = f""" @use_inner_classes_automatically class {name}(USBDevice): device_speed = {speed} device_class = {self.device_class} device_subclass = {self.device_subclass} protocol_revision_number = {self.protocol_revision_number} max_packet_size_ep0 = {self.max_packet_size_ep0} vendor_id = 0x{self.vendor_id:04x} product_id = 0x{self.product_id:04x} manufacturer_string = {self.manufacturer_string.generate_code()} product_string = {self.product_string.generate_code()} serial_number_string = {self.serial_number_string.generate_code()} supported_languages = {languages} device_revision = 0x{self.device_revision:04x} usb_spec_version = 0x{self.usb_spec_version:04x} """ for configuration_id in sorted(self.configurations): code += self.configurations[configuration_id].generate_code(indent=4) return code ================================================ FILE: facedancer/devices/__init__.py ================================================ # # This file is part of Facedancer. # import sys import pprint import asyncio import inspect import argparse from ..errors import EndEmulation from ..logging import configure_default_logging, log def default_main(device_or_type, *coroutines): """ Simple, default main for Facedancer emulation. Parameters: device_type -- The USBDevice type to emulate. """ # Instantiate the relevant device, and connect it to our host. parser = argparse.ArgumentParser(description=f"Emulation frontend for {device_or_type.name}(s).") parser.add_argument('--print-only', action='store_true', help="Prints information about the device without emulating.") parser.add_argument('--suggest', action='store_true', help="Prints suggested code additions after device emualtion is complete.") parser.add_argument('-v', '--verbose', help="Controls verbosity. 0=silent, 3=default, 5=spammy", default=3) args = parser.parse_args() # Set up our logging output. python_loglevel = 50 - (int(args.verbose) * 10) configure_default_logging(level=python_loglevel) if inspect.isclass(device_or_type): device = device_or_type() else: device = device_or_type if args.print_only: pprint.pprint(device) sys.exit(0) # Run the relevant code, along with any added coroutines. log.info("Starting emulation, press 'Control-C' to disconnect and exit.") try: device.emulate(*coroutines) except KeyboardInterrupt: pass finally: if args.suggest: device.print_suggested_additions() ================================================ FILE: facedancer/devices/ftdi.py ================================================ # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # """ Emulation of an FTDI USB-to-serial converter. """ import asyncio import struct from enum import IntFlag from typing import Union from . import default_main from .. import * from ..classes import USBDeviceClass from ..logging import log OUT_ENDPOINT = 2 IN_ENDPOINT = 1 class FTDIFlowControl(IntFlag): """ Constants describing how FTDI flow control works. """ NO_FLOW_CONTROL = 0 RTS_CTS = 1 DTR_DSR = 2 XON_XOFF = 4 @use_inner_classes_automatically class FTDIDevice(USBDevice): """ Class implementing an emulated FTDI device. """ vendor_id : int = 0x0403 product_id : int = 0x6001 device_revision : int = 0x0600 serial_number : str = "FT123450" manufacturer_string : StringRef = StringRef.field(string="not-FTDI") product_string : StringRef = StringRef.field(string="FTDI emulation") serial_number_string : StringRef = StringRef.field(string=serial_number) eeprom_data = [ 0x0440, 0x0304, 0x0160, 0x0006, 0x802D, 0x0800, 0x0002, 0x1812, 0x2A20, 0x4812, 0x0000, 0x0000, 0x1203, 0x6E00, 0x6F00, 0x7400, 0x2D00, 0x4600, 0x5400, 0x4400, 0x4900, 0x1E03, 0x4600, 0x5400, 0x4400, 0x4900, 0x2000, 0x6500, 0x6D00, 0x7500, 0x6C00, 0x6100, 0x7400, 0x6900, 0x6F00, 0x6E00, 0x1203, 0x4600, 0x5400, 0x3100, 0x3200, 0x3300, 0x3400, 0x3500, 0x3000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x6B67 ] class _Configuration(USBConfiguration): configuration_string : str = "FTDI config" class _Interface(USBInterface): # This is a completely vendor-specific device. class_number : int = USBDeviceClass.VENDOR_SPECIFIC subclass_number : int = USBDeviceClass.VENDOR_SPECIFIC protocol_number : int = USBDeviceClass.VENDOR_SPECIFIC class _OutEndpoint(USBEndpoint): number : int = OUT_ENDPOINT direction : USBDirection = USBDirection.OUT transfer_type : USBTransferType = USBTransferType.BULK class _InEndpoint(USBEndpoint): number : int = IN_ENDPOINT direction : USBDirection = USBDirection.IN transfer_type : USBTransferType = USBTransferType.BULK def __post_init__(self): super().__post_init__() # FTDI Windows driver reads serial number descriptor after reading the EEPROM for verification self.strings.add_string(self.serial_number, index=3) self.reset_ftdi() def reset_ftdi(self): """ Resets the FTDI driver back to its original state. """ # Create a fake baud rate. self.baud_rate = 9600 # Start off with DTR/RTS disabled. self.use_dtr = False self.use_rts = False # Create synthetic values for our control signals. self.clear_to_send = True self.data_set_ready = True self.ring_detect = False self.line_status_detect = True self.data_terminal_ready = False self.ready_to_send = False # Start off with no flow control. self.flow_control = FTDIFlowControl.NO_FLOW_CONTROL # # Request handlers. # @vendor_request_handler(number=0) def handle_reset_request(self, request): log.debug("Received FTDI reset; assuming initial settings.") self.reset_ftdi() request.acknowledge() @vendor_request_handler(number=1) def handle_modem_ctrl_request(self, req): log.debug("received modem_ctrl request") dtr = bool(req.value & 0x0001) rts = bool(req.value & 0x0002) self.use_dtr = bool(req.value & 0x0100) self.use_rts = bool(req.value & 0x0200) if dtr: log.info("DTR set -- host appears to have connected via virtual serial.") else: log.info("DTR cleared -- host appears to have disconnected from virtual serial.") if self.use_dtr: self.data_terminal_ready = dtr if self.use_rts: self.ready_to_send = rts req.acknowledge() @vendor_request_handler(number=2) def handle_set_flow_ctrl_request(self, request): """ Control request to set up flow control. """ try: self.flow_control = FTDIFlowControl(request.value) if self.flow_control: log.info(f"Host has set up {self.flow_control.name} flow control.") else: log.info(f"Host has disabled flow control.") request.acknowledge() except KeyError: request.stall() @vendor_request_handler(number=3) def handle_set_baud_rate_request(self, request): """ Control request to set our baud rate. """ if request.value > 9: log.warning("Host specified an unknown baud rate value.") request.acknowledge() return # For most values, the FTDI device uses the value to set the baud divisor, # such that 0 = 300, 1 = 600, etc. if request.value < 8: self.baud_rate = 300 * (2 ** request.value) # For values of 8/9, it jumps up to hit two more standard bauds. elif request.value == 8: self.baud_rate = 57600 elif request.value == 9: self.baud_rate = 115200 log.info(f"Host set baud rate to {self.baud_rate}.") request.acknowledge() @vendor_request_handler(number=4) def handle_set_data_request(self, request): log.debug("received set_data request") request.acknowledge() @vendor_request_handler(number=5) def handle_get_modem_status_request(self, request): """ Handles requests for the FTDI device's modem status. """ # Currently, we're emulating the original FTDI SIO, so we only provide # a single byte of status. Otherwise, we'd have an second byte with line status. response = \ (1 << 4) if self.clear_to_send else 0 | \ (1 << 5) if self.data_set_ready else 0 | \ (1 << 6) if self.ring_detect else 0 | \ (1 << 7) if self.line_status_detect else 0 request.reply((response,)) @vendor_request_handler(number=6) def handle_set_event_char_request(self, request): log.debug("received set_event_char request") request.acknowledge() @vendor_request_handler(number=7) def handle_set_error_char_request(self, request): log.debug("received set_error_char request") request.acknowledge() @vendor_request_handler(number=9) def handle_set_latency_timer_request(self, request): log.debug("received set_latency_timer request") request.acknowledge() @vendor_request_handler(number=10) def handle_get_latency_timer_request(self, request): log.debug("received get_latency_timer request") # Per Travis Goodspeed, this is a "bullshit value". request.reply(b'\x01') @vendor_request_handler(number=144) def handle_read_eeprom_request(self, request): log.info(f"received read_eeprom request at index {request.index}") if 0 <= request.index < len(self.eeprom_data): data_word = self.eeprom_data[request.index] response_bytes = struct.pack('>H', data_word) request.reply(response_bytes) log.debug(f"Handled EEPROM read at index {request.index}: sent {response_bytes.hex()}") elif request.index == 66: request.reply(struct.pack('>H', 0x0000)) log.debug(f"Handled EEPROM read at index {request.index}: sent 0000") elif request.index < 128: request.reply(struct.pack('>H', 0xFFFF)) log.debug(f"Handled EEPROM read at index {request.index}: sent FFFF") else: request.stall() log.info(f"EEPROM read at out-of-bounds index {request.index}: stalled") # # Internal event handlers. # def handle_data_received(self, endpoint, data): """ Called back whenever data is received. """ log.debug(f"received {len(data)} bytes on {endpoint}") self.handle_serial_data_received(data) # # User I/O interface. # async def wait_for_host(self): """ Waits until the host connects by waiting for DTR assertion. """ # Wait for the host to assert DTR. while not self.data_terminal_ready: await asyncio.sleep(0.1) def handle_serial_data_received(self, data): """ Callback executed when serial data is received. Subclasses should override this to capture data from the host. """ log.debug(f"Received serial data: {data}") def transmit(self, data: Union[str, bytes], *, blocking: bool = False, adjust_endings: bool = True): """ Transmits a block of data over the provided FTDI link to the host. Parameters: data -- The data to be sent. blocking -- If true, this method will wait for completion before returning. adjust_endings -- If true, line endings will be adjusted before sending. """ FTDI_PAYLOAD_LENGTH = 62 # If this isn't a set of raw bytes, encode it into bytes. if hasattr(data, 'encode'): if adjust_endings: data = data.replace("\n", "\r\n") data = data.encode('utf-8') # Packetize and send the relevant data. data = bytearray(data) while data: packet = data[0:FTDI_PAYLOAD_LENGTH] del data[0:FTDI_PAYLOAD_LENGTH] self._transmit_packet(packet, blocking=blocking) def _transmit_packet(self, data: bytes, *, blocking: bool = False): """ Sends a single packet of up to 63 data bytes over our link. """ # Generate an FTDI packet. packet = bytearray() # Our first/header byte contains the payload length in bits [7:2], and a packet ID of 01 in [1:0]. packet.append((len(data) << 2) | 0b01) packet.append(0) # The remainder of the packet is our payload. packet.extend(data) self.send(IN_ENDPOINT, packet, blocking=blocking) if __name__ == "__main__": default_main(FTDIDevice) ================================================ FILE: facedancer/devices/keyboard.py ================================================ # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # import asyncio from typing import Iterable from . import default_main from .. import * from ..classes.hid.usage import * from ..classes.hid.descriptor import * from ..classes.hid.keyboard import * # Specifies how many simultaneously keys we want to support. KEY_ROLLOVER = 8 @use_inner_classes_automatically class USBKeyboardDevice(USBDevice): """ Simple USB keyboard device. """ name : str = "USB keyboard device" product_string : str = "Non-suspicious Keyboard" class KeyboardConfiguration(USBConfiguration): """ Primary USB configuration: act as a keyboard. """ class KeyboardInterface(USBInterface): """ Core HID interface for our keyboard. """ name : str = "USB keyboard interface" class_number : int = 3 class KeyEventEndpoint(USBEndpoint): number : int = 3 direction : USBDirection = USBDirection.IN transfer_type : USBTransferType = USBTransferType.INTERRUPT interval : int = 10 # # Raw descriptors -- TODO: build these from their component parts. # class HIDDescriptor(USBDescriptor): number : int = 0 type_number : int = USBDescriptorTypeNumber.HID raw : bytes = b'\x09\x21\x10\x01\x00\x01\x22\x2b\x00' include_in_config : bool = True class ReportDescriptor(HIDReportDescriptor): number : int = 0 fields : tuple = ( # Identify ourselves as a keyboard. USAGE_PAGE (HIDUsagePage.GENERIC_DESKTOP), USAGE (HIDGenericDesktopUsage.KEYBOARD), COLLECTION (HIDCollection.APPLICATION), USAGE_PAGE (HIDUsagePage.KEYBOARD), # Modifier keys. # These span the full range of modifier key codes (left control to right meta), # and each has two possible values (0 = unpressed, 1 = pressed). USAGE_MINIMUM (KeyboardKeys.LEFTCTRL), USAGE_MAXIMUM (KeyboardKeys.RIGHTMETA), LOGICAL_MINIMUM (0), LOGICAL_MAXIMUM (1), REPORT_SIZE (1), REPORT_COUNT (KeyboardKeys.RIGHTMETA - KeyboardKeys.LEFTCTRL + 1), INPUT (variable=True), # One byte of constant zero-padding. # This is required for compliance; and Windows will ignore this report # if the zero byte isn't present. REPORT_SIZE (8), REPORT_COUNT (1), INPUT (constant=True), # Capture our actual, pressed keyboard keys. # Support a standard, 101-key keyboard; which has # keycodes from 0 (NONE) to 101 (COMPOSE). # # We provide the capability to press up to eight keys # simultaneously. Setting the REPORT_COUNT effectively # sets the key rollover; so 8 reports means we can have # up to eight keys pressed at once. USAGE_MINIMUM (KeyboardKeys.NONE), USAGE_MAXIMUM (KeyboardKeys.COMPOSE), LOGICAL_MINIMUM (KeyboardKeys.NONE), LOGICAL_MAXIMUM (KeyboardKeys.COMPOSE), REPORT_SIZE (8), REPORT_COUNT (KEY_ROLLOVER), INPUT (), # End the report. END_COLLECTION (), ) @class_request_handler(number=USBStandardRequests.GET_INTERFACE) @to_this_interface def handle_get_interface_request(self, request): # Silently stall GET_INTERFACE class requests. request.stall() def __post_init__(self): super().__post_init__() # Keep track of any pressed keys, and any pressed modifiers. self.active_keys = set() self.modifiers = 0 def _generate_hid_report(self) -> bytes: """ Generates a single HID report for the given keyboard state. """ # If we have active keypresses, compose a set of scancodes from them. scancodes = \ list(self.active_keys)[:KEY_ROLLOVER] + \ [0] * (KEY_ROLLOVER - len(self.active_keys)) return bytes([self.modifiers, 0, *scancodes]) def handle_data_requested(self, endpoint: USBEndpoint): """ Provide data once per host request. """ report = self._generate_hid_report() endpoint.send(report) # # User-facing API. # def key_down(self, code: KeyboardKeys): """ Marks a given key as pressed; should be a scancode from KeyboardKeys. """ self.active_keys.add(code) def key_up(self, code: KeyboardKeys): """ Marks a given key as released; should be a scancode from KeyboardKeys. """ self.active_keys.remove(code) def modifier_down(self, code: KeyboardModifiers): """ Marks a given modifier as pressed; should be a flag from KeyboardModifiers. """ if code is not None: self.modifiers |= code def modifier_up(self, code: KeyboardModifiers): """ Marks a given modifier as released; should be a flag from KeyboardModifiers. """ if code is not None: self.modifiers &= ~code async def type_scancode(self, code: KeyboardKeys, duration: float = 0.1, modifiers: KeyboardModifiers = None): """ Presses, and then releases, a single key. Parameters: code -- The keyboard key to be pressed's scancode. duration -- How long the given key should be pressed, in seconds. modifiers -- Any modifier keys that should be held while typing. """ self.modifier_down(modifiers) self.key_down(code) await asyncio.sleep(duration) self.key_up(code) self.modifier_up(modifiers) await asyncio.sleep(duration) async def type_scancodes(self, *codes: Iterable[KeyboardKeys], duration: float = 0.1): """ Presses, and then releases, a collection of keys, in order. Parameters: *code -- The keyboard keys to be pressed's scancodes. duration -- How long each key should be pressed, in seconds. """ for code in codes: await self.type_scancode(code, duration=duration) async def type_letter(self, letter: str, duration: float = 0.1, modifiers: KeyboardModifiers = None): """ Attempts to type a single letter, based on its ASCII string representation. Parameters: letter -- A single-character string literal, to be typed. duration -- How long each key should be pressed, in seconds. modifiers -- Any modifier keys that should be held while typing. """ shift, code = KeyboardKeys.get_scancode_for_ascii(letter) modifiers = shift if modifiers is None else modifiers | shift await self.type_scancode(code, modifiers=modifiers, duration=duration) async def type_letters(self, *letters: Iterable[str], duration:float = 0.1): """ Attempts to type a string of letters, based on ASCII string representations. Parameters: *letters -- A collection of single-character string literal, to be typed in order. duration -- How long each key should be pressed, in seconds. """ for letter in letters: await self.type_letter(letter, duration=duration) async def type_string(self, to_type: str, *, duration:float = 0.1, modifiers: KeyboardModifiers = None): """ Attempts to type a python string into the remote host. Parameters: letter -- A collection of single-character string literal, to be typed in order. duration -- How long each key should be pressed, in seconds. modifiers -- Any modifier keys that should be held while typing. """ self.modifier_down(modifiers) for letter in to_type: await self.type_letter(letter, duration=duration) self.modifier_up(modifiers) def all_keys_up(self, *, include_modifiers: bool = True): """ Releases all keys currently pressed. Parameters: include_modifiers -- If set to false, modifiers will be left at their current states. """ self.active_keys.clear() if include_modifiers: self.all_modifiers_up() def all_modifiers_up(self): """ Releases all modifiers currently held. """ self.modifiers = 0 if __name__ == "__main__": default_main(USBKeyboardDevice) ================================================ FILE: facedancer/devices/umass/__init__.py ================================================ from .umass import * from .disk_image import * ================================================ FILE: facedancer/devices/umass/disk_image.py ================================================ from mmap import mmap import os class DiskImage: """ Class representing an arbitrary disk image, which can be procedurally generated, or which can be rendered from e.g. a file. Currently limited to representing disk with 512-byte sectors. """ def close(self): """ Closes and cleans up any resources held by the disk image. """ pass def get_sector_size(self): return 512 def get_sector_count(self): """ Returns the disk's sector count. """ raise NotImplementedError() def get_data(self, address, length): data_to_read = length sector_size = self.get_sector_size() data = bytes() while data_to_read > 0: data.extend(self.get_sector_data(address)) data_to_read -= sector_size address += 1 return data def get_sector_data(self, address): """ Returns the raw binary data for a given sector. """ raise NotImplementedError() def put_data(self, address, data): sector_size = self.get_sector_size() while data: sector = data[:sector_size] data = data[sector_size:] self.put_sector_data(address, sector) address += 1 return data def put_sector_data(self, address, data): """ Sets the raw binary data for a given disk sector. """ sys.stderr.write("WARNING: UMS write ignored; this type of image does not support writing.\n") class FAT32DiskImage(DiskImage): """ Class for manufacturing synthetic FAT32 disk images. """ CLUSTER_SIZE = 512 MBR_SECTOR = 0 BPB_SECTOR = 2048 # set by our MBR partition entry FSINFO_SECTOR = 2049 # set by our BPB entry FAT_START = 2080 # specified by our BPB entry (partition start + reserved sectors) FAT_END = 6113 # specified by our BPB entry (fat start + fat size) DATA_SECTION_START = 10146 # specified by our BPB entry (fat start + num_fats * fat_size) ROOT_DIR_ENTRY = 10146 # specified by our BPB entry (we put the directory at the very start) def __init__(self, size = 1024 * 1024 * 256, verbose=0): self.verbose = verbose self.size = size # Initialize the commands we'll use to handle sector writes. self._initialize_sector_handlers() def _register_sector_handler(self, sector_or_lambda, name, handler=None): if handler is None: handler = self.handle_unhandled_sector descriptor = { "sector_or_lambda": sector_or_lambda, "name": name, "handler": handler } self.sector_handlers.append(descriptor) def _initialize_sector_handlers(self): self.sector_handlers = [] # Handlers for disk special sectors... self._register_sector_handler(self.MBR_SECTOR, "MBR/partition table", self.handle_mbr_read) self._register_sector_handler(self.BPB_SECTOR, "BIOS Parameter Block", self.handle_bpb_read) self._register_sector_handler(self.FSINFO_SECTOR, "FSINFO Block", self.handle_fsinfo_read) self._register_sector_handler(lambda x : x >= self.FAT_START and x < self.FAT_END, "File Allocation Table", self.handle_fat_read) self._register_sector_handler(self.ROOT_DIR_ENTRY, "Root Directory", self.handle_root_dir_read) def handle_mbr_read(self, address): """ Returns a master boot record directing the target device to our emulated FAT32 partition. """ response = 440 * b'\0' # bootstrap code + timestamp response += b'\xDE\xAD\xBE\xEF' # disk signature (we're making one up) response += b'\x00\x00' # 0 = not copy protected response += self._generate_fat_partition_entry() # partition entry for our FAT32 partition response += (16 * 3) * b'\0' # three empty partition slots response += b'\x55\xAA' # end of sector signature return response def handle_bpb_read(self, address): """ Returns a valid Boot Parameter Block, which tells the device how to interpret our FAT filesystem. """ response = b'\xEB\x00\x90' # jump to bootloader (oddly, checked on some non-x86 uCs) response += b'MSWIN4.1' # OEM name (this one seems broadly compatible) # Bytes per disk sector. response += self.get_sector_size().to_bytes(2, byteorder='little') # Sectors per cluster. response += self._sectors_per_cluster().to_bytes(1, byteorder='little') response += b'\x20\x00' # reserved sectors response += b'\x02' # number of FATs (must be 2) response += b'\x00\x00' # root entries (must be 0 for fat32) response += b'\x00\x00' # total 16-bit count of sectors (must be 0 for fat32) response += b'\xF8' # media type: hard drive (0xF8) response += b'\x00\x00' # sectors per FAT (must be 0 for fat32) response += b'\x00\x00' # sectors per track (most likely ignored) response += b'\x00\x00' # number of heads (most likely ignored) response += b'\x00\x00\x00\x00' # hidden sectors (most likely ignored) # The total number of sectors in the volume. response += self.get_partition_sectors().to_bytes(4, byteorder='little') response += b'\xC1\x0F\x00\x00' # sectors per FAT response += b'\x00\x00' # flags response += b'\x00\x00' # filesystem revision response += b'\x02\x00\x00\x00' # cluster for the root directory response += b'\x01\x00' # address of the fsinfo sector response += b'\x06\x00' # address of the backup of the boot sector response += 12 * b'\x00' # reserved space response += b'\x80' # drive number for PC-BIOS (0x80 = hard disk) response += b'\x00' # reserved space response += b'\x29' # boot signature (from mkfs.vfat) response += b'0000' # disk serial number (for volume tracking) response += b'Facedancer ' # volume label (must be 11 bytes; spaces for padding) response += b'FAT32 ' # should be "FAT32" for FAT32, padded to eight bytes response += 420 * b'\x00' # reserved space response += b'\x55\xAA' # end of sector marker return response def handle_fsinfo_read(self, address): """ Returns a valid filesystem info block, which is used to cache information about free sectors on the filesystem. We don't actually sport writing, so we return a valid-but-useless block. """ response = b'\x52\x52\x61\x41' # fsinfo block signature (magic number) response += 480 * b'\x00' # reserved for future use response += b'\x72\x72\x41\x61' # second signature (magic number) response += b'\xFF\xFF\xFF\xFF' # free sector count (-1 = "don't know") response += b'\xFF\xFF\xFF\xFF' # next free sector (-1 = "don't know") response += 12 * b'\x00' # reserved for future use response += b'\x00\x00\x55\xAA' # final signature (magic number) return response def _generate_directory_entry(self, filename, file_size, cluster_number, flags=b'\x00'): # TODO: automatically convert filename, filesize to bytes # TODO: support long form name entries? cluster_number_bytes = cluster_number.to_bytes(4, byteorder='little') cluster_number_low = cluster_number_bytes[:2] cluster_number_high = cluster_number_bytes[2:] entry = filename # short name (first 8 are name; last three are extension) entry += flags # file attributes entry += b'\x00' # reserved byte entry += 5 * b'\x00' # dir creation date/time entry += 2 * b'\x00' # last access date entry += cluster_number_high # high word of the entry's first cluster number entry += 4 * b'\x00' # last write date/time entry += cluster_number_low # low word of the entry's first cluster number entry += file_size.to_bytes(4, byteorder='little') return entry def _short_filename_checksum(self, short_filename): """ Generates a long-form name checksum for a given 8.3 short filename. """ sum = 0 for byte in short_filename: # I'm sorry. This is copied directly from the bloody spec. # Don't judge me. Judge them. sum = (((sum & 1) << 7) | ((sum & 0xfe) >> 1)) + byte return sum & 0xFF def _is_valid_83_char(self, c): """ Returns true iff the given character is a valid 8-3 filename character. """ if c in " !#$%&'()-@^_`{}~'": return True if c.isupper() or c.isdigit(): return True return False def _is_valid_83_name(self, long_filename): """ Returns true iff the given filename is a valid filename. """ if len(long_filename) != 11: return False return all([self._is_valid_83_char(c) for c in long_filename]) def _short_filename_from_long(self, long_filename): """ Generates short-form filenames from a long-form name. """ # TODO: Generalize this to behave like Windows if self._is_valid_83_name(long_filename): return long_filename.encode('utf-8') else: # FIXME: This breaks in lots of cases; it's just Good Enough (TM) # for now. prefix = re.sub(r'\W+', '', long_filename)[:6] extension = long_filename[-3:] short_name = '{}~1{}'.format(prefix, extension) return short_name.encode('utf-8') def _generate_long_directory_entries(self, long_filename, short_filename): """ Generate long-form directory entries for a long filename. Should be called immediately before calling the short_form directory entry functions. """ index = 1 entries = [] # Null terminate our long filename, as the filesystem expects. long_filename += "\0" while long_filename: entry_file = long_filename[:13] long_filename = long_filename[13:] # If this is the final entry, set the sixth bit of the index, # indicating that this is the final index present. if not long_filename: index |= 0x40 # Compute the checksum for the short filename. checksum = self._short_filename_checksum(short_filename) # Encode the filename in UTF-8, padded with FFs as necessary. entry_file = bytes(entry_file.encode('utf-16'))[2:] entry_file = entry_file.ljust(26, b'\xFF') # Generate the entry itself. entry = index.to_bytes(1, byteorder='little') # index of this entry entry += entry_file[:10] # first five characters entry += b'\x0F' # attribute indicating this is a long filename entry += b'\x00' # always zeroes for VFAT LFNs entry += checksum.to_bytes(1, byteorder='little') # checksum of the short name entry += entry_file[10:22] # next six characters of the filename entry += b'\x00\x00' # always zeroes entry += entry_file[22:] # the final two characters of the filename # Move to the next entry... index += 1 entries.append(entry) # Reverse the order of the entries, and convert them to a byte string. return b''.join(entries[::-1]) def handle_root_dir_read(self, address): """ Returns a valid entry describing the root directory of our FAT filesystem. """ # Generate the volume label entry. response = self._generate_directory_entry(b'Facedancer ', 0, 0, flags=b'\x08') return response def _generate_fat_partition_entry(self): """ Returns a partition entry pointing to our synthetic FAT partition. """ response = b'\x00' # Status: 0x00 = not bootable, 0x80 = bootable response += b'\x00\x00\x00' # CHS address of the partition's first sector; typically ignored response += b'\x0B' # disk type: FAT32 with CHS/LBA addressing response += b'\x00\x00\x00' # CHS address of the partition's end; typically ignored # LBA of our first sector. response += self.BPB_SECTOR.to_bytes(4, byteorder='little') # Report the size of the partition, in sectors. We'll use up all "unallocated" # space on the drive with our FAT partition. response += self.get_partition_sectors().to_bytes(4, byteorder='little') return response def _sectors_per_cluster(self): """ Returns the number of sectors in a cluster. """ return int(self.CLUSTER_SIZE / self.get_sector_size()) def handle_fat_read(self, address): """ Handles an access to the device's file allocation table. """ # TODO: Create general method for reading from the FAT based on # virtual files, and methods to add those files! raise NotImplementedError() def handle_unhandled_sector(self, address): """ Handles unsupported sector reads. """ if self.verbose > 3: print("<-- !!! unhandled sector {}, returning all zeroes".format(address)) return bytes(bytearray(self.get_sector_size())) def get_sector_count(self): """ Returns the total number of sectors present on the disk. """ return int(self.size / self.get_sector_size()) - 1 def get_partition_sectors(self): """ Get the amount of sectors available for use by our main FAT partition. """ # Return everything but the MBR and reserved space. return (self.get_sector_count() - 4096) def _find_sector_handler(self, address): """ Locates the function that should handle generation of the given sector. """ # Check each of our sector handlers to see if it is appropriate to handle # the given sector... for handler in self.sector_handlers: sector_or_lambda = handler['sector_or_lambda'] if(callable(sector_or_lambda)): matches = sector_or_lambda(address) else: matches = (sector_or_lambda == address) if matches: return handler return None def get_sector_data(self, address): """ Fetches the data at the given sector of our emulated disk. """ handler = self._find_sector_handler(address) # If we have a handler for this sector, handle it. if handler: name = handler['name'] function = handler['handler'] if self.verbose > 0: print("<-- handling read of {} sector ({})".format(name, address)) # Call the main handler. response = function(address) # If our response is smaller than our sector size, pad it out with zeroes. if len(response) < self.get_sector_size(): needed_bytes = self.get_sector_size() - len(response) response += needed_bytes * b'\x00' if self.verbose > 4: print(" response: {} ({})".format(len(response), response)) return response # Otherwise, run the unknown command handler. else: return self.handle_unhandled_sector(address) class RawDiskImage(DiskImage): """ Raw disk image backed by a file. """ def __init__(self, filename, block_size, verbose=0): self.filename = filename self.block_size = block_size self.verbose = verbose statinfo = os.stat(self.filename) self.size = statinfo.st_size self.file = open(self.filename, 'r+b') self.image = mmap(self.file.fileno(), 0) def close(self): self.image.flush() self.image.close() def get_sector_count(self): return int(self.size / self.block_size) - 1 def get_sector_data(self, address): if self.verbose == 2: print("<-- reading sector {}".format(address)) block_start = address * self.block_size block_end = (address + 1) * self.block_size # slices are NON-inclusive data = self.image[block_start:block_end] if self.verbose > 3: if not any(data): print("<-- reading sector {} [all zeroes]".format(address)) else: print("<-- reading sector {} [{}]".format(address, data)) return data def put_data(self, address, data): if self.verbose > 1: blocks = int(len(data) / self.block_size) print("--> writing {} blocks at lba {}".format(blocks, address)) super().put_data(address, data) def put_sector_data(self, address, data): if self.verbose == 2: print("--> writing sector {}".format(address)) if len(data) > self.block_size: print("WARNING: got {} bytes of sector data; expected a max of {}".format(len(data), self.block_size)) block_start = address * self.block_size block_end = (address + 1) * self.block_size # slices are NON-inclusive if self.verbose > 3: if not any(data): print("--> writing sector {} [all zeroes]".format(address)) else: print("--> writing sector {} [{}]".format(address, data)) self.image[block_start:block_end] = data[:self.block_size] self.image.flush() ================================================ FILE: facedancer/devices/umass/umass.py ================================================ # USBMassStorage.py # # Contains class definitions to implement a USB mass storage device. # """ Emulation of a USB Mass storage device. """ import asyncio import os import re import struct import sys import time from enum import IntFlag from typing import Union from .. import default_main from ... import * from ...classes import USBDeviceClass from ...logging import log ENDPOINT_OUT = 1 ENDPOINT_IN = 3 @use_inner_classes_automatically class USBMassStorageDevice(USBDevice): """ Class implementing an emulated USB Mass Storage device. """ class _Configuration(USBConfiguration): configuration_string : str = "Mass Storage config" class _Interface(USBInterface): # This is a Mass Storage Class device class_number : int = USBDeviceClass.MASS_STORAGE subclass_number : int = 0x06 # SCSI transparent command set protocol_number : int = 0x50 # bulk-only (BBB) transport class _OutEndpoint(USBEndpoint): number : int = ENDPOINT_OUT direction : USBDirection = USBDirection.OUT transfer_type : USBTransferType = USBTransferType.BULK max_packet_size : int = 64 class _InEndpoint(USBEndpoint): number : int = ENDPOINT_IN direction : USBDirection = USBDirection.IN transfer_type : USBTransferType = USBTransferType.BULK max_packet_size : int = 64 def __init__(self, disk_image, name="USB mass storage interface", vendor_id=0x8107, # Sandisk product_id=0x5051, # SDCZ2 Cruzer Mini Flash Drive (thin) device_revision=0x0003, manufacturer_string="Facedancer", product_string="USB Mass Storage emulation", max_packet_size_ep0=64, serial_number_string=None, vendor="LifeScan"): self.disk_image = disk_image self.vendor = vendor # Pass our custom values explicitly to prevent them from being reset super().__init__( name=name, vendor_id=vendor_id, product_id=product_id, device_revision=device_revision, manufacturer_string=manufacturer_string, product_string=product_string, max_packet_size_ep0=max_packet_size_ep0, serial_number_string=serial_number_string ) # # Device overrides # def connect(self): super().connect() # instantiate our SCSI command handler self.scsi_command_handler = ScsiCommandHandler(self, self.disk_image, verbose=3, vendor=self.vendor) def disconnect(self): super().disconnect() # close our disk image self.disk_image.close() def handle_data_received(self, endpoint, data): if endpoint.number == ENDPOINT_OUT: # dispatch received data to our SCSI command handler self.scsi_command_handler.handle_data_received(data) else: log.warning(f"Received data on unexpected endpoint: {endpoint}") # # Class Request handlers. # @class_request_handler(number=254, direction=USBDirection.IN) @to_this_interface def handle_get_max_lun_request(self, request): request.reply(b'\x00') @class_request_handler(number=255, direction=USBDirection.IN) @to_this_interface def handle_bulk_only_mass_storage_reset_request(self, request): request.reply(b'') # TODO is this an internal event handler maybe? async def wait_for_host(self): """ Waits until the host connects by TODO. """ while not True: await asyncio.sleep(0.1) def bytes_as_hex(b, delim=" "): return delim.join(["%02x" % x for x in b]) class ScsiCommandHandler: name : str = "SCSI Command Handler" STATUS_OKAY = 0x00 STATUS_FAILURE = 0x02 # TODO: Should this be 0x01? STATUS_INCOMPLETE = -1 # Special case status that aborts before response. def __init__(self, device, disk_image, verbose=0, vendor="GoodFET "): self.device = device self.disk_image = disk_image self.verbose = verbose self.vendor = vendor self.is_write_in_progress = False self.write_cbw = None self.write_base_lba = 0 self.write_length = 0 self.write_data = b'' self._register_scsi_commands() def handle_data_received(self, data): if self.is_write_in_progress: cbw = self.write_cbw status, response = self.continue_write(cbw, data) else: cbw = CommandBlockWrapper(data) status, response = self.handle_scsi_command(cbw) # If we weren't able to complete the operation, return without # transmitting a response. if status == self.STATUS_INCOMPLETE: return # If we have a response payload to transmit, transmit it. if response: if self.verbose > 2: print("--> responding with", len(response), "bytes [{}], status={}".format(bytes_as_hex(response), status)) self.device.send(ENDPOINT_IN, response, blocking=True) # Otherwise, respond with our status. csw = bytes([ ord('U'), ord('S'), ord('B'), ord('S'), cbw.tag[0], cbw.tag[1], cbw.tag[2], cbw.tag[3], 0x00, 0x00, 0x00, 0x00, status ]) self.device.send(ENDPOINT_IN, csw, blocking=True) def handle_scsi_command(self, cbw): """ Handles an SCSI command. """ opcode = cbw.cb[0] direction = cbw.flags >> 7 # If we have a handler for this routine, handle it. if opcode in self.commands: # Extract the command's data. command = self.commands[opcode] name = command['name'] handler = command['handler'] direction_name = 'IN' if direction else 'OUT' direction_arrow = "<--" if direction else "-->" expected_length = cbw.data_transfer_length if self.verbose > 0: print("{} handling {} ({}) {}:[{}]".format(direction_arrow, name.upper(), direction_name, expected_length, bytes_as_hex(cbw.cb[1:]))) # Delegate to its handler function. return handler(cbw) # Otherwise, run the unknown command handler. else: return self.handle_unknown_command(cbw) def handle_unknown_command(self, cbw): """ Handles unsupported SCSI commands. """ print(self.name, "received unsupported SCSI opcode 0x%x" % cbw.cb[0]) # Generate an empty response to the relevant command. if cbw.data_transfer_length > 0: response = bytes([0] * cbw.data_transfer_length) else: response = None # Return failure. return self.STATUS_FAILURE, response def handle_ignored_event(self, cbw): """ Handles SCSI events that we can safely ignore. """ # Always return success, and no response. return self.STATUS_OKAY, None def handle_sense(self, cbw): """ Handles SCSI sense requests. """ response = b'\x70\x00\xFF\x00\x00\x00\x00\x0A\x00\x00\x00\x00\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00' return self.STATUS_OKAY, response def handle_inquiry(self, cbw): opcode, flags, page_code, allocation_length, control = struct.unpack(">BBBHB", cbw.cb[0:6]) # Print out the details of our inquiry. if self.verbose > 1: print("-- INQUIRY ({}) flags: {} page_code: {} allocation_length: {} control: {}". \ format(opcode, flags, page_code, allocation_length, control)) response = bytes([ 0x00, # 0x00 = device present, and provides direct access to blocks 0x00, # 0x00 = media not removable, 0x80 = media removable 0x05, # 0 = no standards compliance, 3 = SPC compliant, 4 = SPC-2 compliant, 5 = SCSI compliant :) 0x02, # 0x02 = data responses follow the spec 0x14, # Additional length. 0x00, 0x00, 0x00 ]) response += self.vendor.encode('utf-8') # vendor response += b'GoodFET ' # product id response += b' ' # product revision response += b'0.01' # pad up to data_transfer_length bytes diff = cbw.data_transfer_length - len(response) response += bytes([0] * diff) return self.STATUS_OKAY, response def handle_mode_sense_6(self, cbw): page = cbw.cb[2] & 0x3f response = b'\x03\x00\x00\x1c' if page != 0x3f: print(self.name, "unknown page, returning empty page") response = b'\x03\x00\x00\x00' return self.STATUS_OKAY, response def handle_mode_sense_10(self, cbw): page = cbw.cb[2] & 0x3f response = b'\x07\x00\x00\x00\x00\x00\x00\x1c' if page != 0x3f: print(self.name, "unknown page, returning empty page") response = b'\x07\x00\x00\x00\x00\x00\x00\x00' return self.STATUS_OKAY, response def handle_service_action_in(self, cbw): opcode = cbw.cb[0] if opcode == 0x9e: return self.handle_get_read_capacity_16(cbw) else: # Always return success, and no response. return self.STATUS_OKAY, None def handle_get_format_capacity(self, cbw): response = bytes([ 0x00, 0x00, 0x00, 0x08, # capacity list length 0x00, 0x00, 0x10, 0x00, # number of sectors (0x1000 = 10MB) 0x10, 0x00, # reserved/descriptor code 0x02, 0x00, # 512-byte sectors ]) return self.STATUS_OKAY, response def handle_get_read_capacity(self, cbw): lastlba = self.disk_image.get_sector_count() if lastlba > 0xffffffff: lastlba = 0xffffffff response = bytes([ (lastlba >> 24) & 0xff, (lastlba >> 16) & 0xff, (lastlba >> 8) & 0xff, (lastlba ) & 0xff, 0x00, 0x00, 0x02, 0x00, # 512-byte blocks ]) return self.STATUS_OKAY, response def handle_get_read_capacity_16(self, cbw): lastlba = self.disk_image.get_sector_count() response = bytes([ (lastlba >> 56) & 0xff, (lastlba >> 48) & 0xff, (lastlba >> 40) & 0xff, (lastlba >> 32) & 0xff, (lastlba >> 24) & 0xff, (lastlba >> 16) & 0xff, (lastlba >> 8) & 0xff, (lastlba ) & 0xff, 0x00, 0x00, 0x02, 0x00, # 512-byte blocks ]) return self.STATUS_OKAY, response def handle_read(self, cbw): base_lba = cbw.cb[2] << 24 \ | cbw.cb[3] << 16 \ | cbw.cb[4] << 8 \ | cbw.cb[5] num_blocks = cbw.cb[7] << 8 \ | cbw.cb[8] if self.verbose > 0: print("<-- performing READ (10), lba", base_lba, "+", num_blocks, "block(s)") # Note that here we send the data directly rather than putting # something in 'response' and letting the end of the switch send for block_num in range(num_blocks): data = self.disk_image.get_sector_data(base_lba + block_num) self.device.send(ENDPOINT_IN, data, blocking=True) if self.verbose > 3: print("--> responded with {} bytes".format(cbw.data_transfer_length)) return self.STATUS_OKAY, None def handle_read_16(self, cbw): base_lba = cbw.cb[2] << 56 \ | cbw.cb[3] << 48 \ | cbw.cb[4] << 40 \ | cbw.cb[5] << 32 \ | cbw.cb[6] << 24 \ | cbw.cb[7] << 16 \ | cbw.cb[8] << 8 \ | cbw.cb[9] num_blocks = cbw.cb[10] << 24 \ | cbw.cb[11] << 16 \ | cbw.cb[12] << 8 \ | cbw.cb[13] if self.verbose > 0: print("<-- performing READ (16), lba", base_lba, "+", num_blocks, "block(s)") # Note that here we send the data directly rather than putting # something in 'response' and letting the end of the switch send for block_num in range(num_blocks): data = self.disk_image.get_sector_data(base_lba + block_num) self.ep_to_host.send_packet(data, blocking=True) if self.verbose > 3: print("--> responded with {} bytes".format(cbw.data_transfer_length)) return self.STATUS_OKAY, None def handle_write(self, cbw): base_lba = cbw.cb[2] << 24 \ | cbw.cb[3] << 16 \ | cbw.cb[4] << 8 \ | cbw.cb[5] num_blocks = cbw.cb[7] << 8 \ | cbw.cb[8] if self.verbose > 0: print("--> performing WRITE (10), lba", base_lba, "+", num_blocks, "block(s)") # save for later self.write_cbw = cbw self.write_base_lba = base_lba self.write_length = num_blocks * self.disk_image.get_sector_size() self.is_write_in_progress = True # because we need to snarf up the data from wire before we reply # with the CSW return self.STATUS_INCOMPLETE, None def handle_write_16(self, cbw): base_lba = cbw.cb[2] << 56 \ | cbw.cb[3] << 48 \ | cbw.cb[4] << 40 \ | cbw.cb[5] << 32 \ | cbw.cb[6] << 24 \ | cbw.cb[7] << 16 \ | cbw.cb[8] << 8 \ | cbw.cb[9] num_blocks = cbw.cb[10] << 24 \ | cbw.cb[11] << 16 \ | cbw.cb[12] << 8 \ | cbw.cb[13] if self.verbose > 0: print("--> performing WRITE (16), lba", base_lba, "+", num_blocks, "block(s)") # save for later self.write_cbw = cbw self.write_base_lba = base_lba self.write_length = num_blocks * self.disk_image.get_sector_size() self.is_write_in_progress = True # because we need to snarf up the data from wire before we reply # with the CSW return self.STATUS_INCOMPLETE, None def continue_write(self, cbw, data): if self.verbose > 3: print("--> continue write with {} more bytes of data".format(len(data))) self.write_data += data if len(self.write_data) < self.write_length: # more yet to read, don't send the CSW return self.STATUS_INCOMPLETE, None self.disk_image.put_data(self.write_base_lba, self.write_data) self.is_write_in_progress = False self.write_data = b'' return self.STATUS_OKAY, None def _register_scsi_commands(self): self.commands = {} self._register_scsi_command(0x00, "Test Unit Ready", self.handle_ignored_event) self._register_scsi_command(0x03, "Request Sense", self.handle_sense) self._register_scsi_command(0x12, "Inquiry", self.handle_inquiry) self._register_scsi_command(0x1a, "Mode Sense (6)", self.handle_mode_sense_6) self._register_scsi_command(0x5a, "Mode Sense (10)", self.handle_mode_sense_10) self._register_scsi_command(0x1e, "Prevent/Allow Removal", self.handle_ignored_event) self._register_scsi_command(0x23, "Get Format Capacity", self.handle_get_format_capacity) self._register_scsi_command(0x25, "Get Read Capacity", self.handle_get_read_capacity) self._register_scsi_command(0x28, "Read", self.handle_read) self._register_scsi_command(0x88, "Read (16)", self.handle_read_16) self._register_scsi_command(0x2a, "Write (10)", self.handle_write) self._register_scsi_command(0x8a, "Write (16)", self.handle_write_16) self._register_scsi_command(0x36, "Synchronize Cache", self.handle_ignored_event) self._register_scsi_command(0x9e, "Service Action In", self.handle_service_action_in) def _register_scsi_command(self, number, name, handler=None): if handler is None: handler = self.handle_unknown_command descriptor = { "number": number, "name": name, "handler": handler, } self.commands[number] = descriptor class CommandBlockWrapper: def __init__(self, bytestring): self.signature = bytestring[0:4] self.tag = bytestring[4:8] self.data_transfer_length = bytestring[8] \ | bytestring[9] << 8 \ | bytestring[10] << 16 \ | bytestring[11] << 24 self.flags = int(bytestring[12]) self.lun = int(bytestring[13] & 0x0f) self.cb_length = int(bytestring[14] & 0x1f) self.cb = bytestring[15:] def __str__(self): s = "sig: " + bytes_as_hex(self.signature) + "\n" s += "tag: " + bytes_as_hex(self.tag) + "\n" s += "data transfer len: " + str(self.data_transfer_length) + "\n" s += "flags: " + str(self.flags) + "\n" s += "lun: " + str(self.lun) + "\n" s += "command block len: " + str(self.cb_length) + "\n" s += "command block: " + bytes_as_hex(self.cb) + "\n" return s if __name__ == "__main__": default_main(USBMassStorageDevice) ================================================ FILE: facedancer/endpoint.py ================================================ # # This file is part of Facedancer # """ Functionality for describing USB endpoints. """ # Support annotations on Python < 3.9 from __future__ import annotations import struct import textwrap from typing import Iterable, List, Dict from dataclasses import field from collections import defaultdict from .magic import AutoInstantiable, instantiate_subordinates from .descriptor import USBDescribable, USBDescriptor from .request import USBRequestHandler, get_request_handler_methods from .request import to_this_endpoint, standard_request_handler from .types import USBDirection, USBTransferType, USBSynchronizationType from .types import USBUsageType, USBStandardRequests from .logging import log class USBEndpoint(USBDescribable, AutoInstantiable, USBRequestHandler): """ Class representing a USBEndpoint object. Field: number: The endpoint number (without the direction bit) for this endpoint. direction: A USBDirection constant indicating this endpoint's direction. transfer_type: A USBTransferType constant indicating the type of communications used. max_packet_size: The maximum packet size for this endpoint. interval: The polling interval, for an INTERRUPT endpoint. """ DESCRIPTOR_TYPE_NUMBER = 0x05 # Core identifiers. number : int direction : USBDirection # Endpoint attributes. transfer_type : USBTransferType = USBTransferType.BULK synchronization_type : USBSynchronizationType = USBSynchronizationType.NONE usage_type : USBUsageType = USBUsageType.DATA max_packet_size : int = 64 interval : int = 0 # Extra bytes that extend the basic endpoint descriptor. extra_bytes : bytes = b'' # Descriptors that will be included in a GET_CONFIGURATION response. attached_descriptors : List[USBDescriptor] = field(default_factory=list) # Descriptors that can be requested with the GET_DESCRIPTOR request. requestable_descriptors : Dict[tuple[int, int], USBDescriptor] = field(default_factory=dict) parent : USBDescribable = None @classmethod def from_binary_descriptor(cls, data, strings={}): """ 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_from("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=number, direction=USBDirection(direction), transfer_type=USBTransferType(transfer_type), synchronization_type=USBSynchronizationType(sync_type), usage_type=USBUsageType(usage_type), max_packet_size=max_packet_size, interval=interval, extra_bytes=data[7:] ) def __post_init__(self): # Capture any descriptors declared directly on the class. for descriptor in instantiate_subordinates(self, USBDescriptor): self.add_descriptor(descriptor) # Grab our request handlers. self._request_handler_methods = get_request_handler_methods(self) # # User interface. # @staticmethod def address_for_number(endpoint_number: int, direction: USBDirection) -> int: """ Computes the endpoint address for a given number + direction. """ direction_mask = 0x80 if direction == USBDirection.IN else 0x00 return endpoint_number | direction_mask def get_device(self): """ Returns the device associated with the given descriptor. """ return self.parent.get_device() def send(self, data: bytes, *, blocking: bool = False): """ Sends data on this endpoint. Valid only for IN endpoints. Args: data : The data to be sent. blocking : True if we should block until the backend reports the transmission to be complete. """ self.get_device()._send_in_packets(self.number, data, packet_size=self.max_packet_size, blocking=blocking) # # Event handlers. # def handle_data_received(self, data: bytes): """ Handler for receipt of non-control request data. Args: data : The raw bytes received. """ log.info(f"EP{self.number} received {len(data)} bytes of data; " "but has no handler.") def handle_data_requested(self): """ Handler called when the host requests data on this endpoint.""" def handle_buffer_empty(self): """ Handler called when this endpoint first has an empty buffer. """ @standard_request_handler(number=USBStandardRequests.CLEAR_FEATURE) @to_this_endpoint def handle_clear_feature_request(self, request): log.debug(f"received CLEAR_FEATURE request for endpoint {self.number} " f"with value {request.value}") request.acknowledge() # # Properties. # @property def address(self): """ Fetches the address for the given endpoint. """ return self.address_for_number(self.number, self.direction) def get_address(self): """ Method alias for the address property. For backend support. """ return self.address @property def attributes(self): """ Fetches the attributes for the given endpoint, as a single byte. """ return (self.transfer_type & 0x03) | \ ((self.synchronization_type & 0x03) << 2) | \ ((self.usage_type & 0x03) << 4) def add_descriptor(self, descriptor: USBDescriptor): """ Adds the provided descriptor to the endpoint. """ identifier = descriptor.get_identifier() desc_name = type(descriptor).__name__ if descriptor.include_in_config: self.attached_descriptors.append(descriptor) descriptor.parent = self elif descriptor.number is None: raise Exception( f"Descriptor of type {desc_name} cannot be added to this " f"endpoint because it is not to be included in the " f"configuration descriptor, yet does not have a number " f"to request it separately with") elif identifier in self.requestable_descriptors: other = self.requestable_descriptors[identifier] other_name = type(other).__name__ other_type = f"0x{other.type_number:02X}" raise Exception( f"Descriptor of type {desc_name} cannot be added to this " f"endpoint because there is already a descriptor of type " f"{other_name} with the same type code {other_type} and " f"number {other.number}") else: self.requestable_descriptors[identifier] = descriptor descriptor.parent = self def get_descriptor(self) -> bytes: """ Get a descriptor string for this endpoint. """ # FIXME: use construct d = bytearray([ # length of descriptor in bytes 7 + len(self.extra_bytes), # descriptor type 5 == endpoint 5, self.address, self.attributes, self.max_packet_size & 0xff, (self.max_packet_size >> 8) & 0xff, self.interval ]) return d + self.extra_bytes # # Automatic instantiation helpers. # def get_identifier(self) -> int: return self.address def matches_identifier(self, other:int) -> bool: # Use only the MSB and the lower nibble; per the USB specification. masked_other = other & 0b10001111 return self.get_identifier() == masked_other # # Request handling. # def _request_handlers(self) -> Iterable[callable]: return self._request_handler_methods # # Pretty-printing. # def __str__(self): direction = USBDirection(self.direction).name transfer_type = USBTransferType(self.transfer_type).name is_interrupt = (self.transfer_type == USBTransferType.INTERRUPT) additional = f" every {self.interval}ms" if is_interrupt else "" return f"endpoint {self.number:02x}/{direction}: {transfer_type} transfers{additional}" def generate_code(self, name=None, indent=0): if name is None: name = f"Endpoint_{self.number}_{self.direction.name}" direction = f"USBDirection.{self.direction.name}" transfer_type = f"USBTransferType.{self.transfer_type.name}" sync_type = f"USBSynchronizationType.{self.synchronization_type.name}" usage_type = f"USBUsageType.{self.usage_type.name}" values = str.join(", ", map(lambda x: f"0x{x:02x}", self.extra_bytes)) code = f""" class {name}(USBEndpoint): number = {self.number} direction = {direction} transfer_type = {transfer_type} synchronization_type = {sync_type} usage_type = {usage_type} max_packet_size = {self.max_packet_size} interval = {self.interval} extra_bytes = bytes([{values}]) """ # Use alphabetic suffixes to distinguish between multiple attached # descriptors with the same type number. suffixes = defaultdict(lambda: 'A') for descriptor in self.attached_descriptors: type_number = descriptor.type_number suffix = suffixes[type_number] suffixes[type_number] = chr(ord(suffix) + 1) name = f"Descriptor_0x{type_number:02X}_{suffix}" code += descriptor.generate_code(name=name, indent=4) for descriptor_id in sorted(self.requestable_descriptors): descriptor = self.requestable_descriptors[descriptor_id] code += descriptor.generate_code(indent=4) return textwrap.indent(code, indent * ' ') ================================================ FILE: facedancer/errors.py ================================================ # # This file is part of Facedancer. # class DeviceNotFoundError(IOError): """ Error indicating a device was not found. """ pass class EndEmulation(Exception): """ When an EndEmulation exception is thrown the emulation will shutdown and exit. """ ================================================ FILE: facedancer/filters/__init__.py ================================================ from .base import USBProxyFilter from .logging import USBProxyPrettyPrintFilter from .standard import USBProxySetupFilters ================================================ FILE: facedancer/filters/base.py ================================================ # # This file is part of Facedancer. # class USBProxyFilter: """ Base class for filters that modify USB data. """ def filter_control_in_setup(self, request, stalled): """ Filters a SETUP stage for an IN control request. This allows us to modify the SETUP stage before it's proxied to the real device. Args: request : The request to be issued. stalled : True iff the packet has been stalled by a previous filter. Returns: Modified versions of the arguments. If stalled is set to true, the packet will be immediately stalled an not proxied. If stalled is false, but request is returned as None, the packet will be NAK'd instead of proxied. """ return request, stalled def filter_control_in(self, request, data, stalled): """ Filters the data response from the proxied device during an IN control request. This allows us to modify the data returned from the proxied device during a setup stage. Args: request : The request that was issued to the target host. data : The data being proxied during the data stage. stalled : True if the proxied device (or a previous filter) stalled the request. Returns: Modified versions of the arguments. Note that modifying request will _only_ modify the request as seen by future filters, as the SETUP stage has already passed and the request has already been sent to the device. """ return request, data, stalled def filter_control_out(self, request, data): """ Filters handling of an OUT control request, which contains both a request and (optional) data stage. Args: request : The request issued by the target host. data : The data sent by the target host with the request. Returns: Modified versions of the arguments. Returning a request of None will absorb the packet silently and not proxy it to the device. """ return request, data def handle_out_request_stall(self, request, data, stalled): """ Handles an OUT request that was stalled by the proxied device. Args: request : The request header for the request that stalled. data : The data stage for the request that stalled, if appropriate. stalled : True iff the request is still considered stalled. This can be overridden by previous filters, so it's possible for this to be false. """ return request, data, stalled def filter_in_token(self, ep_num): """ Filters an IN token before it's passed to the proxied device. This allows modification of e.g. the endpoint or absorption of the IN token before it's issued to the real device. Args: ep_num : The endpoint number on which the IN token is to be proxied. Returns: A modified version of the arguments. If ep_num is set to None, the token will be absorbed and not issued to the target host. """ return ep_num def filter_in(self, ep_num, data): """ Filters the response to an IN token (the data packet received in response to the host issuing an IN token). Args: ep_num : The endpoint number associated with the data packet. data : The data packet received from the proxied device. Returns: A modified version of the arguments. If data is set to none, the packet will be absorbed, and a NAK will be issued instead of responding to the IN request with data. """ return ep_num, data def filter_out(self, ep_num, data): """ Filters a packet sent from the host via an OUT token. Args: ep_num: The endpoint number associated with the data packet. data: The data packet received from host. Returns: A modified version of the arguments. If data is set to none, the packet will be absorbed, """ return ep_num, data def handle_out_stall(self, ep_num, data, stalled): """ Handles an OUT transfer that was stalled by the victim. Args: ep_num : The endpoint number for the data that stalled. data : The data for the transfer that stalled, if appropriate. stalled : True iff the transfer is still considered stalled. This can be overridden by previous filters, so it's possible for this to be false. """ return ep_num, data, stalled ================================================ FILE: facedancer/filters/hid.py ================================================ # # USBProxy HID logging # from warnings import filterwarnings import hid_parser from hid_parser import HIDComplianceWarning from enum import IntEnum from facedancer.descriptor import USBDescriptorTypeNumber from facedancer.device import USBBaseDevice from facedancer.filters.base import USBProxyFilter from facedancer.request import USBControlRequest from facedancer.types import ( USBDirection, USBRequestRecipient, USBRequestType, USBStandardRequests, ) from ..logging import log GET_REPORT = 0x01 SET_REPORT = 0x09 SET_IDLE = 0x0A filterwarnings("ignore", r"Usage.* has no compatible usage types", HIDComplianceWarning) filterwarnings("ignore", r"Expecting 60 usages but got 1", HIDComplianceWarning) class HIDReportType(IntEnum): HID_TYPE_INPUT = 1 HID_TYPE_OUTPUT = 2 HID_TYPE_FEATURE = 3 class USBProxyHIDFilter(USBProxyFilter): """ Print HID packets If verbose > 2 - print all fields """ def __init__(self, device: USBBaseDevice, verbose=1): self.device = device self.verbose = verbose self.rdescs = {} def filter_control_in(self, req: USBControlRequest | None, data, stalled): if req: if req.type == USBRequestType.STANDARD and USBRequestRecipient.INTERFACE: if req.number == USBStandardRequests.GET_DESCRIPTOR: self._log_desc(req, data) if req.type == USBRequestType.CLASS and USBRequestRecipient.INTERFACE: self._log_in(req, data) return req, data, stalled def _log_desc(self, req, data): kind = req.value_high iface = req.index # index = req.value_low if kind == USBDescriptorTypeNumber.HID: log.info(f"GET_DESC HID_DEVICE I{iface} {dump(data)}") if kind == USBDescriptorTypeNumber.REPORT: log.info(f"GET_DESC HID_REPORT I{iface} {dump(data)}") try: self.rdescs[iface] = rdesc = hid_parser.ReportDescriptor(data) except NotImplementedError as e: log.warning(f"Failed to parse report: {e}") self.rdescs[iface] = None return if self.verbose > 2: for rid in rdesc.output_report_ids: log.info(f" output {rid} {rdesc.get_output_report_size(rid)}") for rid in rdesc.input_report_ids: log.info(f" input {rid} {rdesc.get_input_report_size(rid)}") for rid in rdesc.feature_report_ids: log.info(f" feature {rid} {rdesc.get_feature_report_size(rid)}") def _log_in(self, req, data): iface = req.index if req.number == GET_REPORT: kind = HIDReportType(req.value_high) log.info(f"GET_REPORT {kind} RID {req.value_low} I{iface} {dump(data)}") self._report(iface, "parse_input_report", data) def filter_control_out(self, req, data): if req and req.type == USBRequestType.CLASS and USBRequestRecipient.INTERFACE: self._log_out(req, data) return req, data def _log_out(self, req, data): iface = req.index if req.number == SET_REPORT: kind = HIDReportType(req.value_high) log.info(f"SET_REPORT {kind} RID {req.value_low} I{iface} {dump(data)}") self._report(iface, "parse_output_report", data) if req.number == SET_IDLE: dur = req.value_high * 4 log.info(f"SET_IDLE {dur}ms RID {req.value_low} I{iface} {dump(data)}") def filter_in(self, ep_num, data): if interface := self._find_interface(ep_num): self._log_ep_in(interface.number, ep_num, data) return ep_num, data def _find_interface(self, ep_num): """Return the interface that has ep_num.""" if not self.device.configuration: return for interface in self.device.configuration.active_interfaces.values(): if interface.has_endpoint(ep_num, USBDirection.IN): return interface def _log_ep_in(self, iface, num, data): log.info(f"EP{num} I{iface} RID {data[0]} {dump(data[1:])}") self._report(iface, "parse_input_report", data) def _report(self, iface: int, kind: str, data: bytes): if self.verbose < 3: return rdesc = self.rdescs.get(iface) if len(data) > 1 and rdesc: try: # TODO - handle feature for usage, value in getattr(rdesc, kind)(data).items(): log.info(f" {usage} {value}") except Exception as e: log.warning(f" {e}") def dump(raw: bytes): return raw.hex(" ", -2) ================================================ FILE: facedancer/filters/logging.py ================================================ # # USBProxy logging filters # import datetime from ..logging import log from . import USBProxyFilter class USBProxyPrettyPrintFilter(USBProxyFilter): """ Filter that pretty prints USB transactions according to log levels. """ def __init__(self, verbose=4, decoration=''): """ Sets up a new USBProxy pretty printing filter. """ self.verbose = verbose self.decoration = decoration def filter_control_in(self, req, data, stalled): """ Log IN control requests without modification. """ if self.verbose > 3 and req is None: log.info("{} {}< --filtered out-- ".format(self.timestamp(), self.decoration)) return req, data, stalled if self.verbose > 3: log.info("{} {}{}".format(self.timestamp(), self.decoration, repr(req))) if self.verbose > 3 and stalled: log.info("{} {}< --STALLED-- ".format(self.timestamp(), self.decoration)) if self.verbose > 4 and data: is_string = (req.request == 6) and (req.value >> 8 == 3) self._pretty_print_data(data, '<', self.decoration, is_string) return 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: if self.verbose > 3 and req is None: log.info("{} {}> --filtered out-- ".format(self.timestamp(), self.decoration)) return req, data if self.verbose > 3: log.info("{} {}{}".format(self.timestamp(), self.decoration, repr(req))) if self.verbose > 4 and data: self._pretty_print_data(data, '>', self.decoration) return 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: log.info("{} {}> --STALLED-- ".format(self.timestamp(), self.decoration)) else: log.info("{} {}> --STALLED, but unstalled by filter-- ".format(self.timestamp(), self.decoration)) return req, data, stalled def filter_in(self, ep_num, data): """ Log IN transfers without modification. """ 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 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 representation 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, ep_marker=''): data = self._magic_decode(data) if is_string else bytes(data) log.info("{} {}{}{}: {}".format(self.timestamp(), ep_marker, decoration, direction_marker, data)) ================================================ FILE: facedancer/filters/standard.py ================================================ # # This file is part of Facedancer. # """ Standard filters for USBProxy that should (almost) always be used. """ from .. import * from ..descriptor import USBDescribable from ..errors import * from ..logging import log from . import USBProxyFilter class USBProxySetupFilters(USBProxyFilter): SET_ADDRESS_REQUEST = 5 SET_CONFIGURATION_REQUEST = 9 SET_INTERFACE_REQUEST = 11 GET_DESCRIPTOR_REQUEST = 6 RECIPIENT_DEVICE = 0 RECIPIENT_INTERFACE = 1 DESCRIPTOR_DEVICE = 0x01 DESCRIPTOR_CONFIGURATION = 0x02 MAX_PACKET_SIZE_EP0 = 64 def __init__(self, device, verbose=0): self.device = device self.configurations = {} self.verbose = verbose def filter_control_in(self, req, data, stalled): if stalled: return req, data, stalled # If this is a read of a valid configuration descriptor (and subordinate # descriptors, parse them and store the results for later). 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_CONFIGURATION and req.length >= 32: configuration = USBDescribable.from_binary_descriptor(data) self.configurations[configuration.number] = configuration if self.verbose > 0: log.info("-- Storing configuration {} --".format(configuration)) if descriptor_type == self.DESCRIPTOR_DEVICE and req.length >= 7: # Patch our data to overwrite the maximum packet size on EP0. # See USBProxy.connect for a rationale on this. device = USBDescribable.from_binary_descriptor(data) device.max_packet_size_ep0 = 64 data = bytearray(device.get_descriptor())[:len(data)] if self.verbose > 0: log.info("-- Patched device descriptor. --") 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. if req.get_recipient() == self.RECIPIENT_DEVICE and \ req.request == self.SET_ADDRESS_REQUEST: req.acknowledge(blocking=True) self.device.set_address(req.value) 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: 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: log.info("-- Applying configuration {} --".format(configuration)) self.device.configured(configuration) # Otherwise, the host has applied a configuration without ever reading # its descriptor. This is mighty strange behavior! else: log.warning("-- WARNING: Applying configuration {}, but we've never read that configuration's descriptor! --".format(configuration_index)) # Special case: if this is a SET_INTERFACE_REQUEST, # pass it through, but also tell the device so it can update # its current configuration. if req.get_recipient() == self.RECIPIENT_INTERFACE and \ req.request == self.SET_INTERFACE_REQUEST: interface_number = req.index alternate = req.value self.device.interface_changed(interface_number, alternate) return req, data ================================================ FILE: facedancer/interface.py ================================================ # # This file is part of facedancer. # """ Functionality for defining USB interfaces. """ # Support annotations on Python < 3.9 from __future__ import annotations import struct import textwrap from typing import Dict, List, Iterable from dataclasses import field from collections import defaultdict from .magic import instantiate_subordinates, AutoInstantiable from .types import USBDirection, USBStandardRequests from . import device from .descriptor import USBDescribable, USBDescriptor, USBClassDescriptor, USBDescriptorTypeNumber, StringRef from .request import USBControlRequest, USBRequestHandler, get_request_handler_methods from .request import standard_request_handler, to_this_interface from .endpoint import USBEndpoint from .logging import log class USBInterface(USBDescribable, AutoInstantiable, USBRequestHandler): """ Class representing a USBDevice interface. Fields: number : The interface's index. Zero indexed. class_number, subclass_number, protocol_number : The USB class adhered to on this interface; usually a USBDeviceClass constant. interface_string : A short, descriptive string used to identify the endpoint; or None if not provided. """ DESCRIPTOR_TYPE_NUMBER = 0x4 name : StringRef = StringRef.field(string="generic USB interface") number : int = 0 alternate : int = 0 class_number : int = 0 subclass_number : int = 0 protocol_number : int = 0 interface_string : str = None # Descriptors that will be included in a GET_CONFIGURATION response. attached_descriptors : List[USBDescriptor] = field(default_factory=list) # Descriptors that can be requested with the GET_DESCRIPTOR request. requestable_descriptors : Dict[tuple[int, int], USBDescriptor] = field(default_factory=dict) endpoints : Dict[int, USBEndpoint] = field(default_factory=dict) parent : USBDescribable = None @classmethod def from_binary_descriptor(cls, data, strings={}): """ Generates an interface object from a descriptor. """ interface_number, alternate_setting, num_endpoints, interface_class, \ interface_subclass, interface_protocol, string_index \ = struct.unpack_from("xxBBBBBBB", data) return cls( name=None, number=interface_number, alternate=alternate_setting, class_number=interface_class, subclass_number=interface_subclass, protocol_number=interface_protocol, interface_string=StringRef.lookup(strings, string_index) ) def __post_init__(self): self.interface_string = StringRef.ensure(self.interface_string) # Capture any descriptors/endpoints declared directly on the class. for endpoint in instantiate_subordinates(self, USBEndpoint): self.add_endpoint(endpoint) for descriptor in instantiate_subordinates(self, USBDescriptor): self.add_descriptor(descriptor) # Populate our request handlers. self._request_handler_methods = get_request_handler_methods(self) # # User interface. # def get_device(self): """ Returns the device associated with the given descriptor. """ return self.parent.get_device() def add_endpoint(self, endpoint: USBEndpoint): """ Adds the provided endpoint to the interface. """ if endpoint.address in self.endpoints: ep_name = type(endpoint).__name__ ep_addr = f"0x{endpoint.address:02X}" other = self.endpoints[endpoint.address] other_name = type(other).__name__ raise Exception( f"Endpoint of type {ep_name} cannot be added to this " f"interface because there is already an endpoint of " f"type {other_name} with the same address {ep_addr}") else: self.endpoints[endpoint.address] = endpoint endpoint.parent = self def get_endpoint(self, endpoint_number: int, direction: USBDirection) -> USBEndpoint: """ Attempts to find a subordinate endpoint matching the given number/direction. Args: endpoint_number : The endpoint number to search for. direction : The endpoint direction to be matched. Returns: The matching endpoint; or None if no matching endpoint existed. """ address = USBEndpoint.address_for_number(endpoint_number, direction) return self.endpoints.get(address, None) def has_endpoint(self, endpoint_number: int, direction: USBDirection) -> USBEndpoint: """ Returns true iff we have matching subordinate endpoint. Args: endpoint_number : The endpoint number to search for. direction : The endpoint direction to be matched. """ return (self.get_endpoint(endpoint_number, direction) is not None) def add_descriptor(self, descriptor: USBDescriptor): """ Adds the provided descriptor to the interface. """ identifier = descriptor.get_identifier() desc_name = type(descriptor).__name__ if descriptor.include_in_config: self.attached_descriptors.append(descriptor) descriptor.parent = self elif descriptor.number is None: raise Exception( f"Descriptor of type {desc_name} cannot be added to this " f"interface because it is not to be included in the " f"configuration descriptor, yet does not have a number " f"to request it separately with") elif identifier in self.requestable_descriptors: other = self.requestable_descriptors[identifier] other_name = type(other).__name__ other_type = f"0x{other.type_number:02X}" raise Exception( f"Descriptor of type {desc_name} cannot be added to this " f"interface because there is already a descriptor of type " f"{other_name} with the same type code {other_type} and " f"number {other.number}") else: self.requestable_descriptors[identifier] = descriptor descriptor.parent = self # # Event handlers. # def handle_data_received(self, endpoint: USBEndpoint, data: bytes): """ Handler for receipt of non-control request data. Typically, this method will delegate any data received to the appropriate configuration/interface/endpoint. If overridden, the overriding function will receive all data; and can delegate it by calling the `.handle_data_received` method on `self.configuration`. Args: endpoint_number : The endpoint number on which the data was received. data : The raw bytes received on the relevant endpoint. """ if self.has_endpoint(endpoint.number, endpoint.direction): endpoint.handle_data_received(data) else: self.get_device().handle_unexpected_data_received(endpoint.number, data) def handle_data_requested(self, endpoint: USBEndpoint): """ Handler called when the host requests data on a non-control endpoint. Typically, this method will delegate the request to the appropriate interface+endpoint. If overridden, the overriding function will receive all data. Args: endpoint_number : The endpoint number on which the host requested data. """ if self.has_endpoint(endpoint.number, endpoint.direction): endpoint.handle_data_requested() else: self.get_device().handle_unexpected_data_requested(endpoint.number) def handle_buffer_empty(self, endpoint: USBEndpoint): """ Handler called when a given endpoint first has an empty buffer. Often, an empty buffer indicates an opportunity to queue data for sending ('prime an endpoint'), but doesn't necessarily mean that the host is planning on reading the data. This function is called only once per buffer. """ if self.has_endpoint(endpoint.number, endpoint.direction): endpoint.handle_buffer_empty() # # Backend helpers. # def get_endpoints(self): """ Returns an iterable over all endpoints in this interface. """ return self.endpoints.values() # # Internal interface. # @standard_request_handler(number=USBStandardRequests.GET_DESCRIPTOR) @to_this_interface def handle_get_descriptor_request(self, request): """ Handle GET_DESCRIPTOR requests; per USB2 [9.4.3] """ log.debug("Handling GET_DESCRIPTOR on endpoint.") # This is the same as the USBDevice get descriptor request => avoid duplication. self.get_device().handle_generic_get_descriptor_request(self, request) # Table 9-12 of USB 2.0 spec (pdf page 296) def get_descriptor(self) -> bytes: """ Retrieves the given interface's interface descriptor, with subordinates. """ # FIXME: use construct string_manager = self.get_device().strings d = bytearray([ 9, # length of descriptor in bytes 4, # descriptor type 4 == interface self.number, self.alternate, len(self.endpoints), self.class_number, self.subclass_number, self.protocol_number, string_manager.get_index(self.interface_string) ]) for descriptor in self.attached_descriptors: if callable(descriptor): d += descriptor() else: d += descriptor # ... append each endpoint's endpoint descriptor. for e in self.endpoints.values(): d += e.get_descriptor() for descriptor in e.attached_descriptors: if callable(descriptor): d += descriptor() else: d += descriptor return d # # Alternate interface support. # @standard_request_handler(number=USBStandardRequests.SET_INTERFACE) @to_this_interface def handle_set_interface_request(self, request: USBControlRequest): """ Handle SET_INTERFACE requests; per USB2 [9.4.10] """ log.debug(f"f{self.name} received SET_INTERFACE request") configuration = self.parent device = configuration.parent backend = device.backend if device.configuration is None: request.stall() else: try: # Find this alternate setting and switch to it. number = request.index_low alternate = request.value identifier = (number, alternate) interface = configuration.interfaces[identifier] configuration.active_interfaces[number] = interface # Reset the data toggles of this interface's endpoints. for endpoint in interface.endpoints.values(): backend.clear_halt(endpoint.number, endpoint.direction) request.acknowledge() except KeyError: request.stall() @standard_request_handler(number=USBStandardRequests.GET_INTERFACE) @to_this_interface def handle_get_interface_request(self, request): """ Handle GET_INTERFACE requests; per USB2 [9.4.4] """ log.debug("received GET_INTERFACE request") configuration = self.parent device = configuration.parent if device.configuration is None: request.stall() else: try: number = request.index_low interface = configuration.active_interfaces[number] request.reply(bytes([interface.alternate])) except KeyError: request.stall() # # Automatic instantiation support. # def get_identifier(self) -> (int, int): return (self.number, self.alternate) # Although we identify interfaces by (number, alternate), this helper # is called from the request handling code, where we only want to # match by interface number. The correct alternate interface should have # been selected earlier in the request handling process. def matches_identifier(self, other: int) -> bool: return (other == self.number) # # Request handler functions. # def _request_handlers(self) -> Iterable[callable]: return self._request_handler_methods def _get_subordinate_handlers(self) -> Iterable[callable]: return self.endpoints.values() def generate_code(self, name=None, indent=0): if name is None: if self.alternate == 0: name = f"Interface_{self.number}" else: name = f"Interface_{self.number}_{self.alternate}" code = f""" class {name}(USBInterface): number = {self.number} alternate = {self.alternate} class_number = {self.class_number} subclass_number = {self.subclass_number} protocol_number = {self.protocol_number} interface_string = {self.interface_string.generate_code()} """ # Use alphabetic suffixes to distinguish between multiple attached # descriptors with the same type number. suffixes = defaultdict(lambda: 'A') for descriptor in self.attached_descriptors: type_number = descriptor.type_number suffix = suffixes[type_number] suffixes[type_number] = chr(ord(suffix) + 1) name = f"Descriptor_0x{type_number:02X}_{suffix}" code += descriptor.generate_code(name=name, indent=4) for endpoint in self.endpoints.values(): code += endpoint.generate_code(indent=4) for descriptor_id in sorted(self.requestable_descriptors): descriptor = self.requestable_descriptors[descriptor_id] code += descriptor.generate_code(indent=4) return textwrap.indent(code, indent * ' ') ================================================ FILE: facedancer/logging.py ================================================ import functools import logging import sys LOGLEVEL_TRACE = 5 LOG_FORMAT_COLOR = "\u001b[37;1m%(levelname)-8s| \u001b[0m\u001b[1m%(module)-15s|\u001b[0m %(message)s" LOG_FORMAT_PLAIN = "%(levelname)-8s| %(module)-15s| %(message)s" def configure_default_logging(level=logging.INFO, logger=logging): if sys.stdout.isatty(): log_format = LOG_FORMAT_COLOR else: log_format = LOG_FORMAT_PLAIN logger.basicConfig(level=level, format=log_format) logging.getLogger("facedancer").level = level def _initialize_logging(): # add a TRACE level to logging logging.TRACE = LOGLEVEL_TRACE logging.addLevelName(logging.TRACE, "TRACE") logging.Logger.trace = functools.partialmethod(logging.Logger.log, logging.TRACE) logging.trace = functools.partial(logging.log, logging.TRACE) # Configure facedancer logger logger = logging.getLogger("facedancer") logger.level = logging.WARN return logger log = _initialize_logging() ================================================ FILE: facedancer/magic.py ================================================ # # This file is part of Facedancer. # """ Functionally for automatic instantiations / tracking via decorators. """ import inspect from abc import ABCMeta, abstractmethod from dataclasses import dataclass, is_dataclass, field, fields class DescribableMeta(ABCMeta): """ Metaclass for USBDescribable subclasses. """ def __new__(cls, name, bases, classdict): annotations = classdict.setdefault('__annotations__', {}) for base in bases: if is_dataclass(base): for field in fields(base): if field.name in classdict: if field.name not in annotations: annotations[field.name] = str(field.type) new_cls = ABCMeta.__new__(cls, name, bases, classdict) return dataclass(new_cls, kw_only=True) def adjust_defaults(cls, **kwargs): """ Adjusts the defaults of an existing dataclass. """ assert is_dataclass(cls) for name, value in kwargs.items(): cls.__dataclass_fields__[name] = field(default = value) cls.__init__.__kwdefaults__[name] = value return cls class AutoInstantiable(metaclass=DescribableMeta): """ Base class for methods that can be decorated with use_automatically. """ @abstractmethod def get_identifier(self) -> int: """ Returns a unique integer identifier for this object. This is usually the index or address of the relevant USB object. """ def matches_identifier(self, other: int) -> bool: return (other == self.get_identifier()) class AutoInstantiator: """ Simple wrapper class annotated on objects that can be instantiated automatically. Used for the @use_automatically decorator; which removes a lot of the Facedancer boilerplate at the cost of being somewhat cryptic. """ def __init__(self, target_type): self._target_type = target_type def creates_instance_of(self, expected_type): return issubclass(self._target_type, expected_type) def __call__(self, parent): return self._target_type(parent=parent) def use_automatically(cls): """ Class decorator used to annotate Facedancer inner classes. Implies @dataclass. This decorator can be placed on inner classes that describe "subordinate" objects on USB devices. For example, a USBDevice can have several subordinate USBConfigurations; which select the various configurations for that class. When placed on a subordinate class, this allows the parent class to automatically instantiate the relevant given class during its creation; automatically populating the subordinate properties of the relevant device. For example, assume we have a Facedancer class representing a custom USB device:: class ExampleDevice(USBDevice): product_string : str = "My Example Device" @use_automatically class DefaultConfiguration(USBConfiguration): number : int = 1 In this case, when an ExampleDevice is instantiated, the USBDevice code knows how to instantiate DefaultConfiguration, and will do so automatically. Note that this decorator should _only_ be used for subordinate types; and expects that the decorated class has no explicitly-declared __init__ method. The __post_init__ mechanism of python dataclasses can be overridden to perform any needed initialization. """ return AutoInstantiator(cls) def _use_inner_classes_automatically(cls): # Iterate over the relevant class... for name, member in cls.__dict__.items(): # ... and tag each inner class with both use_automatically # -and- use_inner_classes_automatically. The former if inspect.isclass(member) and issubclass(member, AutoInstantiable): wrapped_class = _use_inner_classes_automatically(member) wrapped_class = use_automatically(member) setattr(cls, name, wrapped_class) return cls def use_inner_classes_automatically(cls): """ Decorator that acts as if all inner classes were defined with `use_automatically`. """ return _use_inner_classes_automatically(cls) def instantiate_subordinates(obj, expected_type): """ Automatically instantiates any inner classes with a matching type. This is used by objects that represent USB hardware behaviors (e.g. USBDevice, USBConfiguration, USBInterface, USBEndpoint) in order to automatically create objects of any inner class decorated with ``@use_automatically``. """ # Search our class for anything decorated with an AutoInstantiator of the relevant type. for member in type(obj).__dict__.values(): if isinstance(member, AutoInstantiator) and member.creates_instance_of(expected_type): yield member(object) ================================================ FILE: facedancer/proxy.py ================================================ # # This file is part of Facedancer. # """ USB Proxy implementation. """ import atexit import platform import usb1 import sys from usb1 import USBError, USBErrorTimeout from . import DeviceSpeed, USBConfiguration, USBDirection from .device import USBBaseDevice from .errors import DeviceNotFoundError from .logging import log from .request import USBControlRequest from .types import USB class USBProxyDevice(USBBaseDevice): """ USB Proxy Device """ name = "USB Proxy Device" filter_list = [] def __init__(self, index=0, quirks=[], scheduler=None, **kwargs): """ Sets up a new USBProxy instance. """ # Finally, initialize our base class with a minimal set of # parameters. We'll do almost nothing, as we'll be proxying # packets by default to the device. super().__init__() # We have only one proxy backend in existence at this time. self.proxied_device = LibUSB1Device # Find the device to proxy matching the given keyword arguments... usb_devices = list(self.proxied_device.find(find_all=True, **kwargs)) if len(usb_devices) <= index: raise DeviceNotFoundError(f"Could not find device to proxy.") device = usb_devices[index] # Open a connection to the proxied device and attempt to # detach it from any kernel-side driver that may prevent us # from communicating with it... device_handle = self.proxied_device.open(device, detach=True) log.info(f"Found {self.proxied_device.device_speed().name} speed device to proxy: {device}") 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 connect(self): """ Initialize this device. We perform a reduced initialization, as we really only want to proxy data. """ # Always use a max_packet_size of 64 on EP0. # This works around a Linux spec violation in which Linux assumes it can read 64 bytes of # control descriptor no matter the device speed and actual maximum packet size. If this # doesn't work, Linux tries to reset / power-cycle the device, and then recovers with an # in-spec read; but this causes a huge delay and/or breakage, depending on the proxied # device. # # Since we're working at the transfer levels, the packet sizes will automatically be # translated, anyway. self.max_packet_size_ep0 = 64 # Get the USB device speed of the device being proxied. device_speed = self.proxied_device.device_speed() # Connect device. super().connect(device_speed=device_speed) # TODO check if we still need this in facedancer v3 # skipping USB.state_attached may not be strictly correct (9.1.1.{1,2}) self.state = USB.state_powered # - event handlers -------------------------------------------------------- def configured(self, configuration: USBConfiguration): """ 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. Args: configuration: The configuration to be applied. """ # All interfaces on the configuration are set to their default setting. configuration.active_interfaces = { interface.number : interface for interface in configuration.get_interfaces() if interface.alternate == 0 } # Pass our configuration on to the core device. self.backend.configured(configuration) configuration.parent = self # FIXME Not great semantics self.configuration = configuration def interface_changed(self, interface_number: int, alternate: int): """ Callback that handles when a SET_INTERFACE request is made to the target. If you're using the standard filters, this will be called automatically; if not, you'll have to call it once you know an alternate setting has been applied. Args: interface_number: The interface number. alternate: The alternate setting to be applied. """ identifier = (interface_number, alternate) interface = self.configuration.interfaces[identifier] self.configuration.active_interfaces[interface_number] = interface def handle_bus_reset(self): super().handle_bus_reset() def handle_request(self, request: USBControlRequest): """ Proxies EP0 requests between the victim and the target. """ if request.get_direction() == 1: self._proxy_in_control_request(request) else: self._proxy_out_control_request(request) def handle_get_configuration_request(self, request): super().handle_get_configuration_request(request) def handle_get_descriptor_request(self, request): super().handle_get_descriptor_request(request) def handle_data_available(self, ep_num, data): """ Handles the case where data is ready from the Facedancer device that needs to be proxied to the target device. """ # Run the data through all of our filters. for f in self.filter_list: ep_num, data = f.filter_out(ep_num, data) # If the data wasn't filtered out, communicate it to the target device. if data: try: self.proxied_device.write(ep_num, data) except USBError as e: stalled = True for f in self.filter_list: request, data, stalled = f.handle_out_stall(ep_num, data, stalled) if stalled: self.backend.stall_endpoint(0, USBDirection.OUT) 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. """ # Make sure the endpoint exists for the current configuration # before attempting to handle NAK events. # Skip handling OUT endpoints, as we handle those in handle_data_available. endpoint = self.configuration.get_endpoint(ep_num, USBDirection.IN) if endpoint is None: return # 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. self._proxy_in_transfer(endpoint) # - helpers --------------------------------------------------------------- def _ack_status_stage(self, blocking=False): self.backend.ack_status_stage(blocking=blocking) def _proxy_in_control_request(self, request: USBControlRequest): """ Proxy IN requests, which gather data from the device and forward it to the target host. """ data = [] stalled = False # Filter the setup stage generated by the target device. We can use this # to e.g. change the setup stage before proxying it to the target device, # or to absorb a packet before it's proxied. for f in self.filter_list: request, stalled = f.filter_control_in_setup(request, stalled) # If we stalled immediately, handle the stall and return without proxying. if stalled: self.backend.stall_endpoint(0, USBDirection.IN) return # If we filtered out the setup request, NAK. if request is None: return # Read any data from the real device... try: data = self.proxied_device.controlRead( request_type=request.request_type, request=request.request, value=request.value, index=request.index, length=request.length, ) except USBError as e: stalled = True # Run filters here. for f in self.filter_list: request, data, stalled = f.filter_control_in(request, data, stalled) #... and proxy it to our victim. if stalled: # TODO: allow stalling of eps other than 0! self.backend.stall_endpoint(0, USBDirection.IN) else: # TODO: support control endpoints other than 0 self.control_send(0, request, data) def _proxy_out_control_request(self, request: USBControlRequest): """ Proxy OUT requests, which sends a request from the victim to the target device. """ data = request.data for f in self.filter_list: request, data = f.filter_control_out(request, data) # ... forward the request to the real device. if request: try: self.proxied_device.controlWrite( request_type=request.request_type, request=request.request, value=request.value, index=request.index, data=data ) self._ack_status_stage() # Special case: we've stalled, allow the filters to decide what to do. except USBError as e: stalled = True for f in self.filter_list: request, data, stalled = f.handle_out_request_stall(request, data, stalled) if stalled: self.backend.stall_endpoint(0, USBDirection.OUT) 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. """ ep_num = endpoint.number # Filter the "IN token" generated by the target device. We can use this # to e.g. change the endpoint before proxying to the target device, or # to absorb a packet before it's proxied. for f in self.filter_list: ep_num = f.filter_in_token(ep_num) if ep_num is None: return try: # Quick hack to improve responsiveness on interrupt endpoints. if endpoint.interval: data = self.proxied_device.read(ep_num, endpoint.max_packet_size, timeout=endpoint.interval) else: data = self.proxied_device.read(ep_num, endpoint.max_packet_size) except usb1.USBErrorPipe: self.proxied_device.clear_halt(ep_num, USBDirection.IN) return except USBErrorTimeout: return # 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(data) class LibUSB1Device: """ A wrapper around the proxied device based on libusb1. """ """ Class variable that stores our global libusb library context. """ context = None """ Class variable that stores our device handle. """ device_handle = None @classmethod def _get_libusb_context(cls): """ Retrieves the libusb context we'll use to fetch libusb device instances. """ # If we don't have a libusb context, create one. if cls.context is None: cls.context = usb1.USBContext().__enter__() atexit.register(cls._destroy_libusb_context) return cls.context @classmethod def _destroy_libusb_context(cls): """ Destroys our libusb context on closing our python instance. """ if cls.device_handle is not None: device = cls.device_handle.getDevice() number = cls.device_handle.getConfiguration() active_configuration = next(filter(lambda c: c.getConfigurationValue() == number, device), None) if active_configuration: for interface in active_configuration: number = interface[0].getNumber() try: cls.device_handle.releaseInterface(number) except usb1.USBErrorNotFound as e: log.warning(f"Failed to releace interface {0} for {device}") pass cls.device_handle.close() cls.device_handle = None if cls.context is not None: cls.context.close() cls.context = None @classmethod def open(cls, device, detach=True): cls.device_handle = device.open() try: cls.device_handle.setAutoDetachKernelDriver(detach) except usb1.USBErrorNotSupported: pass number = cls.device_handle.getConfiguration() active_configuration = next(filter(lambda c: c.getConfigurationValue() == number, device), None) if active_configuration: for interface in active_configuration: number = interface[0].getNumber() try: cls.device_handle.claimInterface(number) except usb1.USBErrorAccess: log.error(f"Failed to claim interface {number} for {device}") if platform.system() == "Darwin": log.error("You may need to run your proxy code as root.\n") elif platform.system() == "Linux": log.error("Please ensure you have configured an entry for the device in your") log.error("/etc/udev/rules.d directory.\n") elif platform.system() == "Windows": log.error("You may need to experiment with the Zadig driver to access the device.\n") sys.exit(1) return cls.device_handle # TODO adapt logic from pygreat usb1.py @classmethod def find(cls, idVendor, idProduct, find_all=True): """ Finds a USB device by its identifiers. """ matching_devices = [] context = cls._get_libusb_context() for device in context.getDeviceList(): if device.getVendorID() == idVendor and device.getProductID() == idProduct: matching_devices.append(device) if find_all: return matching_devices elif matching_devices: return matching_devices else: return None @classmethod def device_speed(cls): return DeviceSpeed(cls.device_handle.getDevice().getDeviceSpeed()) @classmethod def controlRead(cls, request_type, request, value, index, length, timeout=1000): return cls.device_handle.controlRead(request_type, request, value, index, length, timeout) @classmethod def controlWrite(cls, request_type, request, value, index, data, timeout=1000): return cls.device_handle.controlWrite(request_type, request, value, index, data, timeout) @classmethod def read(cls, endpoint_number, length, timeout=1000): # Avoid accidental uses of endpoint address endpoint_number = endpoint_number & 0x7f # TODO support interrupt endpoints return cls.device_handle.bulkRead(endpoint_number, length, timeout) @classmethod def write(cls, endpoint_number, data, timeout=1000): # TODO support interrupt endpoints return cls.device_handle.bulkWrite(endpoint_number, data, timeout) @classmethod def clear_halt(cls, endpoint_number, direction): endpoint_address = direction.to_endpoint_address(endpoint_number) return cls.device_handle.clearHalt(endpoint_address) if __name__ == "__main__": from . import FacedancerUSBApp from .filters.standard import USBProxySetupFilters from .filters.logging import USBProxyPrettyPrintFilter # akai midimix VENDOR_ID = 0x09e8 PRODUCT_ID = 0x0031 # xbox controller #VENDOR_ID = 0x045e #PRODUCT_ID = 0x02d1 device = USBProxyDevice(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) device.add_filter(USBProxySetupFilters(device, verbose=2)) device.add_filter(USBProxyPrettyPrintFilter(verbose=5)) async def configure_logging(): import logging logging.getLogger("facedancer").setLevel(logging.INFO) from facedancer import main main(device, configure_logging()) ================================================ FILE: facedancer/request.py ================================================ # # This file is part of Facedancer. # """ Functionality for declaring and working with USB control requests. """ import inspect import warnings import functools from typing import List, Iterable from dataclasses import dataclass from abc import ABCMeta, abstractmethod from .descriptor import USBDescribable from .types import USBRequestRecipient, USBRequestType, USBDirection, USBStandardRequests def _wrap_with_field_matcher(func, field_name, field_value, match_index=False): """ Internal function; generates a request-refinement decorator. This generates a decorator that ensures a request-handler is only executed if its one if its field (named `field_name`) matches a given value. As an example, if this is called with `field_name`='index' and 'field_value'=3, this modifies `func` so it is only executed for requests with an index of 3. Args: func : The handler function to wrap. field_name : The name of the field to check. field_value : The value the given field must have for the function to execute. match_index : If true, the matcher is further refined in order to only execute for requests targeting a given e.g. interface or endpoint object. In this case, the handler is only executed if the low byte of the function's index matches the owning object's identifier, as verified with `matches_identifier`. """ @functools.wraps(func) def _wrapped(caller, request): # Compute our two conditions... field_matches = (getattr(request, field_name) == field_value) index_matches = \ caller.matches_identifier(request.index & 0xff) \ if hasattr(caller, "matches_identifier") and match_index else True # ... and call the inner function only if they match. if field_matches and index_matches: func(caller, request) # Otherwise, raise NotImplemented, which translates to a "not handled here". else: raise NotImplementedError() return _wrapped class ControlRequestHandler: """ Class representing a control request handler. Instances of this class are generated automatically each time a control request is defined using decorator syntax; and track the association between the relevant handler function and the condition under which it's executed. """ def __init__(self, handler_function, execution_condition): self._handler = handler_function self._condition = execution_condition def __call__(self, caller, request): """ Primary execution; calls the relevant handler if our conditions are met. """ if self._condition(request): try: self._handler(caller, request) return True except NotImplementedError: return False def add_condition(self, condition): """ Refines a control request handler such that it's only called when the added condition is true. """ base_condition = self._condition self._condition = lambda req : base_condition(req) and condition(req) def add_field_matcher(self, field_name, field_value): """ Refines a control request handler such that it's only called when one of its fields matches a given value. Args: field_name : The property of the USBControlRequest object to be checked. field_value : The value the relevant property must match to be called. """ matcher = lambda req : getattr(req, field_name) == field_value self.add_condition(matcher) def __repr__(self): return f" List[callable]: """ Returns a list of all handler methods on a given class or object. This is used to find all methods of an object decorated with the @*_request_handler decorators. """ members = inspect.getmembers(cls) return [m for _, m in members if isinstance(m, ControlRequestHandler)] # # Control request definitions. # @dataclass class USBControlRequest: """ Class encapsulating a USB control request. TODO: document parameters """ direction : USBDirection type : USBRequestType recipient : USBRequestRecipient number : int value : int index : int length : int data : bytes = b"" device : USBDescribable = None @classmethod def from_raw_bytes(cls, raw_bytes: bytes, *, device = None): """ Creates a request object from a sequence of raw bytes. Args: raw_bytes : The raw bytes to create the object from. device : The USBDevice to associate with the given request. Optional, but necessary to use the .reply() / .acknowledge() methods. """ # FIXME: parse using construct fields = { 'direction': (raw_bytes[0] >> 7) & 0b1, 'type': (raw_bytes[0] >> 5) & 0b11, 'recipient': (raw_bytes[0] >> 0) & 0b11111, 'number': raw_bytes[1], 'value': (raw_bytes[3] << 8) | raw_bytes[2], 'index': (raw_bytes[5] << 8) | raw_bytes[4], 'length': (raw_bytes[7] << 8) | raw_bytes[6], 'data': raw_bytes[8:], 'device': device } return cls(**fields) # # I/O API. # def reply(self, data: bytes): """ Replies to the given request with a given set of bytes. """ self.device.control_send(endpoint_number=0, in_request=self, data=data) def acknowledge(self, *, blocking: bool = False): """ Acknowledge the given request without replying. Args: blocking : If true, the relevant control request will complete before returning. """ self.device.control_send(endpoint_number=0, in_request=self, data=b"", blocking=blocking) def ack(self, *, blocking: bool = False): """ Acknowledge the given request without replying. Convenience alias for .acknowledge(). Args: blocking : If true, the relevant control request will complete before returning. """ self.acknowledge(blocking=blocking) def stall(self): """ Stalls the associated device's control request. Used to indicate that a given request isn't supported; or isn't supported with the provided arguments. """ # Always stall IN endpoint for control requests self.device.stall(endpoint_number=0, direction=USBDirection.IN) # # Properties. # @property def request(self) -> int: warnings.warn('`request` should be replaced with `number`', DeprecationWarning) return self.number @property def request_type(self) -> int: """ Fetches the whole `request_type` byte. """ return (self.direction << 7) | \ (self.type << 5) | \ (self.recipient << 0) @property def value_low(self) -> int: return self.value & 0xff @property def value_high(self) -> int: return self.value >> 8 @property def index_low(self) -> int: return self.index & 0xff @property def index_high(self) -> int: return self.index >> 8 def get_direction(self) -> USBDirection: return self.direction def get_type(self) -> USBRequestType: return self.type def get_recipient(self) -> USBRequestRecipient: return self.recipient def raw(self) -> bytes: """ Returns the raw bytes that compose the request. """ # FIXME: use construct? b = bytes([ self.request_type, self.number, self.value & 0xff, (self.value >> 8) & 0xff, self.index & 0xff, (self.index >> 8) & 0xff, self.length & 0xff, (self.length >> 8) & 0xff ]) return b # # Pretty printing & log output. # def __str__(self): direction = USBDirection(self.direction).name type_name = USBRequestType(self.type).name recipient = USBRequestRecipient.from_integer(self.recipient).name name = f"0x{self.number:02x}" # If this is a standard request, try to convert it to a name. if self.type == USBRequestType.STANDARD: try: name = f"{USBStandardRequests(self.number).name} (0x{self.number:02x})" except ValueError: pass return f"{direction} {type_name} request {name} to {recipient} " \ f"[value=0x{self.value:04x}, index=0x{self.index:04x}, length={self.length}]" class USBRequestHandler(metaclass=ABCMeta): """ Base class for any object that handles USB requests. """ @abstractmethod def _request_handlers(self) -> Iterable[callable]: """ Returns an iterable of request handlers provided by the class. """ def _get_subordinate_handlers(self) -> Iterable[callable]: """ Returns an iterable of subordinate handlers who should have an opportunity to handle requests. Normally called by _call_subordinate_handlers; may not be valid if that function is overridden. """ return () def _call_subordinate_handlers(self, request: USBControlRequest) -> bool: """ Calls the ``handle_request`` method of any subordinate handlers. This default implementation uses get_subordinates to get an iterable of subordinates we should call handle_request on. Returns: true iff the request is handled """ handled = False for configuration in self._get_subordinate_handlers(): handled = handled or configuration.handle_request(request) return handled def handle_request(self, request: USBControlRequest) -> bool: """ Core control request handler. This function can be overridden by a subclass if desired; but the typical way to handle a specific control request is to the the ``@control_request_handler`` decorators. Args: request : the USBControlRequest object representing the relevant request Returns: true iff the request is handled """ handled = False # Our default implementation is simple: we try every handler; allowing any # handler that wants to handle the relevant function a chance to handle it. # # Calling the handler for _every_ matching request (as opposed to e.g. the first one) # allows one to trivially add observers. for handler in self._request_handlers(): handled = handler(self, request) or handled # Pass our requests down to our subordinates, as well. handled = self._call_subordinate_handlers(request) or handled return handled ================================================ FILE: facedancer/types.py ================================================ # # This file is part of Facedancer. # """ USB types -- defines enumerations that describe standard USB types """ from enum import Enum, IntFlag, IntEnum class USBDirection(IntEnum): """ Class representing USB directions. """ OUT = 0 IN = 1 def is_in(self): return self is self.IN def is_out(self): return self is self.OUT @classmethod def parse(cls, value): """ Helper that converts a numeric field into a direction. """ return cls(value) @classmethod def from_request_type(cls, request_type_int): """ Helper method that extracts the direction from a request_type integer. """ return cls(request_type_int >> 7) @classmethod def from_endpoint_address(cls, address): """ Helper method that extracts the direction from an endpoint address. """ return cls(address >> 7) def token(self): """ Generates the token corresponding to the given direction. """ return USBPacketID.IN if (self is self.IN) else USBPacketID.OUT def reverse(self): """ Returns the reverse of the given direction. """ return self.OUT if (self is self.IN) else self.IN def to_endpoint_address(self, endpoint_number): """ Helper method that converts and endpoint_number to an address, given direction. """ if self.is_in(): return endpoint_number | (1 << 7) else: return endpoint_number class USBPIDCategory(IntFlag): """ Category constants for each of the groups that PIDs can fall under. """ SPECIAL = 0b00 TOKEN = 0b01 HANDSHAKE = 0b10 DATA = 0b11 MASK = 0b11 class USBPacketID(IntFlag): """ Enumeration specifying all of the valid USB PIDs we can handle. """ # Token group (lsbs = 0b01). OUT = 0b0001 IN = 0b1001 SOF = 0b0101 SETUP = 0b1101 # Data group (lsbs = 0b11). DATA0 = 0b0011 DATA1 = 0b1011 DATA2 = 0b0111 MDATA = 0b1111 # Handshake group (lsbs = 0b10) ACK = 0b0010 NAK = 0b1010 STALL = 0b1110 NYET = 0b0110 # Special group. PRE = 0b1100 ERR = 0b1100 SPLIT = 0b1000 PING = 0b0100 # Flag representing that the PID seems invalid. PID_INVALID = 0b10000 PID_CORE_MASK = 0b01111 @classmethod def from_byte(cls, byte, skip_checks=False): """ Creates a PID object from a byte. """ # Convert the raw PID to an integer. pid_as_int = int.from_bytes(byte, byteorder='little') return cls.from_int(pid_as_int, skip_checks=skip_checks) @classmethod def from_int(cls, value, skip_checks=True): """ Create a PID object from an integer. """ PID_MASK = 0b1111 INVERTED_PID_SHIFT = 4 # Pull out the PID and its inverse from the byte. pid = cls(value & PID_MASK) inverted_pid = value >> INVERTED_PID_SHIFT # If we're not skipping checks, if not skip_checks: if (pid ^ inverted_pid) != PID_MASK: pid |= cls.PID_INVALID return cls(pid) @classmethod def from_name(cls, name): """ Create a PID object from a string representation of its name. """ return cls[name] @classmethod def parse(cls, value): """ Attempt to create a PID object from a number, byte, or string. """ if isinstance(value, bytes): return cls.from_byte(value) if isinstance(value, str): return cls.from_name(value) if isinstance(value, int): return cls.from_int(value) return cls(value) def category(self): """ Returns the USBPIDCategory that each given PID belongs to. """ return USBPIDCategory(self & USBPIDCategory.MASK) def is_data(self): """ Returns true iff the given PID represents a DATA packet. """ return self.category() is USBPIDCategory.DATA def is_token(self): """ Returns true iff the given PID represents a token packet. """ return self.category() is USBPIDCategory.TOKEN def is_handshake(self): """ Returns true iff the given PID represents a handshake packet. """ return self.category() is USBPIDCategory.HANDSHAKE def is_invalid(self): """ Returns true if this object is an attempt to encapsulate an invalid PID. """ return (self & self.PID_INVALID) def direction(self): """ Get a USB direction from a PacketID. """ if self is self.SOF: return None if self is self.SETUP or self is self.OUT: return USBDirection.OUT if self is self.IN: return USBDirection.IN raise ValueError("cannot determine the direction of a non-token PID") def summarize(self): """ Return a summary of the given packet. """ # By default, get the raw name. core_pid = self & self.PID_CORE_MASK name = core_pid.name if self.is_invalid(): return "{} (check-nibble invalid)".format(name) else: return name class USBRequestRecipient(IntEnum): """ Enumeration that describes each 'recipient' of a USB request field. """ DEVICE = 0 INTERFACE = 1 ENDPOINT = 2 OTHER = 3 RESERVED = 4 @classmethod def from_integer(cls, value): """ Special factory that correctly handles reserved values. """ # If we have one of the reserved values; indicate so. if 4 <= value < 32: return cls.RESERVED # Otherwise, translate the raw value. return cls(value) @classmethod def from_request_type(cls, request_type_int): """ Helper method that extracts the type from a request_type integer. """ MASK = 0b11111 return cls(request_type_int & MASK) class USBRequestType(IntEnum): """ Enumeration that describes each possible Type field for a USB request. """ STANDARD = 0 CLASS = 1 VENDOR = 2 RESERVED = 3 @classmethod def from_request_type(cls, request_type_int): """ Helper method that extracts the type from a request_type integer. """ SHIFT = 5 MASK = 0b11 return cls((request_type_int >> SHIFT) & MASK) class USBTransferType(IntEnum): CONTROL = 0 ISOCHRONOUS = 1 BULK = 2 INTERRUPT = 3 def endpoint_number_from_address(number): return number & 0x7F class LanguageIDs(IntEnum): AFRIKAANS = 0X0436 ALBANIAN = 0X041C ARABIC_SAUDI_ARABIA = 0X0401 ARABIC_IRAQ = 0X0801 ARABIC_EGYPT = 0X0C01 ARABIC_LIBYA = 0X1001 ARABIC_ALGERIA = 0X1401 ARABIC_MOROCCO = 0X1801 ARABIC_TUNISIA = 0X1C01 ARABIC_OMAN = 0X2001 ARABIC_YEMEN = 0X2401 ARABIC_SYRIA = 0X2801 ARABIC_JORDAN = 0X2C01 ARABIC_LEBANON = 0X3001 ARABIC_KUWAIT = 0X3401 ARABIC_UAE = 0X3801 ARABIC_BAHRAIN = 0X3C01 ARABIC_QATAR = 0X4001 ARMENIAN = 0X042B ASSAMESE = 0X044D AZERI_LATIN = 0X042C AZERI_CYRILLIC = 0X082C BASQUE = 0X042D BELARUSSIAN = 0X0423 BENGALI = 0X0445 BULGARIAN = 0X0402 BURMESE = 0X0455 CATALAN = 0X0403 CHINESE_TAIWAN = 0X0404 CHINESE_PRC = 0X0804 CHINESE_HONG_KONG = 0X0C04 CHINESE_SINGAPORE = 0X1004 CHINESE_MACAU_SAR = 0X1404 CROATIAN = 0X041A CZECH = 0X0405 DANISH = 0X0406 DUTCH_NETHERLANDS = 0X0413 DUTCH_BELGIUM = 0X0813 ENGLISH_US = 0X0409 ENGLISH_UNITED_KINGDOM = 0X0809 ENGLISH_AUSTRALIAN = 0X0C09 ENGLISH_CANADIAN = 0X1009 ENGLISH_NEW_ZEALAND = 0X1409 ENGLISH_IRELAND = 0X1809 ENGLISH_SOUTH_AFRICA = 0X1C09 ENGLISH_JAMAICA = 0X2009 ENGLISH_CARIBBEAN = 0X2409 ENGLISH_BELIZE = 0X2809 ENGLISH_TRINIDAD = 0X2C09 ENGLISH_ZIMBABWE = 0X3009 ENGLISH_PHILIPPINES = 0X3409 ESTONIAN = 0X0425 FAEROESE = 0X0438 FARSI = 0X0429 FINNISH = 0X040B FRENCH_STANDARD = 0X040C FRENCH_BELGIAN = 0X080C FRENCH_CANADIAN = 0X0C0C FRENCH_SWITZERLAND = 0X100C FRENCH_LUXEMBOURG = 0X140C FRENCH_MONACO = 0X180C GEORGIAN = 0X0437 GERMAN_STANDARD = 0X0407 GERMAN_SWITZERLAND = 0X0807 GERMAN_AUSTRIA = 0X0C07 GERMAN_LUXEMBOURG = 0X1007 GERMAN_LIECHTENSTEIN = 0X1407 GREEK = 0X0408 GUJARATI = 0X0447 HEBREW = 0X040D HINDI = 0X0439 HUNGARIAN = 0X040E ICELANDIC = 0X040F INDONESIAN = 0X0421 ITALIAN_STANDARD = 0X0410 ITALIAN_SWITZERLAND = 0X0810 JAPANESE = 0X0411 KANNADA = 0X044B KASHMIRI_INDIA = 0X0860 KAZAKH = 0X043F KONKANI = 0X0457 KOREAN = 0X0412 KOREAN_JOHAB = 0X0812 LATVIAN = 0X0426 LITHUANIAN = 0X0427 LITHUANIAN_CLASSIC = 0X0827 MACEDONIAN = 0X042F MALAY_MALAYSIAN = 0X043E MALAY_BRUNEI_DARUSSALAM = 0X083E MALAYALAM = 0X044C MANIPURI = 0X0458 MARATHI = 0X044E NEPALI_INDIA = 0X0861 NORWEGIAN_BOKMAL = 0X0414 NORWEGIAN_NYNORSK = 0X0814 ORIYA = 0X0448 POLISH = 0X0415 PORTUGUESE_BRAZIL = 0X0416 PORTUGUESE_STANDARD = 0X0816 PUNJABI = 0X0446 ROMANIAN = 0X0418 RUSSIAN = 0X0419 SANSKRIT = 0X044F SERBIAN_CYRILLIC = 0X0C1A SERBIAN_LATIN = 0X081A SINDHI = 0X0459 SLOVAK = 0X041B SLOVENIAN = 0X0424 SPANISH_TRADITIONAL_SORT = 0X040A SPANISH_MEXICAN = 0X080A SPANISH_MODERN_SORT = 0X0C0A SPANISH_GUATEMALA = 0X100A SPANISH_COSTA_RICA = 0X140A SPANISH_PANAMA = 0X180A SPANISH_DOMINICAN_REPUBLIC = 0X1C0A SPANISH_VENEZUELA = 0X200A SPANISH_COLOMBIA = 0X240A SPANISH_PERU = 0X280A SPANISH_ARGENTINA = 0X2C0A SPANISH_ECUADOR = 0X300A SPANISH_CHILE = 0X340A SPANISH_URUGUAY = 0X380A SPANISH_PARAGUAY = 0X3C0A SPANISH_BOLIVIA = 0X400A SPANISH_EL_SALVADOR = 0X440A SPANISH_HONDURAS = 0X480A SPANISH_NICARAGUA = 0X4C0A SPANISH_PUERTO_RICO = 0X500A SUTU = 0X0430 SWAHILI_KENYA = 0X0441 SWEDISH = 0X041D SWEDISH_FINLAND = 0X081D TAMIL = 0X0449 TATAR_TATARSTAN = 0X0444 TELUGU = 0X044A THAI = 0X041E TURKISH = 0X041F UKRAINIAN = 0X0422 URDU_PAKISTAN = 0X0420 URDU_INDIA = 0X0820 UZBEK_LATIN = 0X0443 UZBEK_CYRILLIC = 0X0843 VIETNAMESE = 0X042A HID_USAGE_DATA_DESCRIPTOR = 0X04FF HID_VENDOR_DEFINED_1 = 0XF0FF HID_VENDOR_DEFINED_2 = 0XF4FF HID_VENDOR_DEFINED_3 = 0XF8FF HID_VENDOR_DEFINED_4 = 0XFCFF class DescriptorTypes(IntEnum): DEVICE = 1 CONFIGURATION = 2 STRING = 3 INTERFACE = 4 ENDPOINT = 5 DEVICE_QUALIFIER = 6 OTHER_SPEED_CONFIGURATION = 7 INTERFACE_POWER = 8 HID = 33 REPORT = 34 class USBSynchronizationType(IntEnum): NONE = 0x00 ASYNC = 0x01 ADAPTIVE = 0x02 SYNCHRONOUS = 0x03 class USBUsageType(IntEnum): DATA = 0 FEEDBACK = 1 IMPLICIT_FEEDBACK = 2 class USBStandardRequests(IntEnum): GET_STATUS = 0 CLEAR_FEATURE = 1 SET_FEATURE = 3 SET_ADDRESS = 5 GET_DESCRIPTOR = 6 SET_DESCRIPTOR = 7 GET_CONFIGURATION = 8 SET_CONFIGURATION = 9 GET_INTERFACE = 10 SET_INTERFACE = 11 SYNCH_FRAME = 12 # Based on libusb's LIBUSB_SPEED_* constants. # # See: https://github.com/libusb/libusb/blob/master/libusb/libusb.h#L1126 class DeviceSpeed(IntEnum): UNKNOWN = 0 LOW = 1 FULL = 2 HIGH = 3 SUPER = 4 SUPER_PLUS = 5 # Contains definition of USB class, which is just a container for a bunch of # constants/enums associated with the USB protocol. # # TODO: would be nice if this module could re-export the other USB* classes so # one need import only USB to get all the functionality # # TODO: check if it still makes sense to keep this around in facedancer v3, it's # only used by USBProxyDevice class USB: state_detached = 0 state_attached = 1 state_powered = 2 state_default = 3 state_address = 4 state_configured = 5 state_suspended = 6 request_direction_host_to_device = 0 request_direction_device_to_host = 1 request_type_standard = 0 request_type_class = 1 request_type_vendor = 2 request_recipient_device = 0 request_recipient_interface = 1 request_recipient_endpoint = 2 request_recipient_other = 3 feature_endpoint_halt = 0 feature_device_remote_wakeup = 1 feature_test_mode = 2 desc_type_device = 1 desc_type_configuration = 2 desc_type_string = 3 desc_type_interface = 4 desc_type_endpoint = 5 desc_type_device_qualifier = 6 desc_type_other_speed_configuration = 7 desc_type_interface_power = 8 desc_type_hid = 33 desc_type_report = 34 # while this holds for HID, it may not be a correct model for the USB # ecosystem at large if_class_to_desc_type = { 3 : desc_type_hid } def interface_class_to_descriptor_type(interface_class): return USB.if_class_to_desc_type.get(interface_class, None) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=64", "wheel", "setuptools-git-versioning<2"] build-backend = "setuptools.build_meta" [project] name = "facedancer" description = "Implement your own USB device in Python, supported by a hardware peripheral such as Cynthion or GreatFET." license = { text = "BSD" } readme = "README.md" requires-python = ">=3.10" authors = [ {name = "Great Scott Gadgets", email = "dev@greatscottgadgets.com"}, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Natural Language :: English", "Environment :: Console", "Environment :: Plugins", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", "Topic :: Security", "Topic :: System :: Hardware :: Universal Serial Bus (USB)", ] dependencies = [ "pyusb", "pyserial", "prompt-toolkit", "libusb1", "hid-parser>=0.1.0", ] dynamic = ["version"] [project.urls] Documentation = "https://facedancer.readthedocs.io" Repository = "https://github.com/greatscottgadgets/facedancer" Issues = "https://github.com/greatscottgadgets/facedancer/issues" [tool.setuptools.package-dir] facedancer = "facedancer" [tool.setuptools-git-versioning] enabled = true starting_version = "3.0.0" ================================================ FILE: test/README.md ================================================ ## Running tests 1. Connect your Facedancer hardware 2. In one terminal, start the test device from the repository root: python -m test.device 3. In another terminal, run the tests from the repository root: python -m unittest ================================================ FILE: test/__init__.py ================================================ ================================================ FILE: test/base.py ================================================ # # This file is part of Facedancer. # import logging import unittest import usb1 from facedancer.errors import DeviceNotFoundError from facedancer.types import USBStandardRequests VENDOR_ID = 0x1209 PRODUCT_ID = 0x0001 OUT_ENDPOINT = 0x01 IN_ENDPOINT = 0x82 OUT_ALT_ENDPOINT = 0x03 IN_ALT_ENDPOINT = 0x84 # This is constrained by pygreat::comms_backends::usb1::LIBGREAT_MAX_COMMAND_SIZE # and is board dependent. MAX_TRANSFER_LENGTH = 768 class FacedancerTestCase(unittest.TestCase): # - life-cycle ------------------------------------------------------------ @classmethod def setUpClass(cls): logging.basicConfig(level=logging.INFO) cls.context = usb1.USBContext().open() cls.device_handle = cls.context.openByVendorIDAndProductID(VENDOR_ID, PRODUCT_ID) if cls.device_handle is None: raise Exception("device not found") cls.device_handle.claimInterface(0) @classmethod def tearDownClass(cls): cls.context.close() # - transfers ------------------------------------------------------------- def bulk_out_transfer(self, ep, data): logging.debug("Testing bulk OUT endpoint") response = self.device_handle.bulkWrite( endpoint = ep, data = data, timeout = 1000, ) logging.debug(f"sent {response} bytes\n") return response def bulk_in_transfer(self, ep, length): logging.debug("Testing bulk IN endpoint") response = self.device_handle.bulkRead( endpoint = ep, length = length, timeout = 1000, ) logging.debug(f"[host] received '{len(response)}' bytes from bulk endpoint") return response def control_out_transfer(self, data): logging.debug("Testing OUT control transfer") hi, lo = len(data).to_bytes(2, byteorder="big") response = self.device_handle.controlWrite( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 10, index = hi, value = lo, data = data, timeout = 1000, ) logging.debug(f"sent {response} bytes\n") return response def control_in_transfer(self, length): logging.debug("Testing IN control transfer") hi, lo = length.to_bytes(2, byteorder="big") response = self.device_handle.controlRead( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 20, index = hi, value = lo, length = length, timeout = 1000, ) logging.debug(f"[host] received '{len(response)}' bytes from control endpoint") return response # - device control ------------------------------------------------------------ def set_in_transfer_length(self, length): hi, lo = length.to_bytes(2, byteorder="big") logging.debug(f"Setting transfer length to {length} bytes") response = self.device_handle.controlWrite( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 1, index = hi, value = lo, data = [], timeout = 1000, ) return response def get_last_out_transfer_data(self): logging.debug("Getting last OUT transfer data") response = self.device_handle.controlRead( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 2, index = 0, value = 0, length = MAX_TRANSFER_LENGTH, timeout = 1000, ) logging.debug(f"[host] sent '{len(response)}' bytes with last out transfer") return response def reset_device_state(self): logging.debug(f"Resetting stress test device state") response = self.device_handle.controlWrite( request_type = usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE, request = 3, index = 0, value = 0, data = [], timeout = 1000, ) return response def set_interface(self, interface_number, alternate): logging.debug("Setting interface {interface_number} to alternate setting {alternate}") self.device_handle.setInterfaceAltSetting(interface_number, alternate) def get_interface(self, interface_number): logging.debug("Getting alternate setting of interface {interface}") response = self.device_handle.controlRead( request_type = usb1.TYPE_STANDARD | usb1.RECIPIENT_INTERFACE, request = USBStandardRequests.GET_INTERFACE, index = interface_number, value = 0, length = 1, timeout = 1000, ) return response[0] ================================================ FILE: test/device.py ================================================ #!/usr/bin/env python3 # pylint: disable=unused-wildcard-import, wildcard-import # # This file is part of Facedancer. # import logging, random, sys from facedancer import * from facedancer import main from .base import VENDOR_ID, PRODUCT_ID, OUT_ENDPOINT, IN_ENDPOINT, OUT_ALT_ENDPOINT, IN_ALT_ENDPOINT, MAX_TRANSFER_LENGTH DEVICE_SPEED = DeviceSpeed.HIGH @use_inner_classes_automatically class StressTestDevice(USBDevice): product_string : str = "Stress Test Device" manufacturer_string : str = "Facedancer" vendor_id : int = VENDOR_ID product_id : int = PRODUCT_ID device_speed : DeviceSpeed = DEVICE_SPEED def __post_init__(self): super().__post_init__() self.in_transfer_length = 0 self.last_out_transfer_data = bytearray() class MyConfiguration(USBConfiguration): class MyInterface(USBInterface): number : int = 0 alternate : int = 0 class MyOutEndpoint(USBEndpoint): number : int = OUT_ENDPOINT direction : USBDirection = USBDirection.OUT max_packet_size : int = 512 if DEVICE_SPEED == DeviceSpeed.HIGH else 64 def handle_data_received(self: USBEndpoint, data): self.get_device().last_out_transfer_data += bytes(data) logging.debug(f"test_bulk_out received {len(data)} bytes") class MyInEndpoint(USBEndpoint): number : int = IN_ENDPOINT & 0x7f direction : USBDirection = USBDirection.IN max_packet_size : int = 512 if DEVICE_SPEED == DeviceSpeed.HIGH else 64 def handle_data_requested(self: USBEndpoint): in_transfer_length = self.get_device().in_transfer_length logging.debug(f"test_bulk_in sending {in_transfer_length} bytes") self.send(generate_data(in_transfer_length), blocking=False) class MyAlternateInterface(USBInterface): number : int = 0 alternate : int = 1 class MyOutEndpoint(USBEndpoint): number : int = OUT_ALT_ENDPOINT direction : USBDirection = USBDirection.OUT max_packet_size : int = 512 if DEVICE_SPEED == DeviceSpeed.HIGH else 64 def handle_data_received(self: USBEndpoint, data): self.get_device().last_out_transfer_data += bytes(data) logging.debug(f"test_bulk_out alternate received {len(data)} bytes") class MyInEndpoint(USBEndpoint): number : int = IN_ALT_ENDPOINT & 0x7f direction : USBDirection = USBDirection.IN max_packet_size : int = 512 if DEVICE_SPEED == DeviceSpeed.HIGH else 64 def handle_data_requested(self: USBEndpoint): in_transfer_length = self.get_device().in_transfer_length logging.debug(f"test_bulk_in alternate sending {in_transfer_length} bytes") self.send(generate_data(in_transfer_length), blocking=False) @vendor_request_handler(number=10, direction=USBDirection.OUT) @to_device def out_vendor_request(self: USBDevice, request: USBControlRequest): self.last_out_transfer_data += bytes(request.data) length = int.from_bytes([request.index, request.value], byteorder="big") logging.debug(f"test_control_out received {len(request.data)}/{length} bytes") request.ack() @vendor_request_handler(number=20, direction=USBDirection.IN) @to_device def in_vendor_request(self: USBDevice, request: USBControlRequest): length = int.from_bytes([request.index, request.value], byteorder="big") logging.debug(f"test_control_in sending {length} bytes") request.reply(generate_data(length)) # - device control -------------------------------------------------------- @vendor_request_handler(number=1, direction=USBDirection.OUT) @to_device def set_in_transfer_length(self: USBDevice, request: USBControlRequest): length = int.from_bytes([request.index, request.value], byteorder="big") self.in_transfer_length = length logging.debug(f"set_in_transfer_length: {length} bytes") request.ack() @vendor_request_handler(number=2, direction=USBDirection.IN) @to_device def get_last_out_transfer_data(self: USBDevice, request: USBControlRequest): logging.debug(f"get_last_out_transfer_data: {len(self.last_out_transfer_data)} bytes") request.reply(self.last_out_transfer_data) self.last_out_transfer_data = bytearray() @vendor_request_handler(number=3, direction=USBDirection.OUT) @to_device def reset_device_state(self: USBDevice, request: USBControlRequest): logging.debug(f"reset_device_state: {len(self.last_out_transfer_data)} bytes") self.in_transfer_length = 0 self.last_out_transfer_data = bytearray() request.ack() # - helpers ------------------------------------------------------------------- def generate_data(length): return bytes([(byte % 256) for byte in range(length)]) if __name__ == "__main__": main(StressTestDevice) ================================================ FILE: test/test_alternate.py ================================================ # # This file is part of Facedancer. # import asyncio, logging, random, sys, time import unittest import usb1 from .base import FacedancerTestCase from .base import VENDOR_ID, PRODUCT_ID, MAX_TRANSFER_LENGTH, OUT_ENDPOINT, IN_ENDPOINT, OUT_ALT_ENDPOINT, IN_ALT_ENDPOINT from .device import generate_data class TestAlternate(FacedancerTestCase): """Test alternate interface settings""" def setUp(self): # reset test device state between tests self.reset_device_state() def test_alternate_interfaces(self): endpoints = { 0: (OUT_ENDPOINT, IN_ENDPOINT), 1: (OUT_ALT_ENDPOINT, IN_ALT_ENDPOINT), } for alt in (0, 1): self.set_interface(0, alt) assert(self.get_interface(0) == alt) out_ep, in_ep = endpoints[alt] # generate test data length = 678 data = generate_data(length) # set desired IN transfer length self.set_in_transfer_length(length) # perform Bulk IN transfer received_data = self.bulk_in_transfer(in_ep, length) # generate a set of data to compare against compare_data = generate_data(length) # did we receive the right amount of data? self.assertEqual(len(received_data), length) # does the content of the received data match the content of our comparison data? self.assertEqual(received_data, compare_data) # perform Bulk OUT transfer bytes_sent = self.bulk_out_transfer(out_ep, data) # request a copy of the received data to compare against received_data = self.get_last_out_transfer_data() # did we send the right amount of data? self.assertEqual(bytes_sent, length) # does the length of the sent data match the length of the received data? self.assertEqual(len(data), len(received_data)) # does the content of the sent data match the content of the received data? self.assertEqual(data, received_data) if __name__ == "__main__": unittest.main(verbosity=1) ================================================ FILE: test/test_descriptors.py ================================================ from facedancer import * import unittest # Test case similar to a game pad seen in the wild. device_data = bytes([ 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x09, 0x12, 0x05, 0x00, 0x00, 0x01, 0x01, 0x02, 0x00, 0x01]) strings = { 1: "Facedancer Test", 2: "Gamepad With Audio", 4: "Interface 1", 6: "Interface 3", } config_data = bytes([ 0x09, 0x02, 0xE3, 0x00, 0x04, 0x01, 0x03, 0xC0, 0xFA, 0x09, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x04, 0x0A, 0x24, 0x01, 0x00, 0x01, 0x49, 0x00, 0x02, 0x01, 0x02, 0x0C, 0x24, 0x02, 0x01, 0x01, 0x01, 0x06, 0x04, 0x33, 0x00, 0x00, 0x00, 0x0C, 0x24, 0x06, 0x02, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x24, 0x03, 0x03, 0x01, 0x03, 0x04, 0x02, 0x00, 0x0C, 0x24, 0x02, 0x04, 0x02, 0x04, 0x03, 0x02, 0x03, 0x00, 0x00, 0x00, 0x09, 0x24, 0x06, 0x05, 0x04, 0x01, 0x03, 0x00, 0x00, 0x09, 0x24, 0x03, 0x06, 0x01, 0x01, 0x01, 0x05, 0x00, 0x09, 0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x09, 0x04, 0x01, 0x01, 0x01, 0x01, 0x02, 0x00, 0x00, 0x07, 0x24, 0x01, 0x01, 0x01, 0x01, 0x00, 0x0B, 0x24, 0x02, 0x01, 0x04, 0x02, 0x10, 0x01, 0x80, 0xBB, 0x00, 0x09, 0x05, 0x01, 0x09, 0x88, 0x01, 0x04, 0x00, 0x00, 0x07, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, 0x04, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x09, 0x04, 0x02, 0x01, 0x01, 0x01, 0x02, 0x00, 0x00, 0x07, 0x24, 0x01, 0x06, 0x01, 0x01, 0x00, 0x0B, 0x24, 0x02, 0x01, 0x02, 0x02, 0x10, 0x01, 0x80, 0xBB, 0x00, 0x09, 0x05, 0x82, 0x05, 0xC4, 0x00, 0x04, 0x00, 0x00, 0x07, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, 0x04, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, 0x06, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x11, 0x01, 0x07, 0x05, 0x84, 0x03, 0x40, 0x00, 0x06, 0x07, 0x05, 0x03, 0x03, 0x40, 0x00, 0x06, ]) report_data = bytes([ 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2F, 0x91, 0x02, 0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02, 0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02, 0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02, 0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02, 0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02, 0xC0 ]) class TestDescriptors(unittest.TestCase): def test_device_descriptor_reconstruction(self): device = USBDevice.from_binary_descriptor(device_data) assert(device.get_descriptor() == device_data) def test_config_descriptor_reconstruction(self): configuration = USBConfiguration.from_binary_descriptor(config_data) device = USBDevice() device.add_configuration(configuration) assert(configuration.get_descriptor() == config_data) def test_code_generation(self): # Construct a device from binary descriptors. device = USBDevice.from_binary_descriptor(device_data, strings) configuration = USBConfiguration.from_binary_descriptor(config_data, strings) device.add_configuration(configuration) report_desc = USBDescriptor(type_number=0x22, number=0, raw=report_data) hid_interface = configuration.interfaces[(3, 0)] hid_interface.add_descriptor(report_desc) # Generate code and check it matches expected output. code = device.generate_code() self.maxDiff = None self.assertEqual(expected_code, code) # Run that code and check that it generates the matching descriptors. exec(code, globals()) new_device = Device() self.assertEqual(new_device.get_descriptor(), device_data) new_config = new_device.configurations[1] self.assertEqual(new_config.get_descriptor(), config_data) new_hid_interface = new_config.interfaces[(3, 0)] new_report_desc = new_hid_interface.requestable_descriptors[(0x22, 0)] self.assertEqual(new_report_desc.raw, report_data) # Check that it also produces the same code again. new_code = new_device.generate_code() self.assertEqual(expected_code, new_code) expected_code = """ @use_inner_classes_automatically class Device(USBDevice): device_speed = None device_class = 0 device_subclass = 0 protocol_revision_number = 0 max_packet_size_ep0 = 64 vendor_id = 0x1209 product_id = 0x0005 manufacturer_string = (1, 'Facedancer Test') product_string = (2, 'Gamepad With Audio') serial_number_string = None supported_languages = (LanguageIDs.ENGLISH_US,) device_revision = 0x0100 usb_spec_version = 0x0200 class Configuration_1(USBConfiguration): number = 1 configuration_string = 3 max_power = 500 self_powered = True supports_remote_wakeup = False class Interface_0(USBInterface): number = 0 alternate = 0 class_number = 1 subclass_number = 1 protocol_number = 0 interface_string = (4, 'Interface 1') @include_in_config class Descriptor_0x24_A(USBDescriptor): raw = bytes([ 0x0A, 0x24, 0x01, 0x00, 0x01, 0x49, 0x00, 0x02, 0x01, 0x02]) @include_in_config class Descriptor_0x24_B(USBDescriptor): raw = bytes([ 0x0C, 0x24, 0x02, 0x01, 0x01, 0x01, 0x06, 0x04, 0x33, 0x00, 0x00, 0x00]) @include_in_config class Descriptor_0x24_C(USBDescriptor): raw = bytes([ 0x0C, 0x24, 0x06, 0x02, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00]) @include_in_config class Descriptor_0x24_D(USBDescriptor): raw = bytes([ 0x09, 0x24, 0x03, 0x03, 0x01, 0x03, 0x04, 0x02, 0x00]) @include_in_config class Descriptor_0x24_E(USBDescriptor): raw = bytes([ 0x0C, 0x24, 0x02, 0x04, 0x02, 0x04, 0x03, 0x02, 0x03, 0x00, 0x00, 0x00]) @include_in_config class Descriptor_0x24_F(USBDescriptor): raw = bytes([ 0x09, 0x24, 0x06, 0x05, 0x04, 0x01, 0x03, 0x00, 0x00]) @include_in_config class Descriptor_0x24_G(USBDescriptor): raw = bytes([ 0x09, 0x24, 0x03, 0x06, 0x01, 0x01, 0x01, 0x05, 0x00]) class Interface_1(USBInterface): number = 1 alternate = 0 class_number = 1 subclass_number = 2 protocol_number = 0 interface_string = None class Interface_1_1(USBInterface): number = 1 alternate = 1 class_number = 1 subclass_number = 2 protocol_number = 0 interface_string = None @include_in_config class Descriptor_0x24_A(USBDescriptor): raw = bytes([ 0x07, 0x24, 0x01, 0x01, 0x01, 0x01, 0x00]) @include_in_config class Descriptor_0x24_B(USBDescriptor): raw = bytes([ 0x0B, 0x24, 0x02, 0x01, 0x04, 0x02, 0x10, 0x01, 0x80, 0xBB, 0x00]) class Endpoint_1_OUT(USBEndpoint): number = 1 direction = USBDirection.OUT transfer_type = USBTransferType.ISOCHRONOUS synchronization_type = USBSynchronizationType.ADAPTIVE usage_type = USBUsageType.DATA max_packet_size = 392 interval = 4 extra_bytes = bytes([0x00, 0x00]) @include_in_config class Descriptor_0x25_A(USBDescriptor): raw = bytes([ 0x07, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00]) class Interface_2(USBInterface): number = 2 alternate = 0 class_number = 1 subclass_number = 2 protocol_number = 0 interface_string = None class Interface_2_1(USBInterface): number = 2 alternate = 1 class_number = 1 subclass_number = 2 protocol_number = 0 interface_string = None @include_in_config class Descriptor_0x24_A(USBDescriptor): raw = bytes([ 0x07, 0x24, 0x01, 0x06, 0x01, 0x01, 0x00]) @include_in_config class Descriptor_0x24_B(USBDescriptor): raw = bytes([ 0x0B, 0x24, 0x02, 0x01, 0x02, 0x02, 0x10, 0x01, 0x80, 0xBB, 0x00]) class Endpoint_2_IN(USBEndpoint): number = 2 direction = USBDirection.IN transfer_type = USBTransferType.ISOCHRONOUS synchronization_type = USBSynchronizationType.ASYNC usage_type = USBUsageType.DATA max_packet_size = 196 interval = 4 extra_bytes = bytes([0x00, 0x00]) @include_in_config class Descriptor_0x25_A(USBDescriptor): raw = bytes([ 0x07, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00]) class Interface_3(USBInterface): number = 3 alternate = 0 class_number = 3 subclass_number = 0 protocol_number = 0 interface_string = (6, 'Interface 3') @include_in_config class Descriptor_0x21_A(USBDescriptor): raw = bytes([ 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x11, 0x01]) class Endpoint_4_IN(USBEndpoint): number = 4 direction = USBDirection.IN transfer_type = USBTransferType.INTERRUPT synchronization_type = USBSynchronizationType.NONE usage_type = USBUsageType.DATA max_packet_size = 64 interval = 6 extra_bytes = bytes([]) class Endpoint_3_OUT(USBEndpoint): number = 3 direction = USBDirection.OUT transfer_type = USBTransferType.INTERRUPT synchronization_type = USBSynchronizationType.NONE usage_type = USBUsageType.DATA max_packet_size = 64 interval = 6 extra_bytes = bytes([]) @requestable(type_number=0x22, number=0) class Descriptor_0x22_0(USBDescriptor): raw = bytes([ 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2F, 0x91, 0x02, 0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02, 0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02, 0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02, 0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02, 0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02, 0xC0]) """ if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_stress.py ================================================ # # This file is part of Facedancer. # import asyncio, logging, random, sys, time import unittest import usb1 from .base import FacedancerTestCase from .base import VENDOR_ID, PRODUCT_ID, MAX_TRANSFER_LENGTH, OUT_ENDPOINT, IN_ENDPOINT from .device import generate_data # How many iterations to run for stress test ITERATIONS = 100 # Transfer length for tests def test_transfer_length(): return random.randrange(1, MAX_TRANSFER_LENGTH) class TestStress(FacedancerTestCase): """Stress tests for test device""" # - life-cycle ------------------------------------------------------------ def setUp(self): # select first interface self.set_interface(0, 0) # reset test device state between tests self.reset_device_state() def test_stress_test(self): def bulk_out_transfer(self, length): bytes_sent = self.bulk_out_transfer(OUT_ENDPOINT, generate_data(length)) self.assertEqual(bytes_sent, length) def bulk_in_transfer(self, length): received = self.bulk_in_transfer(IN_ENDPOINT, length) self.assertEqual(len(received), length) def control_out_transfer(self, length): bytes_sent = self.control_out_transfer(generate_data(length)) self.assertEqual(bytes_sent, length) def control_in_transfer(self, length): received = self.control_in_transfer(length) self.assertEqual(len(received), length) available_tests = [ bulk_out_transfer, bulk_in_transfer, control_out_transfer, control_in_transfer, ] tests = [random.choice(available_tests) for _ in range(ITERATIONS)] # pick a random length for transfers transfer_length = test_transfer_length() self.set_in_transfer_length(transfer_length) logging.debug(f"Running stress test with a transfer length of {transfer_length} bytes") failures = 0 for index, test in enumerate(tests): logging.debug(f"#{index}: {test.__name__}") try: test(self, transfer_length) except Exception as e: failures += 1 logging.error(f"Failed #{index}: {test.__name__} {e}") if failures > 0: logging.error(f"Failed {failures} tests.") raise RuntimeError(f"Failed {failures} tests.") def highly_stressed_edition(): from .test_transfers import TestTransfers available_tests = [ "test_bulk_out_transfer", "test_bulk_in_transfer", "test_control_out_transfer", "test_control_in_transfer", ] tests = [random.choice(available_tests) for _ in range(ITERATIONS)] suite = unittest.TestSuite() for test in tests: suite.addTest(TestTransfers(test)) runner = unittest.TextTestRunner() runner.run(suite) if __name__ == "__main__": #highly_stressed_edition() unittest.main(verbosity=1) ================================================ FILE: test/test_transfers.py ================================================ # # This file is part of Facedancer. # import asyncio, logging, random, sys, time import unittest import usb1 from .base import FacedancerTestCase from .base import VENDOR_ID, PRODUCT_ID, MAX_TRANSFER_LENGTH, OUT_ENDPOINT, IN_ENDPOINT from .device import generate_data # Transfer length for tests def test_transfer_length(): return random.randrange(1, MAX_TRANSFER_LENGTH) # Run tests in random order # # Note: if you can't reproduce a failed run check the order of the # tests in the failed run! unittest.TestLoader.sortTestMethodsUsing = lambda self, a, b: random.choice([1, 0, -1]) class TestTransfers(FacedancerTestCase): """Transfer tests for test device""" # - life-cycle ------------------------------------------------------------ def setUp(self): # select first interface self.set_interface(0, 0) # reset test device state between tests self.reset_device_state() # - transfer checks ------------------------------------------------------- def check_out_transfer(self, length, sent_data, bytes_sent): # request a copy of the received data to compare against received_data = self.get_last_out_transfer_data() # did we send the right amount of data? self.assertEqual(bytes_sent, length) # does the length of the sent data match the length of the received data? self.assertEqual(len(sent_data), len(received_data)) # does the content of the sent data match the content of the received data? self.assertEqual(sent_data, received_data) def check_in_transfer(self, length, received_data): # generate a set of data to compare against compare_data = generate_data(length) # did we receive the right amount of data? self.assertEqual(len(received_data), length) # does the content of the received data match the content of our comparison data? self.assertEqual(received_data, compare_data) # - tests ----------------------------------------------------------------- def test_bulk_out_transfer(self): # generate test data length = test_transfer_length() data = generate_data(length) # perform Bulk OUT transfer bytes_sent = self.bulk_out_transfer(OUT_ENDPOINT, data) # check transfer self.check_out_transfer(length, data, bytes_sent) def test_bulk_in_transfer(self): # set desired IN transfer length length = test_transfer_length() self.set_in_transfer_length(length) # perform Bulk IN transfer received_data = self.bulk_in_transfer(IN_ENDPOINT, length) # check transfer self.check_in_transfer(length, received_data) def test_control_out_transfer(self): # generate test data length = test_transfer_length() data = generate_data(length) # perform Control OUT transfer bytes_sent = self.control_out_transfer(data) # check transfer self.check_out_transfer(length, data, bytes_sent) def test_control_in_transfer(self): # set desired IN transfer length length = test_transfer_length() self.set_in_transfer_length(length) # perform Bulk IN transfer received_data = self.control_in_transfer(length) # check transfer self.check_in_transfer(length, received_data) if __name__ == "__main__": unittest.main(verbosity=1)