├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── readme.rst └── usage.rst ├── pyproject.toml ├── pytest.ini ├── requirements_dev.txt ├── samples └── ant_plus_dongle.pcap ├── setup.cfg ├── setup.py ├── test ├── conftest.py ├── plugins │ ├── test_encode_decode.py │ ├── test_hexdump.py │ └── test_reload.py ├── test_all.py ├── test_pcap_raw.py └── test_proxy.py ├── tools └── hook_none_result_tester.py ├── tox.ini └── usbq ├── __init__.py ├── cli.py ├── defs.py ├── dissect ├── __init__.py ├── fields.py ├── hid.py └── usb.py ├── engine.py ├── exceptions.py ├── hookspec.py ├── model ├── __init__.py ├── endpoint.py ├── identity.py └── interface.py ├── opts.py ├── plugin.py ├── plugins ├── decode.py ├── encode.py ├── hexdump.py ├── ipython.py ├── lookfor.py ├── pcap.py ├── proxy.py └── reload.py ├── pm.py ├── speed.py ├── usbmitm_proto.py ├── usbpcap.py ├── usbproxy.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | test_results/ 104 | pip-wheel-metadata/ 105 | *.pcap 106 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/python/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | - repo: https://github.com/asottile/reorder_python_imports 8 | rev: v1.5.0 9 | hooks: 10 | - id: reorder-python-imports 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: master 13 | hooks: 14 | - id: flake8 -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | Created by Benoît Camredon of Airbus Group. 9 | 10 | https://github.com/airbus-seclab/usbq_userland 11 | 12 | Contributors 13 | ------------ 14 | 15 | Brad Dixon created the Python package. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019, Brad Dixon 4 | 5 | MIT licensed with the approval of the original author, Benoît Camredon, 6 | provided by email on January 7th, 2019 to Brad Dixon. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 usbq tests 55 | 56 | autoflake: 57 | autoflake -i -r usbq --remove-all-unused-imports --remove-unused-variables --expand-star-imports 58 | 59 | test: PHONY ## run tests quickly with the default Python 60 | pytest 61 | 62 | test-all: ## run tests on every Python version with tox 63 | tox 64 | 65 | coverage: ## check code coverage quickly with the default Python 66 | coverage run --source usbq -m pytest 67 | coverage report -m 68 | coverage html 69 | $(BROWSER) htmlcov/index.html 70 | 71 | docs: ## generate Sphinx HTML documentation, including API docs 72 | rm -f docs/usbq.rst 73 | rm -f docs/modules.rst 74 | sphinx-apidoc -o docs/ usbq 75 | $(MAKE) -C docs clean 76 | $(MAKE) -C docs html 77 | $(BROWSER) docs/_build/html/index.html 78 | 79 | servedocs: docs ## compile the docs watching for changes 80 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 81 | 82 | release: dist ## package and upload a release 83 | twine upload dist/* 84 | 85 | dist: clean ## builds source and wheel package 86 | python setup.py sdist 87 | python setup.py bdist_wheel 88 | ls -l dist 89 | 90 | install: clean ## install the package to the active Python's site-packages 91 | python setup.py install 92 | 93 | PHONY: 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USBQ 2 | 3 | USBQ is a Python-based programming framework for monitoring and modifying USB communications. 4 | 5 | This work is MIT licensed. 6 | 7 | ## Installation 8 | 9 | ### Userland Installation 10 | 11 | `pip install git+https://github.com/rbdixon/usbq.git#egg=usbq` 12 | 13 | I'm working on the Pypi package. 14 | 15 | ### Kernel Module 16 | 17 | Right now USBQ requires the kernel module from USBiquitous. I've not modified the module at all and would like to replace this module with mainline kernel capabilities and support for other hardware. 18 | 19 | 1. Clone [usbq_core](https://github.com/airbus-seclab/usbq_core) 20 | 2. Modify `driver.c` line 47 with the IP address of the device that will be executing USBQ. 21 | 3. Build the kernel loadable module. The easiest way is to install development tools on your board and then modify the Makefile to be able to find your kernel headers. 22 | 4. Install the kernel loadable module. 23 | 5. Check your `dmesg` output and see if it is working. 24 | 25 | If you have a Beaglebone Black running the `4.4.9-ti-r25` kernel and you want to use a pre-built kernel module configured for IP address `10.0.10.1` you can use the pre-built binary that I've got: [`ubq_core.ko`](https://usbq.org/other/ubq_core.ko). If that pre-built binary breaks you get to keep both pieces. 26 | 27 | ## Usage 28 | 29 | 1. Install the loadable kernel module. 30 | 2. Plug your MITM board into your host computer. 31 | 3. Start USBQ on your MITM host: `usbq mitm`. 32 | 4. Plug your USB device into your MITM board. 33 | 5. Give it a moment and you should see the USB device pop up on your host computer. 34 | 6. Unplug the USB device. 35 | 7. Control-C to terminate USBQ. 36 | 37 | ## Origin 38 | 39 | The tool was created for the [edope.bike](https://edope.bike) project and is an extensive rewrite of the userspace component of the [USBiquitous USB Intrusion Toolkit](https://www.sstic.org/media/SSTIC2016/SSTIC-actes/usb_toolkit/SSTIC2016-Article-usb_toolkit-camredon.pdf). 40 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = usbq 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # usbq documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another 17 | # directory, add these directories to sys.path here. If the directory is 18 | # relative to the documentation root, use os.path.abspath to make it 19 | # absolute, like shown here. 20 | # 21 | import os 22 | import sys 23 | 24 | sys.path.insert(0, os.path.abspath('..')) 25 | 26 | import usbq 27 | 28 | # -- General configuration --------------------------------------------- 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 36 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix(es) of source filenames. 42 | # You can specify multiple suffix as a list of string: 43 | # 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'USBiquitous' 52 | copyright = u"2018, Brad Dixon" 53 | author = u"Brad Dixon" 54 | 55 | # The version info for the project you're documenting, acts as replacement 56 | # for |version| and |release|, also used in various other places throughout 57 | # the built documents. 58 | # 59 | # The short X.Y version. 60 | version = usbq.__version__ 61 | # The full version, including alpha/beta/rc tags. 62 | release = usbq.__version__ 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This patterns also effect to html_static_path and html_extra_path 74 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = False 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'alabaster' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a 91 | # theme further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | 102 | # -- Options for HTMLHelp output --------------------------------------- 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'usbqdoc' 106 | 107 | 108 | # -- Options for LaTeX output ------------------------------------------ 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | # Latex figure (float) alignment 121 | # 122 | # 'figure_align': 'htbp', 123 | } 124 | 125 | # Grouping the document tree into LaTeX files. List of tuples 126 | # (source start file, target name, title, author, documentclass 127 | # [howto, manual, or own class]). 128 | latex_documents = [ 129 | (master_doc, 'usbq.tex', u'USBiquitous Documentation', u'Brad Dixon', 'manual') 130 | ] 131 | 132 | 133 | # -- Options for manual page output ------------------------------------ 134 | 135 | # One entry per manual page. List of tuples 136 | # (source start file, name, description, authors, manual section). 137 | man_pages = [(master_doc, 'usbq', u'USBiquitous Documentation', [author], 1)] 138 | 139 | 140 | # -- Options for Texinfo output ---------------------------------------- 141 | 142 | # Grouping the document tree into Texinfo files. List of tuples 143 | # (source start file, target name, title, author, 144 | # dir menu entry, description, category) 145 | texinfo_documents = [ 146 | ( 147 | master_doc, 148 | 'usbq', 149 | u'USBiquitous Documentation', 150 | author, 151 | 'usbq', 152 | 'One line description of project.', 153 | 'Miscellaneous', 154 | ) 155 | ] 156 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to USBiquitous's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | modules 12 | contributing 13 | authors 14 | history 15 | 16 | Indices and tables 17 | ================== 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install USBiquitous, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install usbq 16 | 17 | This is the preferred method to install USBiquitous, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for USBiquitous can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/rbdixon/usbq 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OL https://github.com/rbdixon/usbq/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/rbdixon/usbq 51 | .. _tarball: https://github.com/rbdixon/usbq/tarball/master 52 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=usbq 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use USBiquitous in a project:: 6 | 7 | import usbq 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | skip-string-normalization = true 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --basetemp=test_results 3 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==18.1 2 | bumpversion==0.5.3 3 | wheel==0.32.1 4 | watchdog==0.9.0 5 | flake8==3.5.0 6 | tox==3.5.2 7 | coverage==4.5.1 8 | Sphinx==1.8.1 9 | twine==1.12.1 10 | 11 | pytest==3.8.2 12 | pytest-runner==4.2 13 | -------------------------------------------------------------------------------- /samples/ant_plus_dongle.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivision-research/usbq/2accfa010b785721c0ba0199dae8e02d982114be/samples/ant_plus_dongle.pcap -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:usbq/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | ignore = E501, W503, W293 20 | 21 | [aliases] 22 | # Define setup.py command aliases here 23 | test = pytest 24 | 25 | [tool:pytest] 26 | collect_ignore = ['setup.py'] 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """The setup script.""" 4 | from setuptools import find_packages 5 | from setuptools import setup 6 | 7 | requirements = [ 8 | 'Click>=6.0', 9 | 'scapy', 10 | 'pluggy', 11 | 'attrs', 12 | 'frozendict', 13 | 'coloredlogs', 14 | 'python-statemachine', 15 | ] 16 | 17 | setup_requirements = ['pytest-runner'] 18 | 19 | test_requirements = ['pytest'] 20 | 21 | setup( 22 | author="Brad Dixon", 23 | author_email='brad.dixon@carvesystems.com', 24 | classifiers=[ 25 | 'Development Status :: 2 - Pre-Alpha', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Natural Language :: English', 29 | 'Programming Language :: Python :: 3.7', 30 | ], 31 | description="New Python programming framework extending Benoît Camredon's USBiquitous USB intrusion toolkit.", 32 | entry_points={ 33 | 'console_scripts': ['usbq=usbq.cli:main'], 34 | # Pluggy plugin 35 | 'usbq': ['usbq_base = usbq.plugin'], 36 | }, 37 | install_requires=requirements, 38 | license="MIT license", 39 | long_description='USBQ -- Python programming framework for monitoring and modifying USB communications.', 40 | include_package_data=True, 41 | keywords='usbq', 42 | name='usbq', 43 | packages=find_packages(), 44 | setup_requires=setup_requirements, 45 | test_suite='tests', 46 | tests_require=test_requirements, 47 | url='https://github.com/rbdixon/usbq', 48 | version='0.1.0', 49 | zip_safe=False, 50 | ) 51 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | from scapy.config import conf 2 | 3 | from usbq.usbpcap import USBPcap 4 | 5 | # Configure scapy to parse USB 6 | conf.l2types.register(220, USBPcap) 7 | -------------------------------------------------------------------------------- /test/plugins/test_encode_decode.py: -------------------------------------------------------------------------------- 1 | from usbq.plugins.decode import USBDecode 2 | from usbq.plugins.encode import USBEncode 3 | 4 | 5 | def test_decode_encode(): 6 | data = b'\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x06\x00\x01\x00\x00@\x00' 7 | decoder = USBDecode() 8 | encoder = USBEncode() 9 | pkt = decoder.usbq_host_decode(data=data) 10 | assert data == encoder.usbq_host_encode(pkt=pkt) 11 | -------------------------------------------------------------------------------- /test/plugins/test_hexdump.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from usbq.plugins.hexdump import Hexdump 4 | from usbq.usbmitm_proto import USBMessageDevice 5 | from usbq.usbmitm_proto import USBMessageHost 6 | 7 | 8 | @pytest.mark.parametrize('cls', [USBMessageDevice, USBMessageHost]) 9 | def test_hexdump(capsys, cls): 10 | pkt = cls() 11 | assert hasattr(pkt, 'content') 12 | Hexdump().usbq_log_pkt(pkt) 13 | captured = capsys.readouterr() 14 | assert len(captured.out) > 0 15 | -------------------------------------------------------------------------------- /test/plugins/test_reload.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from usbq.plugins.reload import ReloadUSBQHooks 6 | from usbq.pm import enable_plugins 7 | from usbq.pm import pm 8 | 9 | VER_ONE = ''' 10 | from usbq.hookspec import hookimpl 11 | 12 | class USBQHooks(): 13 | @hookimpl 14 | def usbq_host_has_packet(self): 15 | return False 16 | ''' 17 | 18 | VER_TWO = ''' 19 | from usbq.hookspec import hookimpl 20 | 21 | class USBQHooks(): 22 | @hookimpl 23 | def usbq_host_has_packet(self): 24 | return True 25 | ''' 26 | 27 | 28 | @pytest.fixture 29 | def hookfile(): 30 | res = Path('usbq_hooks.py') 31 | yield res 32 | res.unlink() 33 | 34 | 35 | def test_reload(hookfile): 36 | hookfile.write_text(VER_ONE) 37 | enable_plugins(pm) 38 | reloader = ReloadUSBQHooks() 39 | 40 | assert not reloader.changed 41 | assert not all(pm.hook.usbq_host_has_packet()) 42 | 43 | hookfile.write_text(VER_TWO) 44 | reloader.usbq_tick() 45 | assert all(pm.hook.usbq_host_has_packet()) 46 | -------------------------------------------------------------------------------- /test/test_all.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import re 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | 8 | def as_module(p): 9 | return re.sub('/', '.', re.sub(r'\.py', '', str(p))) 10 | 11 | 12 | modules = [ 13 | as_module(f) for f in Path('usbq').glob('**/*.py') if '__init__' not in str(f) 14 | ] 15 | 16 | 17 | @pytest.mark.parametrize('mod_name', modules) 18 | def test_all(mod_name): 19 | 'Test that __all__ contains only names that are actually exported.' 20 | 21 | mod = importlib.import_module(mod_name) 22 | 23 | missing = set( 24 | n for n in getattr(mod, '__all__', []) if getattr(mod, n, None) is None 25 | ) 26 | assert ( 27 | len(missing) == 0 28 | ), f'{mod_name}: __all__ contains unresolved names: {", ".join(missing)}' 29 | -------------------------------------------------------------------------------- /test/test_pcap_raw.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from scapy.all import raw 3 | from scapy.all import rdpcap 4 | from scapy.utils import RawPcapWriter 5 | 6 | from usbq.usbmitm_proto import USBMessageHost 7 | from usbq.usbpcap import usbhost_to_usbpcap 8 | from usbq.usbpcap import USBPcap 9 | 10 | 11 | @pytest.fixture 12 | def pcap_writer(tmp_path): 13 | f = tmp_path / 'test.pcap' 14 | return RawPcapWriter(f.open('wb'), linktype=220, sync=True) 15 | 16 | 17 | @pytest.fixture 18 | def pcap_file(pcap_writer): 19 | binary = b'\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x06\x00\x01\x00\x00@\x00' 20 | pkt = USBMessageHost(binary) 21 | msg = pkt.content 22 | pcap_pkt = USBPcap(usbhost_to_usbpcap(msg)) 23 | pcap_writer.write(raw(pcap_pkt)) 24 | 25 | return pcap_writer.filename 26 | 27 | 28 | def test_read_pcap(pcap_file): 29 | for packet in rdpcap(pcap_file): 30 | assert len(packet) > 0 31 | -------------------------------------------------------------------------------- /test/test_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from usbq.plugins.proxy import ProxyPlugin 4 | 5 | DATA = b'1234' 6 | 7 | 8 | @pytest.fixture() 9 | def proxy(): 10 | 'Setup proxy in a loopback configuration.' 11 | 12 | proxy = ProxyPlugin( 13 | device_addr='127.0.0.1', 14 | device_port=55555, 15 | host_addr='127.0.0.1', 16 | host_port=55555, 17 | ) 18 | assert not proxy.usbq_device_has_packet() 19 | assert not proxy.usbq_host_has_packet() 20 | return proxy 21 | 22 | 23 | @pytest.mark.timeout(1) 24 | def test_send_recv(proxy): 25 | # Host send 26 | proxy.usbq_send_host_packet(DATA) 27 | 28 | while not proxy.usbq_device_has_packet(): 29 | pass 30 | 31 | assert proxy.usbq_get_device_packet() == DATA 32 | assert not proxy.usbq_device_has_packet() 33 | 34 | # Host recv 35 | proxy.usbq_send_device_packet(DATA) 36 | 37 | while not proxy.usbq_host_has_packet(): 38 | pass 39 | 40 | assert proxy.usbq_get_host_packet() == DATA 41 | assert not proxy.usbq_host_has_packet() 42 | 43 | 44 | @pytest.mark.timeout(1) 45 | def test_no_wait(proxy): 46 | assert not proxy.usbq_device_has_packet() 47 | assert not proxy.usbq_host_has_packet() 48 | -------------------------------------------------------------------------------- /tools/hook_none_result_tester.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Test tool for checking plugin caller's resistance to None results. 3 | 4 | 1. Copy to usbq_hooks.py. 5 | 2. Test each hook by doing whatever calls that hook. Usually a device unplug/plug will work. 6 | 3. Increment to the next hook. 7 | 4. Go to #2 8 | 9 | ''' 10 | import logging 11 | 12 | from usbq.hookspec import hookimpl 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | HOOKS = [ 17 | 'usbq_tick', 18 | 'usbq_wait_for_packet', 19 | 'usbq_log_pkt', 20 | 'usbq_device_has_packet', 21 | 'usbq_get_device_packet', 22 | 'usbq_device_decode', 23 | 'usbq_device_modify', 24 | 'usbq_device_encode', 25 | 'usbq_host_has_packet', 26 | 'usbq_get_host_packet', 27 | 'usbq_host_decode', 28 | 'usbq_host_encode', 29 | 'usbq_host_modify', 30 | 'usbq_send_device_packet', 31 | 'usbq_send_host_packet', 32 | 'usbq_device_identity', 33 | 'usbq_handle_device_request', 34 | 'usbq_ipython_ns', 35 | 'usbq_connected', 36 | 'usbq_disconnected', 37 | 'usbq_teardown', 38 | ] 39 | 40 | 41 | class USBQHooks: 42 | def _boom(self): 43 | raise Exception() 44 | 45 | 46 | hookname = HOOKS[0] 47 | log.critical(f'Testing {hookname}') 48 | setattr(USBQHooks, hookname, hookimpl(USBQHooks._boom)) 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, flake8 3 | 4 | [travis] 5 | python = 6 | 3.6: py36 7 | 3.5: py35 8 | 3.4: py34 9 | 2.7: py27 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 usbq 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | deps = 20 | -r{toxinidir}/requirements_dev.txt 21 | ; If you want to make tox run the tests with the same versions, create a 22 | ; requirements.txt with the pinned versions and uncomment the following line: 23 | ; -r{toxinidir}/requirements.txt 24 | commands = 25 | pip install -U pip 26 | py.test --basetemp={envtmpdir} 27 | 28 | 29 | -------------------------------------------------------------------------------- /usbq/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Top-level package for USBiquitous.""" 3 | 4 | __author__ = """Brad Dixon""" 5 | __email__ = 'brad.dixon@carvesystems.com' 6 | __version__ = '0.1.0' 7 | -------------------------------------------------------------------------------- /usbq/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import sys 4 | 5 | import click 6 | import click_config_file 7 | from coloredlogs import ColoredFormatter 8 | 9 | from . import __version__ 10 | from .engine import USBQEngine 11 | from .opts import add_options 12 | from .opts import network_options 13 | from .opts import pcap_options 14 | from .opts import standard_plugin_options 15 | from .opts import usb_device_options 16 | from .pm import AVAILABLE_PLUGINS 17 | from .pm import enable_plugins 18 | from .pm import enable_tracing 19 | from .pm import pm 20 | 21 | __all__ = [] 22 | log = logging.getLogger(__name__) 23 | 24 | CMD_NAME = 'usbq' 25 | CONFIG_FILE = CMD_NAME + '.cfg' 26 | FORMAT = '%(levelname)8s [%(name)24s]: %(message)s' 27 | LOG_FIELD_STYLES = { 28 | 'asctime': {'color': 'green'}, 29 | 'hostname': {'color': 'magenta'}, 30 | 'levelname': {'color': 'green', 'bold': True}, 31 | 'name': {'color': 'blue'}, 32 | 'programname': {'color': 'cyan'}, 33 | } 34 | 35 | 36 | def _setup_logging(logfile, debug): 37 | if debug: 38 | level = logging.DEBUG 39 | else: 40 | level = logging.INFO 41 | 42 | # Turn on logging 43 | root = logging.getLogger() 44 | root.setLevel(level) 45 | 46 | # shush little ones 47 | for mod in ['scapy.loading', 'scapy.runtime']: 48 | logging.getLogger(mod).setLevel(logging.CRITICAL) 49 | 50 | # Colors and formats 51 | ch = logging.StreamHandler(sys.stdout) 52 | ch.setLevel(level) 53 | fh = logging.FileHandler(logfile, 'w') 54 | fh.setLevel(level) 55 | formatter = ColoredFormatter(fmt=FORMAT, field_styles=LOG_FIELD_STYLES) 56 | ch.setFormatter(formatter) 57 | fh.setFormatter(logging.Formatter(FORMAT)) 58 | root.addHandler(ch) 59 | root.addHandler(fh) 60 | 61 | 62 | @click.group(invoke_without_command=True) 63 | @click.option('--debug', is_flag=True, default=False, help='Enable usbq debug logging.') 64 | @click.option( 65 | '--logfile', 66 | type=click.Path(writable=True, dir_okay=False), 67 | default='debug.log', 68 | help='Logfile for --debug output', 69 | ) 70 | @click.option('--trace', is_flag=True, default=False, help='Trace plugins.') 71 | @click.option( 72 | '--dump', is_flag=True, default=False, help='Dump USBQ packets to console.' 73 | ) 74 | @click.option( 75 | '--disable-plugin', type=str, multiple=True, default=[], help='Disable plugin' 76 | ) 77 | @click.option( 78 | '--enable-plugin', type=str, multiple=True, default=[], help='Enable plugin' 79 | ) 80 | @click.pass_context 81 | @click_config_file.configuration_option(cmd_name='usbq', config_file_name='usbq.cfg') 82 | def main(ctx, debug, trace, logfile, **kwargs): 83 | '''USBQ: Python programming framework for monitoring and modifying USB communications.''' 84 | 85 | ctx.ensure_object(dict) 86 | ctx.obj['dump'] = ctx.params['dump'] 87 | ctx.obj['enable_plugin'] = ctx.params['enable_plugin'] 88 | ctx.obj['disable_plugin'] = ctx.params['disable_plugin'] 89 | 90 | if ctx.invoked_subcommand is None: 91 | click.echo(f'usbq version {__version__}\n') 92 | click.echo(ctx.get_help()) 93 | click.echo('\nAvailable plugins:\n') 94 | for pd in sorted(AVAILABLE_PLUGINS.values(), key=lambda pd: pd.name): 95 | click.echo(f'- {pd.name}: {pd.desc}') 96 | click.echo( 97 | f'\nDefault config file: {click.get_app_dir(CMD_NAME)}/{CONFIG_FILE}' 98 | ) 99 | else: 100 | _setup_logging(logfile, debug) 101 | 102 | if trace: 103 | enable_tracing() 104 | 105 | return 0 106 | 107 | 108 | # 109 | # Commands 110 | # 111 | 112 | 113 | @main.command() 114 | @click.pass_context 115 | @add_options(network_options) 116 | @add_options(pcap_options) 117 | @add_options(usb_device_options) 118 | def mitm(ctx, proxy_addr, proxy_port, listen_addr, listen_port, pcap, usb_id): 119 | 'Man-in-the-Middle USB device to host communications.' 120 | 121 | enable_plugins( 122 | pm, 123 | standard_plugin_options( 124 | proxy_addr, proxy_port, listen_addr, listen_port, pcap, dump=ctx.obj['dump'] 125 | ) 126 | + [('lookfor', {'usb_id': usb_id})], 127 | disabled=ctx.obj['disable_plugin'], 128 | enabled=ctx.obj['enable_plugin'], 129 | ) 130 | USBQEngine().run() 131 | 132 | 133 | if __name__ == "__main__": 134 | sys.exit(main()) # pragma: no cover 135 | -------------------------------------------------------------------------------- /usbq/defs.py: -------------------------------------------------------------------------------- 1 | from frozendict import frozendict 2 | 3 | __all__ = ['USBDefs', 'URBDefs'] 4 | 5 | 6 | class AutoDescEnum: 7 | def __init_subclass__(cls, *args, **kwargs): 8 | super().__init_subclass__(*args, **kwargs) 9 | 10 | # Build a dict with descriptions for each field value. 11 | # This is used for scapy packet display. 12 | cls.desc = frozendict( 13 | { 14 | getattr(cls, key): f'{key} (0x{getattr(cls, key):02x})' 15 | for key in dir(cls) 16 | if not key.startswith('_') 17 | } 18 | ) 19 | 20 | assert cls.__doc__ is not None, 'Add documentation to enumeration' 21 | 22 | # Update documentation with values 23 | cls.__doc__ += '\n\n' 24 | for key, value in sorted(cls.desc.items(), key=lambda kv: kv[0]): 25 | cls.__doc__ += f'{key}: {value}\n' 26 | 27 | def __class_getitem__(cls, key): 28 | return cls.desc.get(key, f'[UNKNOWN] (0x{key:02x})') 29 | 30 | 31 | class USBDefs: 32 | 'USB field values' 33 | # https://www.kernel.org/doc/html/latest/driver-api/usb/usb.html#usb-standard-types 34 | 35 | class EP: 36 | 'Endpoint field values' 37 | 38 | class TransferType(AutoDescEnum): 39 | 'USB endpoint transfer types' 40 | CTRL = 0 41 | ISOC = 1 42 | BULK = 2 43 | INT = 3 44 | 45 | class Direction(AutoDescEnum): 46 | 'Endpoint Direction' 47 | OUT = 0 48 | IN = 1 49 | 50 | class Speed(AutoDescEnum): 51 | 'USB Speed' 52 | LOW_SPEED = 1 53 | FULL_SPEED = 2 54 | HIGH_SPEED = 3 55 | 56 | class DescriptorType(AutoDescEnum): 57 | 'Descriptor Type' 58 | DEVICE_DESCRIPTOR = 1 59 | CONFIGURATION_DESCRIPTOR = 2 60 | STRING_DESCRIPTOR = 3 61 | INTERFACE_DESCRIPTOR = 4 62 | ENDPOINT_DESCRIPTOR = 5 63 | BOS_DESCRIPTOR = 0xF 64 | HID_DESCRIPTOR = 0x21 65 | HID_REPORT_DESCRIPTOR = 0x22 66 | 67 | class DeviceClass(AutoDescEnum): 68 | 'Device class' 69 | HID = 3 70 | MASS_STORAGE = 8 71 | 72 | 73 | class URBDefs: 74 | ''' 75 | USB Request Block field values 76 | 77 | See https://www.kernel.org/doc/html/v4.15/driver-api/usb/URB.html 78 | ''' 79 | 80 | class Direction(AutoDescEnum): 81 | 'Direction of request' 82 | HOST_TO_DEVICE = 0 83 | DEVICE_TO_HOST = 1 84 | 85 | class Type(AutoDescEnum): 86 | 'Type of request' 87 | STANDARD = 0 88 | 89 | class Recipient(AutoDescEnum): 90 | 'Recipient of request' 91 | DEVICE = 0 92 | 93 | class Request(AutoDescEnum): 94 | 'Request type' 95 | GET_REPORT = 1 96 | GET_DESCRIPTOR = 6 97 | SET_CONFIGURATION = 9 98 | SET_IDLE = 0xA 99 | SET_INTERFACE = 0xB 100 | 101 | class Language(AutoDescEnum): 102 | 'Language of string descriptor request' 103 | NONE_SPECIFIED = 0 104 | 105 | DescriptorType = USBDefs.DescriptorType 106 | -------------------------------------------------------------------------------- /usbq/dissect/__init__.py: -------------------------------------------------------------------------------- 1 | # Thx git 2 | -------------------------------------------------------------------------------- /usbq/dissect/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from scapy.fields import conf 3 | from scapy.fields import EnumField 4 | from scapy.fields import LEShortEnumField 5 | from scapy.fields import lhex 6 | from scapy.fields import StrField 7 | from scapy.fields import StrFixedLenField 8 | from scapy.fields import StrLenField 9 | from scapy.fields import VolatileValue 10 | 11 | __all__ = [ 12 | 'XLEShortEnumField', 13 | 'BytesFixedLenField', 14 | 'UnicodeStringLenField', 15 | 'LESignedIntEnumField', 16 | 'TypePacketField', 17 | ] 18 | 19 | 20 | class XLEShortEnumField(LEShortEnumField): 21 | def i2repr_one(self, pkt, x): 22 | if ( 23 | self not in conf.noenum 24 | and not isinstance(x, VolatileValue) 25 | and x in self.i2s 26 | ): 27 | return self.i2s[x] 28 | return lhex(x) 29 | 30 | 31 | class BytesFixedLenField(StrFixedLenField): 32 | def i2repr(self, pkt, v): 33 | return repr(v) 34 | 35 | 36 | class UnicodeStringLenField(StrLenField): 37 | def i2repr(self, pkt, v): 38 | v = v.replace(b"\x00", b"") 39 | return repr(v) 40 | 41 | 42 | class LESignedIntEnumField(EnumField): 43 | def __init__(self, name, default, enum): 44 | EnumField.__init__(self, name, default, enum, " len(string_desc): 122 | res = string_desc[0] 123 | else: 124 | res = string_desc[request.descriptor_index] 125 | else: 126 | # Conversion to raw is used to trim descriptor if required by host 127 | if ( 128 | request.bDescriptorType 129 | == USBDefs.DescriptorType.CONFIGURATION_DESCRIPTOR 130 | ): 131 | req_len = request.wLength 132 | res = Descriptor(raw(self[request.bDescriptorType][0])[:req_len]) 133 | else: 134 | req_len = request.wLength 135 | res = Descriptor(raw(self[request.bDescriptorType][0])[:req_len]) 136 | return res 137 | except Exception: 138 | return 139 | 140 | # Device Descriptor access 141 | @property 142 | def device(self): 143 | return self[USBDefs.DescriptorType.DEVICE_DESCRIPTOR][0] 144 | 145 | @device.setter 146 | def device(self, desc): 147 | self.descriptors[USBDefs.DescriptorType.DEVICE_DESCRIPTOR] = [desc] 148 | 149 | # Configuration descriptor access 150 | @property 151 | def configuration(self): 152 | return self[USBDefs.DescriptorType.CONFIGURATION_DESCRIPTOR][0] 153 | 154 | @configuration.setter 155 | def configuration(self, desc): 156 | self.descriptors[USBDefs.DescriptorType.CONFIGURATION_DESCRIPTOR] = [desc] 157 | 158 | @property 159 | def interfaces(self): 160 | return InterfaceList(self.configuration.descriptors) 161 | 162 | @property 163 | def endpoints(self): 164 | return EndpointList(self.configuration.descriptors) 165 | 166 | @property 167 | def strings(self): 168 | return StringList(self[USBDefs.DescriptorType.STRING_DESCRIPTOR]) 169 | 170 | def set_strings(self, strings): 171 | for s in strings: 172 | self.descriptors[USBDefs.DescriptorType.STRING_DESCRIPTOR].append( 173 | StringDescriptor(bString=s) 174 | ) 175 | 176 | def to_new_identity(self): 177 | return ManagementNewDevice( 178 | speed=self.speed, device=self.device, configuration=self.configuration 179 | ) 180 | -------------------------------------------------------------------------------- /usbq/model/interface.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from ..dissect.usb import InterfaceDescriptor 4 | from .endpoint import Endpoint 5 | 6 | __all__ = ['Interface'] 7 | 8 | 9 | @attr.s 10 | class Interface: 11 | _descriptors = attr.ib(default=[]) 12 | cls = attr.ib(converter=int, default=0) 13 | subcls = attr.ib(converter=int, default=0) 14 | proto = attr.ib(converter=int, default=0) 15 | 16 | @property 17 | def descriptors(self): 18 | desc = [ 19 | InterfaceDescriptor( 20 | bInterfaceClass=self.cls, 21 | bInterfaceSubClass=self.subcls, 22 | bInterfaceProtocol=self.proto, 23 | ) 24 | ] 25 | nbep = 0 26 | for d in self._descriptors: 27 | if type(d) is Endpoint: 28 | desc.append(d.descriptor) 29 | nbep += 1 30 | else: 31 | desc.append(d) 32 | desc[0].bNumEndpoint = nbep 33 | return desc 34 | -------------------------------------------------------------------------------- /usbq/opts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pickle 3 | 4 | import click 5 | 6 | # Shared options 7 | 8 | __all__ = [ 9 | 'network_options', 10 | 'pcap_options', 11 | 'identity_options', 12 | 'add_options', 13 | 'standard_plugin_options', 14 | 'load_ident', 15 | 'usb_device_options', 16 | ] 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | network_options = [ 21 | click.option( 22 | '--proxy-addr', 23 | default='127.0.0.1', 24 | type=str, 25 | help='IP address of the USB MITM proxy hardware.', 26 | envvar='USBQ_PROXY_ADDR', 27 | ), 28 | click.option( 29 | '--proxy-port', 30 | default=64241, 31 | type=int, 32 | help='Port number of the USB MITM proxy hardware.', 33 | envvar='USBQ_PROXY_PORT', 34 | ), 35 | click.option( 36 | '--listen-addr', 37 | default='0.0.0.0', 38 | type=str, 39 | help='IP address to bind to for incoming packets from the USB MITM proxy hardware.', 40 | envvar='USBQ_LISTEN_ADDR', 41 | ), 42 | click.option( 43 | '--listen-port', 44 | default=64240, 45 | type=int, 46 | help='Port to bind to for incoming packets from the USB MITM proxy hardware.', 47 | envvar='USBQ_LISTEN_PORT', 48 | ), 49 | ] 50 | 51 | pcap_options = [ 52 | click.option( 53 | '--pcap', 54 | default='usb.pcap', 55 | type=click.Path(dir_okay=False, writable=True, exists=False), 56 | help='PCAP file to record USB traffic.', 57 | ) 58 | ] 59 | 60 | identity_options = [ 61 | click.option( 62 | '--device-identity', 63 | default=None, 64 | type=click.File('rb'), 65 | help='File to load pickled instance of a USB device.', 66 | ) 67 | ] 68 | 69 | usb_device_options = [ 70 | click.option( 71 | '--usb-id', 72 | default=None, 73 | type=str, 74 | help='USB product and vendor ID (fmt: xxxx:xxxx)', 75 | ) 76 | ] 77 | 78 | 79 | def load_ident(fn): 80 | if fn is not None: 81 | d = pickle.load(fn) 82 | log.debug(f'Loaded device ID: {d}') 83 | return d 84 | else: 85 | return None 86 | 87 | 88 | def add_options(options): 89 | def _add_options(func): 90 | for option in reversed(options): 91 | func = option(func) 92 | return func 93 | 94 | return _add_options 95 | 96 | 97 | def standard_plugin_options( 98 | proxy_addr, proxy_port, listen_addr, listen_port, pcap, dump=False, **kwargs 99 | ): 100 | res = [ 101 | ( 102 | 'proxy', 103 | { 104 | 'device_addr': listen_addr, 105 | 'device_port': listen_port, 106 | 'host_addr': proxy_addr, 107 | 'host_port': proxy_port, 108 | }, 109 | ), 110 | ('pcap', {'pcap': pcap}), 111 | ('decode', {}), 112 | ('encode', {}), 113 | ] 114 | 115 | if dump: 116 | res.append(('hexdump', {})) 117 | 118 | return res 119 | -------------------------------------------------------------------------------- /usbq/plugin.py: -------------------------------------------------------------------------------- 1 | 'Default plugin implementations' 2 | from .hookspec import hookimpl 3 | from .hookspec import USBQPluginDef 4 | 5 | 6 | @hookimpl 7 | def usbq_declare_plugins(): 8 | # These are the bundled plugins. 9 | return { 10 | 'proxy': USBQPluginDef( 11 | name='proxy', 12 | desc='Send and receive USB packets from a USBQ proxy device using the usbq_core module.', 13 | mod='usbq.plugins.proxy', 14 | clsname='ProxyPlugin', 15 | ), 16 | 'pcap': USBQPluginDef( 17 | name='pcap', 18 | desc='Write a PCAP file containing USB communications.', 19 | mod='usbq.plugins.pcap', 20 | clsname='PcapFileWriter', 21 | ), 22 | 'decode': USBQPluginDef( 23 | name='decode', 24 | desc='Decode raw USBQ driver packets to Scapy representation.', 25 | mod='usbq.plugins.decode', 26 | clsname='USBDecode', 27 | ), 28 | 'encode': USBQPluginDef( 29 | name='encode', 30 | desc='Encode raw USBQ driver packets to Scapy representation.', 31 | mod='usbq.plugins.encode', 32 | clsname='USBEncode', 33 | ), 34 | 'hexdump': USBQPluginDef( 35 | name='hexdump', 36 | desc='Display USBQ packet and hexdump of USB payload.', 37 | mod='usbq.plugins.hexdump', 38 | clsname='Hexdump', 39 | ), 40 | 'reload': USBQPluginDef( 41 | name='reload', 42 | desc='Monitor usbq_hooks.py file and reload if changed.', 43 | mod='usbq.plugins.reload', 44 | clsname='ReloadUSBQHooks', 45 | ), 46 | 'ipython': USBQPluginDef( 47 | name='ipython', 48 | desc='Start an IPython session so that USBQ can be updated on the fly.', 49 | mod='usbq.plugins.ipython', 50 | clsname='IPythonUI', 51 | ), 52 | 'lookfor': USBQPluginDef( 53 | name='lookfor', 54 | desc='look for a specific USB device to appear', 55 | mod='usbq.plugins.lookfor', 56 | clsname='LookForDevice', 57 | ), 58 | } 59 | -------------------------------------------------------------------------------- /usbq/plugins/decode.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | 5 | from ..hookspec import hookimpl 6 | from ..usbmitm_proto import USBMessageDevice 7 | from ..usbmitm_proto import USBMessageHost 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | @attr.s(cmp=False) 13 | class USBDecode: 14 | 'Decode raw USB packets into USBQ packets.' 15 | 16 | @hookimpl 17 | def usbq_host_decode(self, data): 18 | return USBMessageHost(data) 19 | 20 | @hookimpl 21 | def usbq_device_decode(self, data): 22 | return USBMessageDevice(data) 23 | -------------------------------------------------------------------------------- /usbq/plugins/encode.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | from scapy.all import raw 5 | 6 | from ..hookspec import hookimpl 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | @attr.s(cmp=False) 12 | class USBEncode: 13 | 'Encode host and device packets to USBQ packets.' 14 | 15 | @hookimpl 16 | def usbq_host_encode(self, pkt): 17 | return raw(pkt) 18 | 19 | @hookimpl 20 | def usbq_device_encode(self, pkt): 21 | return raw(pkt) 22 | -------------------------------------------------------------------------------- /usbq/plugins/hexdump.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | from scapy.all import hexdump 5 | 6 | from ..hookspec import hookimpl 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | @attr.s(cmp=False) 12 | class Hexdump: 13 | 'Print packets as a hexdump to the console.' 14 | 15 | @hookimpl 16 | def usbq_log_pkt(self, pkt): 17 | # Dump to console 18 | log.info(repr(pkt)) 19 | 20 | if hasattr(pkt, 'content'): 21 | hexdump(pkt.content) 22 | print() 23 | -------------------------------------------------------------------------------- /usbq/plugins/ipython.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | import IPython 5 | 6 | from ..hookspec import hookimpl 7 | from ..pm import pm 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | @attr.s(cmp=False) 13 | class IPythonUI: 14 | 'IPython UI for usbq' 15 | 16 | ns = {} 17 | 18 | @hookimpl 19 | def usbq_ipython_ns(self): 20 | res = {'pm': pm} 21 | return res 22 | 23 | def run(self, engine): 24 | self._engine = engine 25 | # Short enough to be responsive but not so short as to ramp up CPU usage 26 | proxy = pm.get_plugin('proxy') 27 | proxy.timeout = 0.01 28 | 29 | self.ns.update( 30 | {key: value for d in pm.hook.usbq_ipython_ns() for key, value in d.items()} 31 | ) 32 | 33 | IPython.terminal.pt_inputhooks.register('usbq', self._ipython_loop) 34 | IPython.start_ipython(argv=['-i', '-c', '%gui usbq'], user_ns=self.ns) 35 | 36 | def _ipython_loop(self, context): 37 | while not context.input_is_ready(): 38 | self._engine.event() 39 | 40 | def _load_ipy_ns(self): 41 | res = {'pm': pm} 42 | res.update({name: plugin for name, plugin in pm.list_name_plugin()}) 43 | return res 44 | -------------------------------------------------------------------------------- /usbq/plugins/lookfor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | import usb 5 | from statemachine import State 6 | from statemachine import StateMachine 7 | 8 | from ..hookspec import hookimpl 9 | from ..pm import pm 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | @attr.s 15 | class USBId: 16 | vendor = attr.ib(converter=int) 17 | product = attr.ib(converter=int) 18 | 19 | def __str__(self): 20 | return f'{self.vendor:04x}:{self.product:04x}' 21 | 22 | @staticmethod 23 | def parse(usb_id): 24 | if usb_id is None: 25 | return 26 | 27 | raw_vid, raw_pid = usb_id.split(':') 28 | return USBId(vendor=int(raw_vid, 16), product=int(raw_pid, 16)) 29 | 30 | 31 | @attr.s(cmp=False) 32 | class LookForDevice(StateMachine): 33 | usb_id = attr.ib(converter=USBId.parse, default=None) 34 | 35 | # States 36 | idle = State('idle', initial=True) 37 | present = State('present') 38 | not_present = State('not_present') 39 | 40 | # Valid state transitions 41 | connected = idle.to(present) | not_present.to(present) 42 | disconnected = idle.to(not_present) | present.to(not_present) 43 | 44 | def __attrs_post_init__(self): 45 | # Workaround to mesh attr and StateMachine 46 | super().__init__() 47 | 48 | if self.usb_id is not None: 49 | log.info(f'Searching for USB device {self.usb_id}') 50 | 51 | def _look(self): 52 | dev = usb.core.find(idVendor=self.usb_id.vendor, idProduct=self.usb_id.product) 53 | if dev is not None and not self.is_present: 54 | self.connected() 55 | elif dev is None and self.is_present: 56 | self.disconnected() 57 | 58 | def on_connected(self): 59 | log.info(f'USB device {self.usb_id} connected to host') 60 | pm.hook.usbq_connected() 61 | 62 | def on_disconnected(self): 63 | log.info(f'USB device {self.usb_id} disconnected from host') 64 | pm.hook.usbq_disconnected() 65 | 66 | @hookimpl 67 | def usbq_tick(self): 68 | if self.usb_id is not None: 69 | self._look() 70 | -------------------------------------------------------------------------------- /usbq/plugins/pcap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Union 3 | 4 | import attr 5 | from scapy.all import raw 6 | from scapy.utils import RawPcapWriter 7 | 8 | from ..defs import USBDefs 9 | from ..hookspec import hookimpl 10 | from ..usbmitm_proto import USBMessageDevice 11 | from ..usbmitm_proto import USBMessageHost 12 | from ..usbpcap import ack_from_msg 13 | from ..usbpcap import req_from_msg 14 | from ..usbpcap import usbdev_to_usbpcap 15 | from ..usbpcap import usbhost_to_usbpcap 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | @attr.s(cmp=False) 21 | class PcapFileWriter: 22 | 'Write a PCAP file containing all proxied USB traffic.' 23 | 24 | #: Filename for the PCAP file. 25 | pcap = attr.ib(converter=str) 26 | 27 | def __attrs_post_init__(self): 28 | log.info(f'Logging packets to PCAP file {self.pcap}') 29 | self._pcap = RawPcapWriter(self.pcap, linktype=220, sync=True) 30 | 31 | def _do_host(self, msg): 32 | # Convert and write 33 | pcap_pkt = usbhost_to_usbpcap(msg) 34 | self._pcap.write(raw(pcap_pkt)) 35 | 36 | # We do not receive ACK from device for OUT data 37 | if msg.ep.epdir == msg.URBEPDirection.URB_OUT: 38 | ack = ack_from_msg(msg) 39 | self._pcap.write(raw(ack)) 40 | 41 | def _do_device(self, msg): 42 | # We do not receive REQUEST from host if type is not CTRL 43 | if msg.ep.eptype != USBDefs.EP.TransferType.CTRL: 44 | req = req_from_msg(msg) 45 | self._pcap.write(raw(req)) 46 | 47 | # Convert and write 48 | pcap_pkt = usbdev_to_usbpcap(msg) 49 | self._pcap.write(raw(pcap_pkt)) 50 | 51 | @hookimpl 52 | def usbq_log_pkt(self, pkt: Union[USBMessageDevice, USBMessageHost]): 53 | # Only log USB Host or Device type packets to the pcap file 54 | if type(pkt) in [USBMessageDevice, USBMessageHost]: 55 | if pkt.type != pkt.MitmType.USB: 56 | return 57 | 58 | msg = pkt.content 59 | if type(pkt) == USBMessageDevice: 60 | self._do_device(msg) 61 | else: 62 | self._do_host(msg) 63 | -------------------------------------------------------------------------------- /usbq/plugins/proxy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import select 3 | import socket 4 | 5 | import attr 6 | from attr.converters import optional 7 | from statemachine import State 8 | from statemachine import StateMachine 9 | 10 | from ..hookspec import hookimpl 11 | from ..pm import pm 12 | from ..usbmitm_proto import ManagementMessage 13 | from ..usbmitm_proto import ManagementReload 14 | from ..usbmitm_proto import ManagementReset 15 | from ..usbmitm_proto import USBMessageDevice 16 | from ..usbmitm_proto import USBMessageHost 17 | 18 | log = logging.getLogger(__name__) 19 | TIMEOUT = ([], [], []) 20 | 21 | 22 | @attr.s(cmp=False) 23 | class ProxyPlugin(StateMachine): 24 | 'Proxy USB communications using a ubq_core enabled hardware device.' 25 | 26 | #: Address to listen to for USB device. 27 | _device_addr = attr.ib(converter=optional(str), default=None) 28 | 29 | #: Port to listen to for USB device. 30 | _device_port = attr.ib(converter=optional(int), default=None) 31 | 32 | #: Address to send to for USB host. 33 | _host_addr = attr.ib(converter=optional(str), default=None) 34 | 35 | #: Port to send to for USB host. 36 | _host_port = attr.ib(converter=optional(int), default=None) 37 | 38 | #: Timeout for select statement that waits for incoming USBQ packets 39 | timeout = attr.ib(converter=int, default=1) 40 | 41 | # States 42 | idle = State('idle', initial=True) 43 | running = State('running') 44 | 45 | # Valid state transitions 46 | start = idle.to(running) 47 | reset = running.to(idle) | idle.to(idle) 48 | reload = idle.to(running) 49 | 50 | EMPTY = [] 51 | MANAGEMENT_MSG = { 52 | ManagementMessage.ManagementType.NEW_DEVICE: 'New device connected to USBQ proxy', 53 | ManagementMessage.ManagementType.RESET: 'Device reset sent from USBQ proxy', 54 | } 55 | 56 | def __attrs_post_init__(self): 57 | # Workaround to mesh attr and StateMachine 58 | super().__init__() 59 | self._socks = [] 60 | self._proxy_host = True 61 | self._proxy_device = True 62 | self._device_dst = None 63 | self._detected_host = False 64 | self._detected_device = False 65 | 66 | if self._device_addr is None or self._device_port is None: 67 | self._proxy_device = False 68 | 69 | if self._host_addr is None or self._host_port is None: 70 | self._proxy_host = False 71 | 72 | if self._proxy_device: 73 | log.info(f'Device listen to {self._device_addr}:{self._device_port}') 74 | self._device_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 75 | self._device_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 76 | self._device_sock.setblocking(False) 77 | self._device_sock.bind((self._device_addr, self._device_port)) 78 | self._socks.append(self._device_sock) 79 | 80 | if self._proxy_host: 81 | log.info(f'Host send to {self._host_addr}:{self._host_port}') 82 | self._host_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 83 | self._host_sock.setblocking(False) 84 | self._host_dst = (self._host_addr, self._host_port) 85 | self._socks.append(self._host_sock) 86 | 87 | def _has_data(self, socks, timeout=0): 88 | (read, write, error) = select.select(socks, self.EMPTY, socks, timeout) 89 | if len(read) != 0: 90 | return True 91 | return False 92 | 93 | @hookimpl 94 | def usbq_host_has_packet(self): 95 | if self._proxy_host: 96 | if self._has_data([self._host_sock]): 97 | return True 98 | 99 | @hookimpl 100 | def usbq_device_has_packet(self): 101 | if self._proxy_device: 102 | if self._has_data([self._device_sock]): 103 | return True 104 | 105 | @hookimpl 106 | def usbq_wait_for_packet(self): 107 | # Poll for data from non-proxy source 108 | queued_data = [] 109 | if not self._proxy_host: 110 | queued_data += pm.hook.usbq_host_has_packet() 111 | if not self._proxy_device: 112 | queued_data += pm.hook.usbq_device_has_packet() 113 | 114 | if any(queued_data): 115 | return True 116 | else: 117 | # Wait 118 | if self._has_data(self._socks, timeout=self.timeout): 119 | return True 120 | 121 | @hookimpl 122 | def usbq_get_host_packet(self): 123 | data, self._host_dst = self._host_sock.recvfrom(4096) 124 | 125 | if not self._detected_host: 126 | log.info('First USBQ host packet detected from proxy') 127 | self._detected_host = True 128 | 129 | return data 130 | 131 | @hookimpl 132 | def usbq_get_device_packet(self): 133 | data, self._device_dst = self._device_sock.recvfrom(4096) 134 | 135 | if not self._detected_device: 136 | log.info('First USBQ device packet detected from proxy') 137 | self._detected_device = True 138 | 139 | return data 140 | 141 | @hookimpl 142 | def usbq_send_host_packet(self, data): 143 | return self._host_sock.sendto(data, self._host_dst) > 0 144 | 145 | @hookimpl 146 | def usbq_send_device_packet(self, data): 147 | if self._device_dst is not None: 148 | return self._device_sock.sendto(data, self._device_dst) > 0 149 | 150 | @hookimpl 151 | def usbq_log_pkt(self, pkt): 152 | if ManagementMessage in pkt: 153 | msg = self.MANAGEMENT_MSG.get(pkt.content.management_type, None) 154 | if msg is not None: 155 | log.info(msg) 156 | 157 | if pkt.content.management_type == ManagementMessage.ManagementType.RESET: 158 | self._detected_device = False 159 | 160 | def on_start(self): 161 | log.info('Starting proxy.') 162 | 163 | def _send_host_mgmt(self, pkt): 164 | data = pm.hook.usbq_host_encode( 165 | pkt=USBMessageDevice(type=USBMessageHost.MitmType.MANAGEMENT, content=pkt) 166 | ) 167 | self.usbq_send_host_packet(data) 168 | 169 | def _send_device_mgmt(self, pkt): 170 | data = pm.hook.usbq_device_encode( 171 | pkt=USBMessageHost(type=USBMessageDevice.MitmType.MANAGEMENT, content=pkt) 172 | ) 173 | self.usbq_send_device_packet(data) 174 | 175 | def on_reset(self): 176 | log.info('Reset device.') 177 | 178 | self._send_device_mgmt( 179 | ManagementMessage( 180 | management_type=ManagementMessage.ManagementType.RESET, 181 | management_content=ManagementReset(), 182 | ) 183 | ) 184 | 185 | def on_reload(self): 186 | log.info('Reload device.') 187 | 188 | self._send_device_mgmt( 189 | ManagementMessage( 190 | management_type=ManagementMessage.ManagementType.RELOAD, 191 | management_content=ManagementReload(), 192 | ) 193 | ) 194 | -------------------------------------------------------------------------------- /usbq/plugins/reload.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import logging 4 | import traceback 5 | from pathlib import Path 6 | 7 | import attr 8 | 9 | from ..hookspec import hookimpl 10 | from ..pm import HOOK_CLSNAME 11 | from ..pm import HOOK_MOD 12 | from ..pm import pm 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @attr.s(cmp=False) 18 | class ReloadUSBQHooks: 19 | 'Reload usbq_hooks.py when it changes' 20 | 21 | _hookfile = attr.ib(default='usbq_hooks.py') 22 | 23 | def __attrs_post_init__(self): 24 | self._mtime = None 25 | self._path = Path(self._hookfile) 26 | if self._path.is_file(): 27 | log.info(f'Monitoring {self._path} for changes.') 28 | self._mtime = self.mtime 29 | 30 | @property 31 | def mtime(self): 32 | if self._path.is_file(): 33 | return self._path.stat().st_mtime 34 | 35 | @property 36 | def changed(self): 37 | if self._mtime != self.mtime: 38 | self._mtime = self.mtime 39 | log.debug(f'Monitored hook file {self._path} was modified.') 40 | return True 41 | else: 42 | return False 43 | 44 | def _catch(self, outcome): 45 | try: 46 | outcome.get_result() 47 | except Exception: 48 | frm = inspect.trace()[-1] 49 | mod = inspect.getmodule(frm[0]) 50 | if mod.__name__ == HOOK_MOD: 51 | log.critical(f'Error executing hook in {HOOK_MOD}. Disabling plugin.') 52 | traceback.print_tb(outcome.excinfo[2]) 53 | pm.unregister(name=HOOK_MOD) 54 | outcome.force_result(None) 55 | 56 | @hookimpl(hookwrapper=True) 57 | def usbq_tick(self): 58 | if self.changed: 59 | # Reload 60 | try: 61 | mod = importlib.import_module(HOOK_MOD) 62 | importlib.reload(mod) 63 | except Exception: 64 | log.critical('Could not reload usbq_hooks.py.') 65 | yield 66 | return 67 | 68 | # Unregister 69 | pm.unregister(name=HOOK_MOD) 70 | 71 | # Register 72 | cls = getattr(mod, HOOK_CLSNAME) 73 | pm.register(cls(), name=HOOK_MOD) 74 | log.info('Reloaded usbq_hooks.py.') 75 | 76 | outcome = yield 77 | self._catch(outcome) 78 | 79 | def _wrapper(self, *args, **kwargs): 80 | outcome = yield 81 | self._catch(outcome) 82 | 83 | 84 | # Create usbq_hook error handlers for all defined hooks 85 | for hookname in [ 86 | 'usbq_wait_for_packet', 87 | 'usbq_log_pkt', 88 | 'usbq_device_has_packet', 89 | 'usbq_get_device_packet', 90 | 'usbq_device_decode', 91 | 'usbq_device_modify', 92 | 'usbq_device_encode', 93 | 'usbq_host_has_packet', 94 | 'usbq_get_host_packet', 95 | 'usbq_host_decode', 96 | 'usbq_host_encode', 97 | 'usbq_host_modify', 98 | 'usbq_send_device_packet', 99 | 'usbq_send_host_packet', 100 | 'usbq_device_identity', 101 | 'usbq_handle_device_request', 102 | 'usbq_ipython_ns', 103 | 'usbq_connected', 104 | 'usbq_disconnected', 105 | 'usbq_teardown', 106 | ]: 107 | setattr( 108 | ReloadUSBQHooks, hookname, hookimpl(ReloadUSBQHooks._wrapper, hookwrapper=True) 109 | ) 110 | -------------------------------------------------------------------------------- /usbq/pm.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | import os.path 4 | import sys 5 | from collections import ChainMap 6 | from collections import OrderedDict 7 | 8 | import pluggy 9 | 10 | from .exceptions import USBQInvocationError 11 | from .hookspec import USBQ_EP 12 | from .hookspec import USBQHookSpec 13 | from .hookspec import USBQPluginDef 14 | 15 | __all__ = ['AVAILABLE_PLUGINS', 'enable_plugins', 'enable_tracing'] 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | # Search current directory. Needed for usbq_hooks.py 20 | sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # Load the plugin manager and list available plugins 23 | pm = pluggy.PluginManager(USBQ_EP) 24 | pm.add_hookspecs(USBQHookSpec) 25 | pm.load_setuptools_entrypoints(USBQ_EP) 26 | 27 | AVAILABLE_PLUGINS = OrderedDict(ChainMap({}, *pm.hook.usbq_declare_plugins())) 28 | 29 | # Add optional 30 | HOOK_MOD = 'usbq_hooks' 31 | HOOK_CLSNAME = 'USBQHooks' 32 | AVAILABLE_PLUGINS['usbq_hooks'] = USBQPluginDef( 33 | name='usbq_hooks', 34 | desc='Optional user-provided hook implementations automatically loaded from from ./usbq_hooks.py', 35 | mod=HOOK_MOD, 36 | clsname=HOOK_CLSNAME, 37 | optional=True, 38 | ) 39 | 40 | 41 | def enable_plugins(pm, pmlist=[], disabled=[], enabled=[]): 42 | extra = [(pdname, {}) for pdname in enabled] 43 | pdlist = [('reload', {})] + pmlist + extra + [('usbq_hooks', {})] 44 | pdnames = [pdinfo[0] for pdinfo in pdlist] 45 | log.info(f'Loading plugins: {", ".join(pdnames)}') 46 | 47 | for pdinfo in pdlist: 48 | pdname, pdopts = pdinfo 49 | 50 | if pdname not in AVAILABLE_PLUGINS: 51 | msg = f'{pdname} is not a valid USBQ plugin.' 52 | log.critical(msg) 53 | raise USBQInvocationError(msg) 54 | 55 | if pdname in disabled: 56 | log.info(f'Disabling plugin {pdname}.') 57 | continue 58 | 59 | pd = AVAILABLE_PLUGINS[pdname] 60 | 61 | try: 62 | mod = importlib.import_module(pd.mod) 63 | cls = getattr(mod, pd.clsname) 64 | 65 | try: 66 | pm.register(cls(**pdopts), name=pdname) 67 | except Exception as e: 68 | log.critical( 69 | f'Could not start plugin {pdname} ({cls.__name__}) with options {pdopts}' 70 | ) 71 | raise e 72 | 73 | log.debug( 74 | f'Loaded {pd.name} plugin from {pd.mod}:{pd.clsname} with kwargs {pdopts}' 75 | ) 76 | except ModuleNotFoundError: 77 | if pd.optional: 78 | log.info( 79 | f'Could not load optional plugin {pd.name}: Module not available.' 80 | ) 81 | else: 82 | raise 83 | except AttributeError: 84 | if pd.optional: 85 | log.info( 86 | f'Could not load optional plugin {pd.name}: Could not instantiate {pd.clsname}.' 87 | ) 88 | else: 89 | raise 90 | except Exception as e: 91 | if pd.mod == 'usbq_hooks': 92 | log.critical(f'Could not load usbq_hooks.py: {e}') 93 | else: 94 | raise 95 | 96 | 97 | def enable_tracing(): 98 | # Trace pluggy 99 | tracer = logging.getLogger('trace') 100 | before_msg = None 101 | 102 | def before(hook_name, hook_impls, kwargs): 103 | nonlocal before_msg 104 | 105 | arglst = [ 106 | f'{key}={repr(value)}' 107 | for key, value in sorted(kwargs.items(), key=lambda v: v[0]) 108 | ] 109 | argstr = ', '.join(arglst) 110 | plst = ', '.join([p.plugin_name for p in reversed(hook_impls)]) 111 | before_msg = f'{hook_name}({argstr}) [{plst}]' 112 | 113 | def after(outcome, hook_name, hook_impls, kwargs): 114 | nonlocal before_msg 115 | 116 | res = outcome.get_result() 117 | has_result = [ 118 | type(res) == list and len(res) > 0, 119 | type(res) != list and res is not None, 120 | hook_name 121 | in [ 122 | 'usbq_device_modify', 123 | 'usbq_host_modify', 124 | 'usbq_connected', 125 | 'usbq_disconnected', 126 | 'usbq_teardown', 127 | ], 128 | ] 129 | if any(has_result): 130 | tracer.debug(f'{before_msg} -> {repr(res)} [{type(res)}]') 131 | 132 | pm.add_hookcall_monitoring(before, after) 133 | -------------------------------------------------------------------------------- /usbq/speed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | 4 | 5 | def ls2hs_interval(interval): 6 | """ Fix interval of EndpointDescriptors 7 | Board is acting as a High speed device, so bInterval is interpreted 8 | as a polling rate equal to (bInterval-1) units with units equat to 125µs. 9 | Value is then changed to match behavior of a low speed device 10 | """ 11 | return math.log(interval * 8, 2) + 1 12 | -------------------------------------------------------------------------------- /usbq/usbmitm_proto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from scapy.fields import ConditionalField 4 | from scapy.fields import EnumField 5 | from scapy.fields import LEIntField 6 | from scapy.fields import LEShortField 7 | from scapy.fields import LESignedIntField 8 | from scapy.fields import PacketField 9 | from scapy.fields import StrField 10 | from scapy.fields import struct 11 | from scapy.packet import Packet 12 | 13 | from .defs import AutoDescEnum 14 | from .defs import USBDefs 15 | from .dissect.fields import TypePacketField 16 | from .dissect.usb import ConfigurationDescriptor 17 | from .dissect.usb import Descriptor 18 | from .dissect.usb import DeviceDescriptor 19 | from .dissect.usb import GetDescriptor 20 | from .dissect.usb import URB 21 | 22 | __all__ = [ 23 | 'USBMessageHost', 24 | 'USBMessageDevice', 25 | 'ManagementMessage', 26 | 'ManagementReset', 27 | 'ManagementNewDevice', 28 | 'ManagementReload', 29 | 'USBMessageRequest', 30 | 'USBMessageResponse', 31 | 'USBAck', 32 | ] 33 | 34 | 35 | class USBMitm(Packet): 36 | def desc(self): 37 | return '%r' % (self,) 38 | 39 | class MitmType(AutoDescEnum): 40 | 'USBQ Protocol Packet Type' 41 | 42 | # ubq_core/msg.h 43 | USB = 0 44 | ACK = 1 45 | MANAGEMENT = 2 46 | 47 | class ManagementType(AutoDescEnum): 48 | 'USBQ Management Packet Type' 49 | 50 | # ubq_core/msg.h 51 | RESET = 0 52 | NEW_DEVICE = 1 53 | RELOAD = 2 54 | 55 | class USBSpeed(AutoDescEnum): 56 | 'USBQ Device Speed' 57 | 58 | # kernel linux/usb/ch9.h 59 | LOW_SPEED = 1 60 | FULL_SPEED = 2 61 | HIGH_SPEED = 3 62 | 63 | class URBEPDirection(AutoDescEnum): 64 | ''' 65 | URB EP direction 66 | 67 | From the Linux kernel's perspective the direction of the 68 | endpoint. 69 | ''' 70 | 71 | # ubq_core/types.h 72 | URB_IN = 0 73 | URB_OUT = 1 74 | 75 | 76 | class USBEp(USBMitm): 77 | fields_desc = [ 78 | LEShortField('epnum', 0), 79 | EnumField( 80 | 'eptype', USBDefs.EP.TransferType.CTRL, USBDefs.EP.TransferType.desc, ' 0: 125 | s.append('+data (len:%u)' % (len(self.data))) 126 | return ' '.join(s) 127 | 128 | 129 | class USBMessageResponse(USBMitm): 130 | fields_desc = [ 131 | PacketField('ep', USBEp(), USBEp), 132 | ConditionalField( 133 | PacketField('request', GetDescriptor(), URB), lambda p: p.ep.is_ctrl_0() 134 | ), 135 | ConditionalField( 136 | PacketField('response', DeviceDescriptor(), Descriptor), 137 | lambda p: p.ep.is_ctrl_0() and type(p.request) is GetDescriptor, 138 | ), 139 | StrField('data', ''), 140 | ] 141 | 142 | def get_usb_payload(self): 143 | if self.ep.is_ctrl_0() and type(self.request) is GetDescriptor: 144 | return self.response 145 | return self.data 146 | 147 | def desc(self): 148 | s = [] 149 | if self.ep.is_ctrl_0() and type(self.request) is GetDescriptor: 150 | return self.response.desc() 151 | if len(self.data) > 0: 152 | s.append('+data (len:%u)' % (len(self.data))) 153 | return ' '.join(s) 154 | 155 | 156 | class ManagementNewDevice(USBMitm): 157 | fields_desc = [ 158 | EnumField('speed', USBMitm.USBSpeed.HIGH_SPEED, USBMitm.USBSpeed.desc, 'host.' 241 | 242 | name = 'USBMessageDevice' 243 | fields_desc = [ 244 | LEIntField('len', None), 245 | EnumField('type', USBMitm.MitmType.USB, USBMitm.MitmType.desc, 'device.' 260 | 261 | name = 'USBMessageHost' 262 | fields_desc = [ 263 | LEIntField('len', None), 264 | EnumField( 265 | 'type', USBMitm.ManagementType.RESET, USBMitm.ManagementType.desc, ' 0, 180 | ), 181 | StrField('data', ''), 182 | ] 183 | 184 | def is_ctrl_request(self): 185 | return self.urb_type == SUBMIT and self.urb_transfert == PCAP_CTRL 186 | 187 | def is_ctrl_response(self): 188 | return self.urb_type == COMPLETE and self.urb_transfert == PCAP_CTRL 189 | -------------------------------------------------------------------------------- /usbq/usbproxy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import select 3 | import socket 4 | 5 | import attr 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | TIMEOUT = ([], [], []) 10 | 11 | 12 | @attr.s 13 | class USBProxy: 14 | 'Proxy for a remote USB Host or Device accessible over UDP' 15 | 16 | #: Human-facing device name 17 | name = attr.ib(converter=str) 18 | 19 | #: Host IP address to connect to or bind to. 20 | host = attr.ib(converter=str) 21 | 22 | #: Port to connect to or bind to. 23 | port = attr.ib(converter=int) 24 | 25 | #: Set to True if the proxied USB termination is a USB host. False indicates a USB device. 26 | device = attr.ib(converter=bool) 27 | 28 | def __attrs_post_init__(self): 29 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 30 | self.dst = None 31 | 32 | log.info( 33 | f'USB proxy setup for {self._devtype} {self.name} ({self.host}:{self.port})' 34 | ) 35 | 36 | if self.device: 37 | self._setup_device() 38 | else: 39 | self._setup_host() 40 | 41 | def _setup_host(self): 42 | self.dst = (self.host, self.port) 43 | 44 | def _setup_device(self): 45 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 46 | self.sock.bind((self.host, self.port)) 47 | 48 | @property 49 | def _devtype(self): 50 | return 'device' if self.device else 'host' 51 | 52 | def data_ready(self, timeout=10, wait_on_error=False): 53 | ''' 54 | Return True if data is available. 55 | 56 | :param timeout: Timeout, in seconds, to wait for data. 57 | :param wait_on_error: If select() returns an error then continue to wait for data. Otherwise return False on error. 58 | ''' 59 | 60 | while True: 61 | log.debug( 62 | f'Waiting for USB {self._devtype} {self.name} ({self.host}:{self.port})' 63 | ) 64 | (read, write, error) = select.select([self.sock], [], [self.sock], timeout) 65 | if error: 66 | log.error( 67 | f'Error waiting for USB {self._devtype} {self.name} ({self.host}:{self.port}): {error}' 68 | ) 69 | if wait_on_error: 70 | continue 71 | else: 72 | return False 73 | elif read: 74 | return True 75 | elif (read, write, error) == TIMEOUT and timeout > 0: 76 | log.info( 77 | f'Timeout waiting for USB {self._devtype} {self.name} ({self.host}:{self.port}): {error}' 78 | ) 79 | return False 80 | 81 | def read(self): 82 | 'Read a raw USB packet from the remote termination.' 83 | 84 | data, self.dst = self.sock.recvfrom(4096) 85 | return data 86 | 87 | def write(self, data): 88 | 'Write a raw USB packet to the remote termination.' 89 | 90 | return self.sock.sendto(data, self.dst) 91 | -------------------------------------------------------------------------------- /usbq/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __all__ = ['InsensitiveDict', 'Color', 'colorize'] 4 | 5 | 6 | class InsensitiveDict(dict): 7 | def __setitem__(self, key, value): 8 | super(InsensitiveDict, self).__setitem__(key.lower(), value) 9 | 10 | def __getitem__(self, key): 11 | return super(InsensitiveDict, self).__getitem__(key.lower()) 12 | 13 | def __contains__(self, key): 14 | return super(InsensitiveDict, self).__contains__(key.lower()) 15 | 16 | 17 | class Color: 18 | normal = 7 19 | black = 0 20 | red = 160 21 | green = 28 22 | yellow = 220 23 | blue = 12 24 | purple = 126 25 | cyan = 45 26 | grey = 239 27 | 28 | normal = 0 29 | bold = 1 30 | 31 | 32 | def colorize(s, color): 33 | if type(color) is tuple: 34 | color, modif = color 35 | return "\033[%dm\x1b[38;5;%dm%s\x1b[0m\033[0m" % (modif, color, s) 36 | else: 37 | return "\x1b[38;5;%dm%s\x1b[0m" % (color, s) 38 | --------------------------------------------------------------------------------