├── .github ├── actions │ └── install-patcherex2 │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── docker.yml │ ├── docs.yml │ └── pypi.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── .pages ├── _css │ └── extra.css ├── advanced_usages │ ├── add_new_target_support.md │ └── advanced_usage.md ├── api_references │ ├── patcherex.md │ └── patches.md ├── core_ideas │ └── patches.md ├── examples │ ├── insert_instruction_patch.md │ ├── modify_function_patch.md │ └── multiple_patches.md └── index.md ├── examples ├── insert_instruction_patch │ ├── add │ ├── add.c │ └── patch.py ├── insert_instruction_patch_c │ ├── add │ ├── add.c │ └── patch.py ├── modify_function_patch │ ├── add.c │ └── patch.py └── multiple_patches │ ├── getline │ ├── getline.c │ └── patch.py ├── mkdocs.yml ├── patcherex2.png ├── pyproject.toml ├── src └── patcherex2 │ ├── __init__.py │ ├── components │ ├── allocation_managers │ │ ├── __init__.py │ │ └── allocation_manager.py │ ├── archinfo │ │ ├── __init__.py │ │ ├── aarch64.py │ │ ├── amd64.py │ │ ├── arm.py │ │ ├── mips.py │ │ ├── mips64.py │ │ ├── ppc.py │ │ ├── ppc64.py │ │ ├── ppc_vle.py │ │ ├── s390x.py │ │ ├── sparc.py │ │ └── x86.py │ ├── assemblers │ │ ├── __init__.py │ │ ├── assembler.py │ │ ├── bcc.py │ │ ├── keystone.py │ │ ├── keystone_arm.py │ │ ├── keystone_sparc.py │ │ └── ppc_vle.py │ ├── assets │ │ ├── .gitignore │ │ └── assets.py │ ├── binary_analyzers │ │ ├── __init__.py │ │ ├── angr.py │ │ ├── binary_analyzer.py │ │ ├── ghidra.py │ │ └── ida.py │ ├── binfmt_tools │ │ ├── __init__.py │ │ ├── binary.py │ │ ├── binfmt_tool.py │ │ ├── elf.py │ │ └── ihex.py │ ├── compilers │ │ ├── __init__.py │ │ ├── bcc.py │ │ ├── clang.py │ │ ├── clang_arm.py │ │ ├── compiler.py │ │ ├── llvm_recomp.py │ │ ├── llvm_recomp_arm.py │ │ └── ppc_vle.py │ ├── disassemblers │ │ ├── __init__.py │ │ ├── capstone.py │ │ ├── capstone_arm.py │ │ ├── disassembler.py │ │ └── ppc_vle.py │ ├── patch_managers │ │ ├── __init__.py │ │ ├── builtin.py │ │ ├── imp.py │ │ └── patch_manager.py │ └── utils │ │ ├── __init__.py │ │ └── utils.py │ ├── patcherex.py │ ├── patches │ ├── __init__.py │ ├── data_patches.py │ ├── dummy_patches.py │ ├── function_patches.py │ ├── instruction_patches.py │ ├── patch.py │ └── raw_patches.py │ └── targets │ ├── __init__.py │ ├── bin_arm_bare.py │ ├── elf_aarch64_linux.py │ ├── elf_amd64_linux.py │ ├── elf_amd64_linux_recomp.py │ ├── elf_arm_bare.py │ ├── elf_arm_linux.py │ ├── elf_arm_linux_recomp.py │ ├── elf_arm_mimxrt1052.py │ ├── elf_leon3_bare.py │ ├── elf_mips64_linux.py │ ├── elf_mips64el_linux.py │ ├── elf_mips_linux.py │ ├── elf_mipsel_linux.py │ ├── elf_ppc64_linux.py │ ├── elf_ppc64le_linux.py │ ├── elf_ppc_linux.py │ ├── elf_s390x_linux.py │ ├── elf_x86_linux.py │ ├── ihex_ppc_bare.py │ └── target.py └── tests ├── test_aarch64.py ├── test_arm.py ├── test_binaries ├── aarch64 │ ├── iip_c │ ├── iip_c.c │ ├── iip_c_asm_header │ ├── iip_c_asm_header.c │ ├── iip_c_float │ ├── iip_c_float.c │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── amd64 │ ├── iip_c │ ├── iip_c.c │ ├── iip_c_asm_header │ ├── iip_c_asm_header.c │ ├── iip_c_float │ ├── iip_c_float.c │ ├── issue8 │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── armhf │ ├── iip_c │ ├── iip_c.c │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── mips │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── mips64 │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── mips64el │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── mipsel │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── ppc │ ├── printf_nopie │ └── replace_function_patch ├── ppc64 │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── ppc64le │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── s390x │ ├── printf.c │ ├── printf_nopie │ ├── printf_pie │ ├── replace_function_patch │ └── replace_function_patch.c └── x86 │ ├── printf_nopie │ ├── printf_pie │ └── replace_function_patch ├── test_i386.py ├── test_mips.py ├── test_mips64.py ├── test_mips64el.py ├── test_mipsel.py ├── test_ppc.py ├── test_ppc64.py ├── test_ppc64le.py ├── test_s390x.py └── test_x86_64.py /.github/actions/install-patcherex2/action.yml: -------------------------------------------------------------------------------- 1 | name: "Install Patcherex2" 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Set up Python 3 6 | uses: actions/setup-python@v5 7 | with: 8 | python-version: "3.10" 9 | - name: Install dependencies 10 | shell: bash 11 | run: | 12 | sudo apt-get update 13 | sudo apt-get install -y \ 14 | clang-15 lld-15 \ 15 | qemu-user \ 16 | gcc-multilib \ 17 | libc6-dev-armhf-cross libc6-dev-arm64-cross \ 18 | libc6-dev-mips-cross libc6-dev-mips64-cross \ 19 | libc6-dev-powerpc-cross libc6-dev-ppc64-cross \ 20 | libc6-dev-mipsel-cross libc6-dev-mips64el-cross \ 21 | libc6-dev-ppc64el-cross libc6-s390x-cross \ 22 | - name: Install clang-19 lld-19 23 | shell: bash 24 | run: | 25 | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc 26 | echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-19 main" | sudo tee /etc/apt/sources.list.d/llvm.list 27 | sudo apt-get update 28 | sudo apt-get install -y clang-19 lld-19 29 | - name: Install Ghidra 30 | shell: bash 31 | run: | 32 | cd $HOME 33 | wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0.3_build/ghidra_11.0.3_PUBLIC_20240410.zip 34 | unzip ghidra_11.0.3_PUBLIC_20240410.zip 35 | echo GHIDRA_INSTALL_DIR=$PWD/ghidra_11.0.3_PUBLIC >> $GITHUB_ENV 36 | - name: Install Patcherex2 37 | shell: bash 38 | run: | 39 | python3 -m pip install --upgrade pip 40 | python3 -m pip install -e .[all] 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '0 14 * * 1' # Runs at 2 PM UTC every Monday 8 | 9 | jobs: 10 | ci: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | task: [test, lint, format, private-test] 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Install Patcherex2 20 | uses: ./.github/actions/install-patcherex2 21 | - name: Install Packages 22 | run: python3 -m pip install --upgrade pytest ruff 23 | - name: Run pytest 24 | if: matrix.task == 'test' 25 | run: python3 -m pytest 26 | - name: Run formatter 27 | if: matrix.task == 'format' 28 | run: python3 -m ruff format . --check 29 | - name: Run linter 30 | if: matrix.task == 'lint' 31 | run: python3 -m ruff check . 32 | - name: Checkout Private Tests 33 | if: matrix.task == 'private-test' && github.event_name != 'pull_request' 34 | uses: actions/checkout@v4 35 | with: 36 | repository: purseclab/patcherex2-private 37 | ssh-key: ${{ secrets.PRIVATE_TESTS_DEPLOY_KEY }} 38 | path: private 39 | - name: Run pytest for Private Tests 40 | if: matrix.task == 'private-test' && github.event_name != 'pull_request' 41 | working-directory: ./private 42 | run: python3 -m pytest -q --no-summary 43 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ created ] 4 | 5 | env: 6 | REGISTRY: ghcr.io 7 | IMAGE_NAME: ${{ github.repository }} 8 | 9 | jobs: 10 | Publish: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Log in to GitHub Container Registry 22 | uses: docker/login-action@v3 23 | with: 24 | registry: ${{ env.REGISTRY }} 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Extract metadata 29 | id: meta 30 | uses: docker/metadata-action@v5 31 | with: 32 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 33 | 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@v5 36 | with: 37 | context: . 38 | push: true 39 | tags: ${{ steps.meta.outputs.tags }} 40 | labels: ${{ steps.meta.outputs.labels }} 41 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ created ] 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | jobs: 12 | deploy: 13 | environment: 14 | name: github-pages 15 | url: ${{ steps.deployment.outputs.page_url }} 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Install Patcherex2 21 | uses: ./.github/actions/install-patcherex2 22 | - name: Build Docs 23 | run: | 24 | python3 -m pip install mkdocs mkdocstrings[python] mkdocs-material mkdocs-awesome-pages-plugin 25 | python3 -m mkdocs build -d mkdocs_build -s 26 | - name: Setup Pages 27 | uses: actions/configure-pages@v4 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@v3 30 | with: 31 | path: mkdocs_build 32 | - name: Deploy to GitHub Pages 33 | id: deployment 34 | uses: actions/deploy-pages@v4 35 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ created ] 4 | 5 | jobs: 6 | Publish: 7 | name: Publish to PyPI 8 | runs-on: ubuntu-latest 9 | permissions: 10 | id-token: write 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v4 14 | - name: Set up Python 3 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.10" 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python -m 27 | build 28 | --sdist 29 | --wheel 30 | --outdir dist/ 31 | . 32 | - name: Publish to PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ruff_cache/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | git wget unzip \ 7 | virtualenvwrapper python3-dev python3-pip python-is-python3 python3-venv \ 8 | openjdk-17-jdk \ 9 | clang-15 lld-15 \ 10 | qemu-user \ 11 | gcc-multilib \ 12 | libc6-armhf-cross libc6-arm64-cross \ 13 | libc6-mips-cross libc6-mips64-cross \ 14 | libc6-powerpc-cross libc6-powerpc-ppc64-cross \ 15 | libc6-mipsel-cross libc6-mips64el-cross \ 16 | libc6-ppc64el-cross libc6-s390x-cross \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | RUN wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc \ 20 | && echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-19 main" | tee /etc/apt/sources.list.d/llvm.list \ 21 | && apt-get update && apt-get install -y clang-19 lld-19 \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | RUN wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0.3_build/ghidra_11.0.3_PUBLIC_20240410.zip \ 25 | && unzip /ghidra_11.0.3_PUBLIC_20240410.zip 26 | 27 | ENV GHIDRA_INSTALL_DIR=/ghidra_11.0.3_PUBLIC 28 | 29 | COPY . /patcherex2 30 | 31 | RUN pip install -U pip pytest ruff 32 | RUN pip install -e /patcherex2[all] 33 | 34 | CMD ["/bin/bash"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, PurSec Lab 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Patcherex2 3 | 4 | Latest Release 5 | 6 | 7 | PyPI Statistics 8 | 9 | 10 | CI 11 | 12 | 13 | License 14 | 15 | 16 |

17 | 18 | Patcherex2 is a rewritten adaptation of the original [Patcherex](https://github.com/angr/patcherex) project, aimed at building upon its core ideas and extending its capabilities. 19 | 20 | ## Installation 21 | 22 | Patcherex2 is available on PyPI and can be installed using pip. Alternatively, you can use the provided Docker image. 23 | 24 | ### pip 25 | ```bash 26 | pip install patcherex2 27 | ``` 28 |
29 | Install from latest commit 30 | 31 | ```bash 32 | pip install git+https://github.com/purseclab/Patcherex2.git 33 | ``` 34 |
35 | 36 | ### Docker 37 | ```bash 38 | docker run --rm -it -v ${PWD}:/workdir -w /workdir ghcr.io/purseclab/patcherex2 39 | ``` 40 | 41 |
42 | Build from latest commit 43 | 44 | ```bash 45 | docker build -t patcherex2 --platform linux/amd64 https://github.com/purseclab/Patcherex2.git 46 | docker run --rm -it -v ${PWD}:/workdir -w /workdir patcherex2 47 | ``` 48 |
49 | 50 | 51 | ## Usage 52 | You can find usage examples [here](https://purseclab.github.io/Patcherex2/examples/insert_instruction_patch/). 53 | 54 | 55 | ## Documentation 56 | General documentation and API reference for Patcherex2 can be found at [purseclab.github.io/Patcherex2](https://purseclab.github.io/Patcherex2/). 57 | 58 | 59 | ## Supported Targets 60 | 61 | | | Linux x86 | Linux amd64 | Linux arm | Linux aarch64 | Linux PowerPC (32bit) | Linux PowerPC (64bit) | Linux PowerPCle (64bit) | Linux MIPS (32bit) | Linux MIPS (64bit) | Linux MIPSEL
​(32bit) | Linux MIPSEL
(64bit) | Linux s390x | SPARCv8 (LEON3) | PowerPC (VLE) (IHEX) 62 | |-|-|-|-|-|-|-|-|-|-|-|-|-|-|-| 63 | InsertDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 64 | RemoveDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 65 | ModifyDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 66 | InsertInstructionPatch (ASM) | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 67 | InsertInstructionPatch (C) | 🟥 | 🟩 | 🟥 | 🟩 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 🟥 | 68 | RemoveInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 69 | ModifyInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | ⬜ | ⬜ | 70 | InsertFunctionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟨 | ⬜ | ⬜ | 71 | ModifyFunctionPatch | 🟨 | 🟩 | 🟩 | 🟩 | 🟨 | 🟨 | 🟨 | 🟨 | 🟨 | 🟨 | 🟨 | 🟨 | ⬜ | ⬜ | 72 | 73 | 🟩 Fully Functional, 🟨 Limited Functionality, 🟥 Not Working, ⬜ Not Tested, 🟪 Work in Progress 74 | 75 | 76 | ## Acknowledgements 77 | This project was developed as part of the [DARPA AMP](https://www.darpa.mil/program/assured-micropatching) program, under contract N6600120C4031. 78 | 79 | -------------------------------------------------------------------------------- /docs/.pages: -------------------------------------------------------------------------------- 1 | nav: 2 | - Home: index.md 3 | - Examples: examples 4 | - Core Ideas: core_ideas 5 | - Advanced Usages: advanced_usages 6 | - API References: api_references 7 | - ... 8 | 9 | -------------------------------------------------------------------------------- /docs/_css/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="pursec"] { 2 | --md-primary-fg-color: #4f6da9; 3 | --md-accent-fg-color: #bd9020; 4 | } 5 | -------------------------------------------------------------------------------- /docs/advanced_usages/add_new_target_support.md: -------------------------------------------------------------------------------- 1 | # Adding New Target Support 2 | 3 | Patcherex2 has been designed with extensibility in mind, making it easy to add support for new targets. This document will walk you through the process of defining a new target in Patcherex2. 4 | 5 | ## Defining a New Target 6 | 7 | The first step is to define a new target class that inherits from the `Target` base class. This class should specify the required components to support the target. Here's an example of the existing `elf_amd64_linux` target definition. 8 | 9 | ```python title="src/patcherex2/targets/elf_amd64_linux.py" 10 | --8<-- "src/patcherex2/targets/elf_amd64_linux.py" 11 | ``` 12 | 13 | ### `detect_target` Method 14 | 15 | The `detect_target` static method is responsible for automatically detecting if a given binary is supported by this target. It should return `True` if the binary matches the target criteria, or `False` otherwise. In the example above, it checks if the file is an ELF binary for the AMD64 architecture. 16 | 17 | ### `get_{component}` Methods 18 | 19 | The target definition should define methods to get the required components. The method names should be in the format `get_{component}`. The following are the list of components that must be defined for a target: 20 | 21 | - assembler 22 | - disassembler 23 | - compiler 24 | - binary_analyzer (Extract extra information from the binary file) 25 | - allocation_manager (Find free space or allocate new space in the binary) 26 | - binfmt_tool (Parse and modify binary formats, such as ELF, PE, IHEX, etc.) 27 | - utils 28 | - archinfo (Architecture specific information, such as register names, sizes, etc.) 29 | 30 | These methods allow you to specify the appropriate implementation for each component based on the target's requirements. Patcherex2 provides multiple implementations for common components that you can choose from. 31 | 32 | ##### Adding New Components 33 | 34 | If your target requires custom components not provided by Patcherex2, you can define new component classes that inherit from the respective base component classes. These custom components should implement the necessary methods to support your target's specific needs. 35 | 36 | ## Registering the New Target 37 | 38 | Once you have defined your target class, Patcherex2 will automatically register it if it is defined before creating a Patcherex2 instance (`p = Patcherex("/path/to/bin")`). Patcherex2 will call the `detect_target` method of each registered target to determine the appropriate target for the given binary. 39 | 40 | ## Manually Selecting the Target 41 | 42 | If your target is designed for manual selection only (i.e., `detect_target` always returns `False`), or if you want to override the automatic target detection, you can specify the target class when creating the Patcherex2 instance: 43 | 44 | ```python 45 | p = Patcherex("/path/to/binary", target_cls=MyCustomTarget) 46 | ``` 47 | 48 | ## Configuring the Target 49 | 50 | ### Selecting Component Implementations 51 | 52 | Some targets may support multiple implementations for a given component, allowing you to choose the desired implementation. You can configure the target by passing a configuration dictionary to the Patcherex2 constructor. 53 | 54 | For example, if your target's `get_assembler` method supports multiple assemblers: 55 | 56 | ```python 57 | def get_assembler(self, assembler): 58 | assembler = assembler or "keystone" 59 | if assembler == "keystone": 60 | return Keystone() 61 | elif assembler == "gas": 62 | return Gas() 63 | raise NotImplementedError() 64 | ``` 65 | 66 | You can select the assembler like this: 67 | 68 | ```python 69 | p = Patcherex("/path/to/binary", target_opts={"assembler": "gas"}) 70 | ``` 71 | 72 | This will use the `Gas` assembler instead of the default `Keystone` assembler. 73 | 74 | ### Configuring Components 75 | 76 | Some component implementations accept additional keyword arguments for configuration. You can pass these options through the `component_opts` parameter when creating the Patcherex2 instance. 77 | 78 | For example, if your target's `get_assembler` method accepts keyword arguments: 79 | 80 | ```python 81 | def get_assembler(self, assembler, **kwargs): 82 | assembler = assembler or "some_assembler" 83 | if assembler == "some_assembler": 84 | return SomeAssembler(**kwargs) 85 | raise NotImplementedError() 86 | ``` 87 | 88 | You can configure the assembler options like this: 89 | 90 | ```python 91 | p = Patcherex("/path/to/binary", component_opts={"assembler": {"arch": "x86", "mode": "64"}}) 92 | ``` 93 | 94 | This will create the `SomeAssembler` instance with the provided keyword arguments: 95 | 96 | ```python 97 | SomeAssembler(arch="x86", mode="64") 98 | ``` 99 | 100 | By following these steps and leveraging the extensible architecture of Patcherex2, you can easily add support for new targets and customize their behavior to suit your specific requirements. 101 | -------------------------------------------------------------------------------- /docs/advanced_usages/advanced_usage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | ## Reuse Unreachable Code Locations 4 | Patcherex2 can be used to reuse unreachable code locations in the binary. 5 | Add the following code anywhere before `apply_patches` to reuse unreachable code. 6 | 7 | ```python 8 | for func in p.binary_analyzer.get_unused_funcs(): 9 | p.allocation_manager.add_free_space(func["addr"], func["size"], "RX") 10 | ``` 11 | 12 | ## Pre- and Post- Function Hooks 13 | Patcherex2 allows you to add pre- and post- function hooks to the function call when using `InsertFunctionPatch` and first argument is a address. 14 | 15 | ```python 16 | InsertFunctionPatch(0xdeadbeef, "int foo(int a) { return bar(); }", prefunc="mov rdi, 0x10", postfunc="mov rdi, rax") 17 | ``` 18 | At the address `0xdeadbeef`, pre-function hook `mov rdi, 0x10` will be executed before the function `foo` is called and post-function hook `mov rdi, rax` will be executed after the function `foo` is called. This is useful when you want to pass arguments to the function or get the return value from the function. 19 | 20 | ## Save Context and Restore Context when using `Insert*Patch` 21 | When using `InsertInstructionPatch` or `InsertFunctionPatch`, it is possible to save the context before the inserted content and restore the context after the inserted content. This is useful when the inserted content modifies the context. 22 | 23 | ```python 24 | InsertInstructionPatch(0xdeadbeef, "push rbp", save_context=True) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/api_references/patcherex.md: -------------------------------------------------------------------------------- 1 | :::patcherex2.patcherex 2 | -------------------------------------------------------------------------------- /docs/api_references/patches.md: -------------------------------------------------------------------------------- 1 | :::patcherex2.patches -------------------------------------------------------------------------------- /docs/core_ideas/patches.md: -------------------------------------------------------------------------------- 1 | ## Patch Types 2 | 3 | The core of Patcherex2 consists of 9 different types of patches, which are used to manipulate the binary in different ways. 4 | 5 | | | Data | Instruction | Function | 6 | |---------:|-------------------|---------------------|---------------------| 7 | | _**Insert**_ | InsertDataPatch | InsertInstructionPatch | InsertFunctionPatch | 8 | | _**Remove**_ | RemoveDataPatch | RemoveInstructionPatch | RemoveFunctionPatch | 9 | | _**Modify**_ | ModifyDataPatch | ModifyInstructionPatch | ModifyFunctionPatch | 10 | 11 | These patches are categorized into three tiers: 12 | 13 | - Data Patches: 14 | Operating at the raw bytes level, data patches are ideal for patching the `.data` section or any other raw data. 15 | 16 | - Instruction Patches: 17 | These patches target the instruction level, enabling modifications to the assembly code of the binary. 18 | 19 | - Function Patches: 20 | At the highest level, function patches manipulate the binary through C code, this level deals with modifications at the function level. 21 | 22 | Each tier features three patch types: 23 | 24 | - Insert Patch: Adds new data, instructions, or functions to the binary. 25 | - Remove Patch: Deletes existing data, instructions, or functions from the binary. 26 | - Modify Patch: Replaces the content of data, instructions, or functions within the binary. 27 | 28 | ### Insert{Data, Instruction, Function}Patch 29 | - Syntax 30 | ```python 31 | Insert*Patch(addr_or_name, content) 32 | ``` 33 | - Arguments 34 | - `addr_or_name`: The address or name of the {data, instruction, function} to be inserted. 35 | - When the first argument is an address, patcherex will insert content right before the given address. 36 | - When the first argument is a name, patcherex will automatically find free spaces in the binary and insert the content there, and the `name` provided can be later used for referencing the inserted content. 37 | - `content`: The content to be inserted. 38 | - Content is different for each patch type: 39 | - For `InsertDataPatch`, `content` is a byte string. 40 | - For `InsertInstructionPatch`, `content` is a list of assembly instructions, separated by newlines. 41 | - For `InsertFunctionPatch`, `content` is a C function. 42 | 43 | ### Modify{Data, Instruction, Function}Patch 44 | - Syntax 45 | ```python 46 | Modify*Patch(addr_or_name, content) 47 | ``` 48 | - Arguments 49 | - `addr_or_name`: The address or name of the {data, instruction, function} to be modified. 50 | - When the first argument is an address, patcherex will modify the content at the given address. 51 | - When the first argument is a name, patcherex will try to first find the address of the given name/symbol and then modify the content at that address. 52 | - `content`: The new content to replace the existing content. 53 | - Content is different for each patch type: 54 | - For `ModifyDataPatch`, `content` is a byte string. 55 | - For `ModifyInstructionPatch`, `content` is a list of assembly instructions, separated by newlines. 56 | - For `ModifyFunctionPatch`, `content` is a C function. 57 | 58 | ### Remove{Data, Instruction, Function}Patch 59 | - Syntax 60 | ```python 61 | Remove*Patch(addr_or_name, num_bytes: int) 62 | ``` 63 | - Arguments 64 | - `addr_or_name`: The address or name of the {data, instruction, function} to be removed. 65 | - When the first argument is an address, patcherex will remove the content at the given address. 66 | - When the first argument is a name, patcherex will try to first find the address of the given name/symbol and then remove the content at that address. 67 | - `num_bytes`: This is optional for `RemoveInstructionPatch` and `RemoveFunctionPatch`, but required for `RemoveDataPatch`, and specifies the number of bytes to be removed. 68 | 69 | ### Referencing previously inserted content. 70 | Examples: 71 | 72 | - This will load effective address of the data `my_data` into the `rsi` register. 73 | ```python 74 | InsertDataPatch("my_data", b"Hello, World!") 75 | InsertInstructionPatch(0xdeadbeef, "lea rsi, [{my_data}]") 76 | ``` 77 | - This will replace the content of function `foo` to call function `bar` and return the result. 78 | ```python 79 | InsertFunctionPatch("bar", "int bar() { return 42; }") 80 | ModifyFunctionPatch("foo", "int bar(void); int foo() { return bar(); }") 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/examples/insert_instruction_patch.md: -------------------------------------------------------------------------------- 1 | # InsertInstructionPatch 2 | 3 | We have a simple C program: 4 | 5 | ```c title="examples/insert_instruction_patch/add.c" 6 | --8<-- "examples/insert_instruction_patch/add.c" 7 | ``` 8 | 9 | And here is the disassembly of the compiled binary: 10 | 11 | ```asm title="examples/insert_instruction_patch/add" 12 | 0000000000001149 : 13 | 1149: f3 0f 1e fa endbr64 14 | 114d: 55 push %rbp 15 | 114e: 48 89 e5 mov %rsp,%rbp 16 | 1151: 89 7d fc mov %edi,-0x4(%rbp) 17 | 1154: 89 75 f8 mov %esi,-0x8(%rbp) 18 | 1157: 8b 55 fc mov -0x4(%rbp),%edx 19 | 115a: 8b 45 f8 mov -0x8(%rbp),%eax 20 | 115d: 01 d0 add %edx,%eax 21 | 115f: 5d pop %rbp 22 | 1160: c3 ret 23 | 24 | 0000000000001161
: 25 | 1161: f3 0f 1e fa endbr64 26 | 1165: 55 push %rbp 27 | 1166: 48 89 e5 mov %rsp,%rbp 28 | 1169: be 03 00 00 00 mov $0x3,%esi 29 | 116e: bf 02 00 00 00 mov $0x2,%edi 30 | 1173: e8 d1 ff ff ff call 1149 31 | 1178: 89 c6 mov %eax,%esi 32 | 117a: 48 8d 05 83 0e 00 00 lea 0xe83(%rip),%rax # 2004 <_IO_stdin_used+0x4> 33 | 1181: 48 89 c7 mov %rax,%rdi 34 | 1184: b8 00 00 00 00 mov $0x0,%eax 35 | 1189: e8 c2 fe ff ff call 1050 36 | 118e: b8 00 00 00 00 mov $0x0,%eax 37 | 1193: 5d pop %rbp 38 | 1194: c3 ret 39 | ``` 40 | 41 | Suppose we want to modify the add function to do some 42 | extra calculations on the first argument, for example doubling it and adding 5, without changing the rest of the 43 | function. We can use Patcherex2's `InsertInstructionPatch` 44 | to insert these instructions at the address `114d` which is 45 | at the beginning of the function. To insert instructions, we 46 | need at least enough space to fit a jump instruction 47 | before the function ends, so we cannot insert them 48 | later on in the function. 49 | Here is how: 50 | 51 | ```python title="examples/insert_instruction_patch/patch.py" 52 | --8<-- "examples/insert_instruction_patch/patch.py" 53 | ``` 54 | 55 | Now we can run this script and run the patched binary 56 | to see the result: 57 | 58 | ```bash 59 | $ ./add.patched 60 | 2 + 3 = 12 61 | ``` 62 | 63 | We have successfully modified the binary at the 64 | instruction level. 65 | -------------------------------------------------------------------------------- /docs/examples/modify_function_patch.md: -------------------------------------------------------------------------------- 1 | # ModifyFunctionPatch 2 | 3 | Consider a simple C program: 4 | 5 | ```c title="examples/modify_function_patch/add.c" 6 | --8<-- "examples/modify_function_patch/add.c" 7 | ``` 8 | 9 | After compiling and executing this program, the output is: 10 | 11 | ```bash 12 | $ gcc -o add add.c && ./add 13 | 2 + 3 = 5 14 | ``` 15 | 16 | Now, we can use Patcherex2 to modify the `add` function to multiply the two arguments instead of adding them. 17 | 18 | ```python title="examples/modify_function_patch/patch.py" 19 | --8<-- "examples/modify_function_patch/patch.py" 20 | ``` 21 | 22 | Executing the patched program yields a different result: 23 | 24 | ```bash 25 | $ ./add_patched 26 | 2 + 3 = 6 27 | ``` 28 | 29 | 💥 We've successfully modified the binary with Patcherex2! 30 | -------------------------------------------------------------------------------- /docs/examples/multiple_patches.md: -------------------------------------------------------------------------------- 1 | # Multiple Patches 2 | 3 | Here is a simple example of a vulnerable C program which we will use to show how different patches can be used together: 4 | 5 | ```c title="examples/multiple_patches/getline.c" 6 | --8<-- "examples/multiple_patches/getline.c" 7 | ``` 8 | 9 | And here is the disassembly of the relevant functions: 10 | 11 | ```asm title="examples/multiple_patches/getline" 12 | 0000000000001189 : 13 | 1189: f3 0f 1e fa endbr64 14 | 118d: 55 push %rbp 15 | 118e: 48 89 e5 mov %rsp,%rbp 16 | 1191: 48 83 ec 20 sub $0x20,%rsp 17 | 1195: 48 89 7d e8 mov %rdi,-0x18(%rbp) 18 | 1199: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 19 | 11a0: 48 8b 05 69 2e 00 00 mov 0x2e69(%rip),%rax # 4010 20 | 11a7: 48 89 c7 mov %rax,%rdi 21 | 11aa: e8 e1 fe ff ff call 1090 22 | 11af: 88 45 fb mov %al,-0x5(%rbp) 23 | 11b2: 80 7d fb 0a cmpb $0xa,-0x5(%rbp) 24 | 11b6: 74 1b je 11d3 25 | 11b8: 8b 45 fc mov -0x4(%rbp),%eax 26 | 11bb: 8d 50 01 lea 0x1(%rax),%edx 27 | 11be: 89 55 fc mov %edx,-0x4(%rbp) 28 | 11c1: 48 63 d0 movslq %eax,%rdx 29 | 11c4: 48 8b 45 e8 mov -0x18(%rbp),%rax 30 | 11c8: 48 01 c2 add %rax,%rdx 31 | 11cb: 0f b6 45 fb movzbl -0x5(%rbp),%eax 32 | 11cf: 88 02 mov %al,(%rdx) 33 | 11d1: eb cd jmp 11a0 34 | 11d3: 90 nop 35 | 11d4: 8b 45 fc mov -0x4(%rbp),%eax 36 | 11d7: 48 63 d0 movslq %eax,%rdx 37 | 11da: 48 8b 45 e8 mov -0x18(%rbp),%rax 38 | 11de: 48 01 d0 add %rdx,%rax 39 | 11e1: c6 00 00 movb $0x0,(%rax) 40 | 11e4: 8b 45 fc mov -0x4(%rbp),%eax 41 | 11e7: c9 leave 42 | 11e8: c3 ret 43 | 44 | 00000000000011e9
: 45 | 11e9: f3 0f 1e fa endbr64 46 | 11ed: 55 push %rbp 47 | 11ee: 48 89 e5 mov %rsp,%rbp 48 | 11f1: 48 83 ec 20 sub $0x20,%rsp 49 | 11f5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 50 | 11fc: 00 00 51 | 11fe: 48 89 45 f8 mov %rax,-0x8(%rbp) 52 | 1202: 31 c0 xor %eax,%eax 53 | 1204: 48 8d 45 ee lea -0x12(%rbp),%rax 54 | 1208: 48 89 c7 mov %rax,%rdi 55 | 120b: e8 79 ff ff ff call 1189 56 | 1210: 48 8d 45 ee lea -0x12(%rbp),%rax 57 | 1214: 48 89 c7 mov %rax,%rdi 58 | 1217: e8 54 fe ff ff call 1070 59 | 121c: b8 00 00 00 00 mov $0x0,%eax 60 | 1221: 48 8b 55 f8 mov -0x8(%rbp),%rdx 61 | 1225: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx 62 | 122c: 00 00 63 | 122e: 74 05 je 1235 64 | 1230: e8 4b fe ff ff call 1080 <__stack_chk_fail@plt> 65 | 1235: c9 leave 66 | 1236: c3 ret 67 | ``` 68 | 69 | The program currently reads a string from standard input and echos it back. We can run it with short strings: 70 | ```bash 71 | $ ./getline 72 | aaaaa 73 | aaaaa 74 | ``` 75 | And it works fine. Using a longer string, however will cause it to crash: 76 | ```bash 77 | $ ./getline 78 | aaaaaaaaaaaaaaaaaaa 79 | aaaaaaaaaaaaaaaaaaa 80 | *** stack smashing detected ***: terminated 81 | [1] 194473 IOT instruction (core dumped) ./getline 82 | ``` 83 | 84 | We will patch this program to fix the vulnerability, by adding a second argument to the `my_getline` function, and printing a message when the buffer would have overflowed. Here is the script: 85 | 86 | ```python title="examples/multiple_patches/patch.py" 87 | --8<-- "examples/multiple_patches/patch.py" 88 | ``` 89 | 90 | This patch adds the buffer size as a second argument to `my_getline`. To do this we insert instructions at the beginning to save the argument, and in the loop body we insert a check to see if the index is out of bounds. When it is, we print a message, which was inserted using the `InsertDataPatch`. We can use the `SAVE_CONTEXT` and `RESTORE_CONTEXT` macros (expanded by Patcherex2) to do operations that could modify data we have in registers, like calling a function. We also insert instructions in `main` to put the buffer size in `esi` before calling the function. Now we can run the script to patch the binary and run the new fixed program: 91 | ```bash 92 | $ ./getline.patched 93 | aaaaaaaaaaaaaaaaaaaaaaa 94 | Ran out of space 95 | aaaaaaaaa 96 | ``` 97 | 98 | We have successfully fixed the bug with Patcherex2. 99 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Home" 3 | --- 4 | 5 | ---8<--- 'README.md' 6 | -------------------------------------------------------------------------------- /examples/insert_instruction_patch/add: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/examples/insert_instruction_patch/add -------------------------------------------------------------------------------- /examples/insert_instruction_patch/add.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int add(int a, int b) { 4 | return a + b; 5 | } 6 | 7 | int main() { 8 | printf("2 + 3 = %d\n", add(2, 3)); 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /examples/insert_instruction_patch/patch.py: -------------------------------------------------------------------------------- 1 | from patcherex2 import * 2 | 3 | p = Patcherex("add") 4 | 5 | 6 | asm_str = """ 7 | add edi, edi 8 | add edi, 5 9 | """ 10 | 11 | p.patches.append(InsertInstructionPatch(0x114d,asm_str)) 12 | p.apply_patches() 13 | 14 | p.save_binary() 15 | -------------------------------------------------------------------------------- /examples/insert_instruction_patch_c/add: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/examples/insert_instruction_patch_c/add -------------------------------------------------------------------------------- /examples/insert_instruction_patch_c/add.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int add(int a, int b) { 4 | return a + b; 5 | } 6 | 7 | int main() { 8 | printf("2 + 3 = %d\n", add(2, 3)); 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /examples/insert_instruction_patch_c/patch.py: -------------------------------------------------------------------------------- 1 | from patcherex2 import * 2 | import logging 3 | 4 | logger = logging.getLogger("patcherex2.patches.instruction_patches") 5 | logger.setLevel(logging.INFO) 6 | 7 | p = Patcherex("add", target_opts={"compiler": "clang19"}) 8 | 9 | c_forward_header = """ 10 | // This string will be inserted outside the micropatch function. It will be inserted before your code. 11 | // This is how you can define types and function forward declarations used by your C micropatch 12 | #include 13 | """ 14 | 15 | # The asm_header is inserted in the main body of the patch before the C code. This header is primarily 16 | # useful for gaining access to the stack pointer, which is a register that is unavailable in our C 17 | # code. In this example we have moved rsp to the r12 register, which is a register that is accessible. 18 | # This means that inside the C code we can access variables on the stack by using the r10 variable. 19 | # There is also an asm_footer 20 | asm_header = "mov r12, rsp" 21 | 22 | # We can access assembly registers directly by using their name, while still using high level C constructs 23 | # as well as intermediate variables. Note that you can use a return statement anywhere in your C micropatch 24 | # to jump back to the next instruction after the micropatch insertion point. 25 | c_str = """ 26 | rdi += rdi; 27 | rdi += 5; 28 | // Print out rsp as it was before the patch was started 29 | printf("%p\\n", (void *) r12); 30 | """ 31 | 32 | # It is generally a good idea to mark some registers as scratch to give the compiler 33 | # breathing room for allocating registers to use for intermediate variables in your micropatch 34 | # All of the registers that we mark as scratch can be freely clobbered by the compiler 35 | # Note that you can still read from scratch registers stored in the variables. What the scratch 36 | # register denotation will indicate however is that the register can be re-used after the variable 37 | # is no longer live. 38 | c_scratch_regs = [ 39 | 'r8', 'r9', 'r10', 'r11', 'r13', 'r14', 'r15' 40 | 'xmm0', 'xmm1', 'xmm2', 'xmm3', 'xmm4', 'xmm5', 'xmm6', 'xmm7', 'xmm9', 'xmm10', 'xmm11', 'xmm12', 'xmm13', 'xmm14', 'xmm15' 41 | ] 42 | 43 | # By default floating point registers will have the 'float' type. We can use c_regs_sort to override 44 | # certain registers so they hold different types. In this example we denote that xmm8 is of type double 45 | c_regs_sort = [('xmm8', 'double')] 46 | 47 | config = InsertInstructionPatch.CConfig( 48 | c_forward_header = c_forward_header, 49 | scratch_regs=c_scratch_regs, 50 | regs_sort=c_regs_sort, 51 | asm_header=asm_header 52 | ) 53 | 54 | p.patches.append(InsertInstructionPatch(0x114d, c_str, language="C", c_config=config)) 55 | p.apply_patches() 56 | 57 | p.binfmt_tool.save_binary() 58 | -------------------------------------------------------------------------------- /examples/modify_function_patch/add.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int add(int a, int b) { 4 | return a + b; 5 | } 6 | 7 | int main() { 8 | printf("2 + 3 = %d\n", add(2, 3)); 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /examples/modify_function_patch/patch.py: -------------------------------------------------------------------------------- 1 | from patcherex2 import * 2 | 3 | p = Patcherex("add") 4 | 5 | new_add_func = """ 6 | int add(int a, int b) { 7 | return a * b; 8 | } 9 | """ 10 | 11 | p.patches.append(ModifyFunctionPatch("add", new_add_func)) 12 | 13 | p.apply_patches() 14 | p.save_binary("add_patched") 15 | -------------------------------------------------------------------------------- /examples/multiple_patches/getline: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/examples/multiple_patches/getline -------------------------------------------------------------------------------- /examples/multiple_patches/getline.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int my_getline(char* buf) { 4 | int i = 0; 5 | while (1) { 6 | char c = getc(stdin); 7 | if (c == '\n') break; 8 | buf[i++] = c; 9 | } 10 | buf[i] = '\0'; 11 | return i; 12 | } 13 | 14 | int main() { 15 | 16 | char buf[10]; 17 | my_getline(buf); 18 | puts(buf); 19 | 20 | return 0; 21 | } -------------------------------------------------------------------------------- /examples/multiple_patches/patch.py: -------------------------------------------------------------------------------- 1 | from patcherex2 import * 2 | 3 | p = Patcherex("getline") 4 | 5 | p.patches.append(InsertInstructionPatch(0x120b,"mov esi, 0xa\n")) 6 | p.patches.append(InsertDataPatch("my_str",b"Ran out of space\0")) 7 | 8 | p.patches.append(InsertInstructionPatch(0x1199,"mov [rbp-0xc],esi")) 9 | 10 | asm_string = """ 11 | cmp edx, [rbp-0xc] 12 | jl less 13 | SAVE_CONTEXT 14 | lea rdi, [{my_str}] 15 | call {puts} 16 | RESTORE_CONTEXT 17 | mov rsi, [rbp-0x18] 18 | mov byte ptr [rsi+rax], 0 19 | mov eax, edx 20 | leave 21 | ret 22 | less: 23 | """ 24 | 25 | p.patches.append(InsertInstructionPatch(0x11c1,asm_string)) 26 | 27 | p.apply_patches() 28 | 29 | p.save_binary() -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Patcherex2 Documentation 2 | repo_url: https://github.com/purseclab/patcherex2 3 | repo_name: purseclab/patcherex2 4 | edit_uri: edit/main/docs/ 5 | 6 | theme: 7 | name: "material" 8 | features: 9 | - search.suggest 10 | - content.code.copy 11 | - toc.follow 12 | - content.action.edit 13 | - navigation.tabs 14 | - navigation.tracking 15 | - navigation.path 16 | - navigation.top 17 | palette: 18 | - scheme: pursec 19 | 20 | plugins: 21 | - mkdocstrings: 22 | handlers: 23 | python: 24 | options: 25 | docstring_style: sphinx 26 | show_symbol_type_heading: true 27 | show_symbol_type_toc: true 28 | - search 29 | - awesome-pages 30 | 31 | extra_css: 32 | - _css/extra.css 33 | 34 | markdown_extensions: 35 | - pymdownx.highlight 36 | - pymdownx.inlinehilite 37 | - pymdownx.snippets 38 | - pymdownx.superfences 39 | -------------------------------------------------------------------------------- /patcherex2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/patcherex2.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64", "setuptools-scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "patcherex2" 7 | dynamic = ["version"] 8 | requires-python = ">=3.8" 9 | readme = "README.md" 10 | dependencies = [ 11 | "angr", 12 | "pyelftools", 13 | "pypcode", 14 | "lief", 15 | "keystone-engine", 16 | "intelhex", 17 | "requests" 18 | ] 19 | 20 | [project.optional-dependencies] 21 | ghidra = ["pyhidra"] 22 | all = ["patcherex2[ghidra]"] 23 | 24 | [project.urls] 25 | "Homepage" = "https://github.com/purseclab/Patcherex2" 26 | "Source" = "https://github.com/purseclab/Patcherex2" 27 | "Tracker" = "https://github.com/purseclab/Patcherex2/issues" 28 | 29 | [tool.setuptools.packages.find] 30 | where = ["src"] 31 | 32 | [tool.ruff] 33 | exclude = ["examples"] 34 | 35 | [tool.ruff.lint] 36 | extend-select = ["I", "N", "UP", "B"] 37 | 38 | [tool.setuptools_scm] 39 | # empty 40 | -------------------------------------------------------------------------------- /src/patcherex2/__init__.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa 2 | from importlib import metadata 3 | 4 | from .patcherex import Patcherex 5 | from .patches import * 6 | 7 | __version__ = metadata.version("patcherex2") 8 | 9 | __all__ = ["Patcherex"] + patches.__all__ 10 | -------------------------------------------------------------------------------- /src/patcherex2/components/allocation_managers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/allocation_managers/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/archinfo/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/aarch64.py: -------------------------------------------------------------------------------- 1 | class Aarch64Info: 2 | nop_bytes = b"\x1f\x20\x03\xd5" 3 | nop_size = 4 4 | jmp_asm = "b {dst}" 5 | jmp_size = 4 6 | alignment = 4 7 | bits = 64 8 | is_variable_length_isa = False 9 | instr_size = 4 10 | call_asm = "bl {dst}" 11 | pc_reg_names = ["pc", "ip"] 12 | save_context_asm = """ 13 | sub sp, sp, #0x1f0 14 | stp x0, x1, [sp, #0x0] 15 | stp x2, x3, [sp, #0x10] 16 | stp x4, x5, [sp, #0x20] 17 | stp x6, x7, [sp, #0x30] 18 | stp x8, x9, [sp, #0x40] 19 | stp x10, x11, [sp, #0x50] 20 | stp x12, x13, [sp, #0x60] 21 | stp x14, x15, [sp, #0x70] 22 | stp x16, x17, [sp, #0x80] 23 | stp x18, x19, [sp, #0x90] 24 | stp x20, x21, [sp, #0xa0] 25 | stp x22, x23, [sp, #0xb0] 26 | stp x24, x25, [sp, #0xc0] 27 | stp x26, x27, [sp, #0xd0] 28 | stp x28, x29, [sp, #0xe0] 29 | str x30, [sp, #0xf0] 30 | """ 31 | restore_context_asm = """ 32 | ldp x0, x1, [sp, #0x0] 33 | ldp x2, x3, [sp, #0x10] 34 | ldp x4, x5, [sp, #0x20] 35 | ldp x6, x7, [sp, #0x30] 36 | ldp x8, x9, [sp, #0x40] 37 | ldp x10, x11, [sp, #0x50] 38 | ldp x12, x13, [sp, #0x60] 39 | ldp x14, x15, [sp, #0x70] 40 | ldp x16, x17, [sp, #0x80] 41 | ldp x18, x19, [sp, #0x90] 42 | ldp x20, x21, [sp, #0xa0] 43 | ldp x22, x23, [sp, #0xb0] 44 | ldp x24, x25, [sp, #0xc0] 45 | ldp x26, x27, [sp, #0xd0] 46 | ldp x28, x29, [sp, #0xe0] 47 | ldr x30, [sp, #0xf0] 48 | add sp, sp, #0x1f0 49 | """ 50 | 51 | cc = { 52 | "Linux": ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"], 53 | "LinuxPreserveNone": [ 54 | "x20", 55 | "x21", 56 | "x22", 57 | "x23", 58 | "x24", 59 | "x25", 60 | "x26", 61 | "x27", 62 | "x28", 63 | "x0", 64 | "x1", 65 | "x2", 66 | "x3", 67 | "x4", 68 | "x5", 69 | "x6", 70 | "x7", 71 | "x10", 72 | "x11", 73 | "x12", 74 | "x13", 75 | "x14", 76 | "x9", 77 | ], 78 | } 79 | callee_saved = { 80 | "Linux": [ 81 | "x19", 82 | "x20", 83 | "x21", 84 | "x22", 85 | "x23", 86 | "x24", 87 | "x25", 88 | "x26", 89 | "x27", 90 | "x28", 91 | "x29", 92 | "x30", 93 | ] 94 | } 95 | cc_float = {"Linux": ["v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7"]} 96 | callee_saved_float = { 97 | "Linux": ["v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15"] 98 | } 99 | 100 | float_types = {32: "float", 64: "double", 128: "long double"} 101 | 102 | @property 103 | def regs(self): 104 | return list(self.subregisters.keys()) 105 | 106 | @property 107 | def regs_float(self): 108 | return list(self.subregisters_float.keys()) 109 | 110 | subregisters = {f"x{i}": {64: [f"x{i}"], 32: [f"w{i}"]} for i in range(0, 30 + 1)} 111 | 112 | subregisters_float = { 113 | f"v{i}": { 114 | 128: [f"v{i}"], 115 | 64: [f"d{i}"], 116 | 32: [f"s{i}"], 117 | 16: [f"h{i}"], 118 | 8: [f"b{i}"], 119 | } 120 | for i in range(0, 30 + 1) 121 | } 122 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/amd64.py: -------------------------------------------------------------------------------- 1 | class Amd64Info: 2 | nop_bytes = b"\x90" 3 | nop_size = 1 4 | jmp_asm = "jmp {dst}" 5 | jmp_size = 6 6 | alignment = 4 7 | bits = 64 8 | is_variable_length_isa = True 9 | instr_size = -1 # variable length 10 | call_asm = "call {dst}" 11 | pc_reg_names = ["rip"] 12 | save_context_asm = """ 13 | push rax 14 | push rbx 15 | push rcx 16 | push rdx 17 | push rsi 18 | push rdi 19 | push rbp 20 | push rsp 21 | push r8 22 | push r9 23 | push r10 24 | push r11 25 | push r12 26 | push r13 27 | push r14 28 | push r15 29 | """ 30 | restore_context_asm = """ 31 | pop r15 32 | pop r14 33 | pop r13 34 | pop r12 35 | pop r11 36 | pop r10 37 | pop r9 38 | pop r8 39 | pop rsp 40 | pop rbp 41 | pop rdi 42 | pop rsi 43 | pop rdx 44 | pop rcx 45 | pop rbx 46 | pop rax 47 | """ 48 | 49 | cc = { 50 | "Linux": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"], 51 | "LinuxPreserveNone": [ 52 | "r12", 53 | "r13", 54 | "r14", 55 | "r15", 56 | "rdi", 57 | "rsi", 58 | "rdx", 59 | "rcx", 60 | "r8", 61 | "r9", 62 | "r11", 63 | "rax", 64 | ], 65 | "Windows": ["rcx", "rdx", "r8", "r9"], 66 | } 67 | callee_saved = {"Linux": ["r12", "r13", "r14", "r15", "rbx", "rsp", "rbp"]} 68 | cc_float = { 69 | "Linux": ["xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"] 70 | } 71 | callee_saved_float = {"Linux": []} 72 | 73 | float_types = {32: "float", 64: "double", 128: "__float128"} 74 | 75 | @property 76 | def regs(self): 77 | return list(self.subregisters.keys()) 78 | 79 | @property 80 | def regs_float(self): 81 | return list(self.subregisters_float.keys()) 82 | 83 | subregisters = { 84 | "rax": { 85 | 64: ["rax"], 86 | 32: ["eax"], 87 | 16: ["ax"], 88 | # Note that the order of the children registers is important. Only the 0th 89 | # element of this list (al) is used when determining the calling convention. 90 | # That is, we can only use the following argument 'uint8_t al' in the 91 | # calling convention at the rax position. 'uint8_t ah' is NOT allowed. 92 | 8: ["al", "ah"], 93 | }, 94 | "rbx": {64: ["rbx"], 32: ["ebx"], 16: ["bx"], 8: ["bl", "bh"]}, 95 | "rcx": {64: ["rcx"], 32: ["ecx"], 16: ["cx"], 8: ["cl", "ch"]}, 96 | "rdx": {64: ["rdx"], 32: ["edx"], 16: ["dx"], 8: ["dl", "dh"]}, 97 | "rsi": {64: ["rsi"], 32: ["esi"], 16: ["si"], 8: ["sil"]}, 98 | "rdi": {64: ["rdi"], 32: ["edi"], 16: ["di"], 8: ["dil"]}, 99 | "rbp": {64: ["rbp"], 32: ["ebp"], 16: ["bp"], 8: ["bpl"]}, 100 | "rsp": {64: ["rsp"], 32: ["esp"], 16: ["sp"], 8: ["spl"]}, 101 | "r8": {64: ["r8"], 32: ["r8d"], 16: ["r8w"], 8: ["r8b"]}, 102 | "r9": {64: ["r9"], 32: ["r9d"], 16: ["r9w"], 8: ["r9b"]}, 103 | "r10": {64: ["r10"], 32: ["r10d"], 16: ["r10w"], 8: ["r10b"]}, 104 | "r11": {64: ["r11"], 32: ["r11d"], 16: ["r11w"], 8: ["r11b"]}, 105 | "r12": {64: ["r12"], 32: ["r12d"], 16: ["r12w"], 8: ["r12b"]}, 106 | "r13": {64: ["r13"], 32: ["r13d"], 16: ["r13w"], 8: ["r13b"]}, 107 | "r14": {64: ["r14"], 32: ["r14d"], 16: ["r14w"], 8: ["r14b"]}, 108 | "r15": {64: ["r15"], 32: ["r15d"], 16: ["r15w"], 8: ["r15b"]}, 109 | } 110 | 111 | subregisters_float = {f"xmm{i}": {128: [f"xmm{i}"]} for i in range(0, 15 + 1)} 112 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/arm.py: -------------------------------------------------------------------------------- 1 | class ArmInfo: 2 | nop_bytes = b"\x00\xf0\x20\xe3" # TODO: thumb 3 | nop_size = 4 4 | jmp_asm = "b {dst}" 5 | jmp_size = 4 6 | alignment = 4 7 | bits = 32 8 | is_variable_length_isa = False 9 | instr_size = 4 # TODO: thumb 2 10 | call_asm = "bl {dst}" 11 | pc_reg_names = ["pc", "r15"] 12 | save_context_asm = """ 13 | push {r0-r11} 14 | """ 15 | restore_context_asm = """ 16 | pop {r0-r11} 17 | """ 18 | 19 | cc = { 20 | "Linux": ["r0", "r1", "r2", "r3"], 21 | # TODO: update LinuxPreserveNone once aarch64 support lands 22 | # in LLVM for preserve_none, currently defaults to Linux 23 | "LinuxPreserveNone": ["r0", "r1", "r2", "r3"], 24 | } 25 | callee_saved = { 26 | "Linux": ["r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r13", "r14", "r15"] 27 | } 28 | cc_float = {"Linux": ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"]} 29 | callee_saved_float = { 30 | "Linux": ["d8", "d9", "d10", "d11", "d12", "d13", "d14", "d15"] 31 | } 32 | 33 | float_types = {32: "float", 64: "double"} 34 | 35 | @property 36 | def regs(self): 37 | return list(self.subregisters.keys()) 38 | 39 | @property 40 | def regs_float(self): 41 | return list(self.subregisters_float.keys()) 42 | 43 | subregisters = {f"r{i}": {32: [f"r{i}"]} for i in range(0, 16)} 44 | 45 | subregisters_float = {} 46 | for i in range(0, 16): 47 | subregisters_float[f"d{i}"] = {64: [f"d{i}"], 32: [f"s{2 * i}"]} 48 | for i in range(16, 32): 49 | subregisters_float[f"d{i}"] = {64: [f"d{i}"]} 50 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/mips.py: -------------------------------------------------------------------------------- 1 | class MipsInfo: 2 | nop_bytes = b"\x00\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "j {dst}" 5 | # NOTE: keystone will always add nop for branch delay slot, so include it in size 6 | jmp_size = 8 7 | alignment = 4 8 | bits = 32 9 | is_variable_length_isa = False 10 | instr_size = 4 11 | call_asm = "jal {dst}" 12 | pc_reg_names = ["pc"] 13 | save_context_asm = """ 14 | sub $sp, $sp, -124 15 | sw $ra, 120($sp) 16 | sw $s0, 116($sp) 17 | sw $s1, 112($sp) 18 | sw $s2, 108($sp) 19 | sw $s3, 104($sp) 20 | sw $s4, 100($sp) 21 | sw $s5, 96($sp) 22 | sw $s6, 92($sp) 23 | sw $s7, 88($sp) 24 | sw $s8, 84($sp) 25 | sw $s9, 80($sp) 26 | sw $s10, 76($sp) 27 | sw $s11, 72($sp) 28 | sw $s12, 68($sp) 29 | sw $s13, 64($sp) 30 | sw $s14, 60($sp) 31 | sw $s15, 56($sp) 32 | sw $s16, 52($sp) 33 | sw $s17, 48($sp) 34 | sw $s18, 44($sp) 35 | sw $s19, 40($sp) 36 | sw $s20, 36($sp) 37 | sw $s21, 32($sp) 38 | sw $s22, 28($sp) 39 | sw $s23, 24($sp) 40 | sw $s24, 20($sp) 41 | sw $s25, 16($sp) 42 | sw $s26, 12($sp) 43 | sw $s27, 8($sp) 44 | sw $s28, 4($sp) 45 | sw $s29, 0($sp) 46 | """ 47 | restore_context_asm = """ 48 | lw $s29, 0($sp) 49 | lw $s28, 4($sp) 50 | lw $s27, 8($sp) 51 | lw $s26, 12($sp) 52 | lw $s25, 16($sp) 53 | lw $s24, 20($sp) 54 | lw $s23, 24($sp) 55 | lw $s22, 28($sp) 56 | lw $s21, 32($sp) 57 | lw $s20, 36($sp) 58 | lw $s19, 40($sp) 59 | lw $s18, 44($sp) 60 | lw $s17, 48($sp) 61 | lw $s16, 52($sp) 62 | lw $s15, 56($sp) 63 | lw $s14, 60($sp) 64 | lw $s13, 64($sp) 65 | lw $s12, 68($sp) 66 | lw $s11, 72($sp) 67 | lw $s10, 76($sp) 68 | lw $s9, 80($sp) 69 | lw $s8, 84($sp) 70 | lw $s7, 88($sp) 71 | lw $s6, 92($sp) 72 | lw $s5, 96($sp) 73 | lw $s4, 100($sp) 74 | lw $s3, 104($sp) 75 | lw $s2, 108($sp) 76 | lw $s1, 112($sp) 77 | lw $s0, 116($sp) 78 | lw $ra, 120($sp) 79 | add $sp, $sp, 124 80 | """ 81 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/mips64.py: -------------------------------------------------------------------------------- 1 | class Mips64Info: 2 | nop_bytes = b"\x00\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "j {dst}" 5 | # NOTE: keystone will aldays add nop for branch delay slot, so include it in size 6 | jmp_size = 8 7 | alignment = 4 8 | bits = 64 9 | is_variable_length_isa = False 10 | instr_size = 4 11 | call_asm = "jal {dst}" 12 | pc_reg_names = ["pc"] 13 | save_context_asm = """ 14 | sub $sp, $sp, -248 15 | sd $ra, 240($sp) 16 | sd $s0, 232($sp) 17 | sd $s1, 224($sp) 18 | sd $s2, 216($sp) 19 | sd $s3, 208($sp) 20 | sd $s4, 200($sp) 21 | sd $s5, 192($sp) 22 | sd $s6, 184($sp) 23 | sd $s7, 176($sp) 24 | sd $s8, 168($sp) 25 | sd $s9, 160($sp) 26 | sd $s10, 152($sp) 27 | sd $s11, 144($sp) 28 | sd $s12, 136($sp) 29 | sd $s13, 128($sp) 30 | sd $s14, 120($sp) 31 | sd $s15, 112($sp) 32 | sd $s16, 104($sp) 33 | sd $s17, 96($sp) 34 | sd $s18, 88($sp) 35 | sd $s19, 80($sp) 36 | sd $s20, 72($sp) 37 | sd $s21, 64($sp) 38 | sd $s22, 56($sp) 39 | sd $s23, 48($sp) 40 | sd $s24, 40($sp) 41 | sd $s25, 32($sp) 42 | sd $s26, 24($sp) 43 | sd $s27, 16($sp) 44 | sd $s28, 8($sp) 45 | sd $s29, 0($sp) 46 | """ 47 | restore_context_asm = """ 48 | ld $s29, 0($sp) 49 | ld $s28, 8($sp) 50 | ld $s27, 16($sp) 51 | ld $s26, 24($sp) 52 | ld $s25, 32($sp) 53 | ld $s24, 40($sp) 54 | ld $s23, 48($sp) 55 | ld $s22, 56($sp) 56 | ld $s21, 64($sp) 57 | ld $s20, 72($sp) 58 | ld $s19, 80($sp) 59 | ld $s18, 88($sp) 60 | ld $s17, 96($sp) 61 | ld $s16, 104($sp) 62 | ld $s15, 112($sp) 63 | ld $s14, 120($sp) 64 | ld $s13, 128($sp) 65 | ld $s12, 136($sp) 66 | ld $s11, 144($sp) 67 | ld $s10, 152($sp) 68 | ld $s9, 160($sp) 69 | ld $s8, 168($sp) 70 | ld $s7, 176($sp) 71 | ld $s6, 184($sp) 72 | ld $s5, 192($sp) 73 | ld $s4, 200($sp) 74 | ld $s3, 208($sp) 75 | ld $s2, 216($sp) 76 | ld $s1, 224($sp) 77 | ld $s0, 232($sp) 78 | ld $ra, 240($sp) 79 | add $sp, $sp, 248 80 | """ 81 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/ppc.py: -------------------------------------------------------------------------------- 1 | class PpcInfo: 2 | nop_bytes = b"\x60\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "b {dst}" 5 | jmp_size = 4 6 | alignment = 4 7 | bits = 32 8 | is_variable_length_isa = False 9 | instr_size = 4 10 | call_asm = "bl {dst}" 11 | pc_reg_names = [] 12 | save_context_asm = """ 13 | stwu r1, -0x80(r1) 14 | stmw r3, 0x8(r1) 15 | """ 16 | restore_context_asm = """ 17 | lmw r3, 0x8(r1) 18 | addi r1, r1, 0x80 19 | """ 20 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/ppc64.py: -------------------------------------------------------------------------------- 1 | class Ppc64Info: 2 | nop_bytes = b"\x60\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "b {dst}" 5 | jmp_size = 4 6 | alignment = 4 7 | bits = 64 8 | is_variable_length_isa = False 9 | instr_size = 4 10 | call_asm = "bl {dst}" 11 | pc_reg_names = [] 12 | save_context_asm = """ 13 | stwu r1, -0x80(r1) 14 | stmw r3, 0x8(r1) 15 | """ 16 | restore_context_asm = """ 17 | lmw r3, 0x8(r1) 18 | addi r1, r1, 0x80 19 | """ 20 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/ppc_vle.py: -------------------------------------------------------------------------------- 1 | class PpcVleInfo: 2 | nop_bytes = b"\x01\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "b {dst}" 5 | jmp_size = 4 6 | alignment = 4 7 | bits = 32 8 | is_variable_length_isa = True 9 | instr_size = -1 # variable length 10 | call_asm = "bl {dst}" 11 | pc_reg_names = [] 12 | save_context_asm = "" # TODO 13 | restore_context_asm = "" # TODO 14 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/s390x.py: -------------------------------------------------------------------------------- 1 | class S390xInfo: 2 | nop_bytes = b"\x07\x00" 3 | nop_size = 2 4 | jmp_asm = "jg {dst}" 5 | jmp_size = 6 6 | alignment = 2 7 | bits = 64 8 | is_variable_length_isa = True 9 | instr_size = -1 # variable length 10 | call_asm = "brasl %r14, {dst}" 11 | pc_reg_names = [] 12 | save_context_asm = """ 13 | stmg %r6,%r15,48(%r15) 14 | aghi %r15,-160 15 | """ 16 | restore_context_asm = """ 17 | lmg %r6,%r15,208(%r15) 18 | """ 19 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/sparc.py: -------------------------------------------------------------------------------- 1 | class SparcInfo: 2 | nop_bytes = b"\x01\x00\x00\x00" 3 | nop_size = 4 4 | jmp_asm = "b {dst}\nnop" # nop due to delay slot 5 | jmp_size = 8 6 | alignment = 4 7 | is_variable_length_isa = False 8 | instr_size = 4 9 | call_asm = "call {dst}" 10 | pc_reg_names = ["pc"] 11 | save_context_asm = "" # TODO 12 | restore_context_asm = "" # TODO 13 | -------------------------------------------------------------------------------- /src/patcherex2/components/archinfo/x86.py: -------------------------------------------------------------------------------- 1 | class X86Info: 2 | nop_bytes = b"\x90" 3 | nop_size = 1 4 | jmp_asm = "jmp {dst}" 5 | jmp_size = 5 6 | alignment = 4 7 | bits = 32 8 | is_variable_length_isa = True 9 | instr_size = -1 # variable length 10 | call_asm = "call {dst}" 11 | pc_reg_names = ["eip"] 12 | save_context_asm = """ 13 | pusha 14 | """ 15 | restore_context_asm = """ 16 | popa 17 | """ 18 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/assemblers/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/assembler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class Assembler: 7 | def __init__(self, p) -> None: 8 | self.p = p 9 | 10 | def resolve_symbols(self, code: str, symbols=None): 11 | if symbols is None: 12 | symbols = {} 13 | _symbols = {} 14 | _symbols.update(self.p.symbols) 15 | _symbols.update(self.p.binary_analyzer.get_all_symbols()) 16 | _symbols.update(symbols) 17 | 18 | for symbol, addr in _symbols.items(): 19 | code = code.replace(f"{{{symbol}}}", hex(addr)) 20 | return code 21 | 22 | def _assemble(self, code: str, base=0, **kwargs) -> None: 23 | raise NotImplementedError() 24 | 25 | def _pre_assemble_hook(self, code: str, base=0) -> None: 26 | return code 27 | 28 | def assemble(self, code: str, base=0, symbols=None, **kwargs) -> None: 29 | if code == "": 30 | return b"" 31 | if symbols is None: 32 | symbols = {} 33 | logger.debug(f"Assembling `{code}` at {hex(base)}") 34 | code = self.resolve_symbols(code, symbols=symbols) 35 | code = self._pre_assemble_hook(code, base=base) 36 | 37 | return self._assemble(code, base=base, **kwargs) 38 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/bcc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import subprocess 4 | import tempfile 5 | 6 | from ..assets.assets import Assets 7 | from .assembler import Assembler 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Bcc(Assembler): 13 | def __init__(self, p) -> None: 14 | super().__init__(p) 15 | self.assets_path = Assets("bcc").path 16 | 17 | def _assemble(self, code: str, base=0, **kwargs) -> bytes: 18 | with tempfile.TemporaryDirectory() as td: 19 | with open(os.path.join(td, "code.s"), "w") as f: 20 | f.write(f".org {hex(base)}\n") 21 | f.write(code) 22 | try: 23 | subprocess.run( 24 | [ 25 | os.path.join(self.assets_path, "sparc-gaisler-elf-as"), 26 | "-Aleon", 27 | os.path.join(td, "code.s"), 28 | "-o", 29 | os.path.join(td, "obj.o"), 30 | ], 31 | check=True, 32 | capture_output=True, 33 | ) 34 | except subprocess.CalledProcessError as e: 35 | logger.error(e.stderr.decode("utf-8")) 36 | raise e 37 | try: 38 | subprocess.run( 39 | [ 40 | os.path.join(self.assets_path, "sparc-gaisler-elf-objcopy"), 41 | "-O", 42 | "binary", 43 | "-j", 44 | ".text", 45 | os.path.join(td, "obj.o"), 46 | os.path.join(td, "obj.bin"), 47 | ], 48 | check=True, 49 | capture_output=True, 50 | ) 51 | except subprocess.CalledProcessError as e: 52 | logger.error(e.stderr.decode("utf-8")) 53 | raise e 54 | with open(os.path.join(td, "obj.bin"), "rb") as f: 55 | if base != 0: 56 | f.seek(base) 57 | binary = f.read() 58 | logger.debug(f"Assembled bytes: {bytes(binary).hex()}") 59 | return bytes(binary) 60 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/keystone.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import keystone 4 | 5 | from .assembler import Assembler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Keystone(Assembler): 11 | def __init__(self, p, arch: int, mode: int) -> None: 12 | super().__init__(p) 13 | self.arch = arch 14 | self.mode = mode 15 | self.ks = keystone.Ks(arch, mode) 16 | 17 | def _assemble(self, code: str, base=0, **kwargs) -> bytes: 18 | try: 19 | binary, _ = self.ks.asm(code, base) 20 | logger.debug(f"Assembled bytes: {bytes(binary).hex()}") 21 | return bytes(binary) 22 | except Exception as e: 23 | raise Exception( 24 | f'Failed to assemble: """\n{code}\n"""\nat base: {hex(base)}' 25 | ) from e 26 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/keystone_arm.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import keystone 4 | 5 | from .assembler import Assembler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class KeystoneArm(Assembler): 11 | def __init__(self, p) -> None: 12 | super().__init__(p) 13 | self.ks_arm = keystone.Ks( 14 | keystone.KS_ARCH_ARM, keystone.KS_MODE_ARM + keystone.KS_MODE_LITTLE_ENDIAN 15 | ) 16 | self.ks_thumb = keystone.Ks( 17 | keystone.KS_ARCH_ARM, 18 | keystone.KS_MODE_THUMB + keystone.KS_MODE_LITTLE_ENDIAN, 19 | ) 20 | 21 | def _assemble(self, code: str, base=0, is_thumb=False) -> bytes: 22 | try: 23 | ks = self.ks_thumb if is_thumb else self.ks_arm 24 | binary, _ = ks.asm(code, base) 25 | logger.debug(f"Assembled bytes: {bytes(binary).hex()}") 26 | return bytes(binary) 27 | except Exception as e: 28 | raise Exception( 29 | f'Failed to assemble: """\n{code}\n"""\nat base: {hex(base)}' 30 | ) from e 31 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/keystone_sparc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import keystone 5 | 6 | from .keystone import Keystone 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class KeystoneSparc(Keystone): 12 | def _pre_assemble_hook(self, code: str, base=0) -> str: 13 | # Hack for sparc, remove this hack if keystone supports sparc relative addressing correctly 14 | # https://www.gaisler.com/doc/sparcv8.pdf 15 | # call (B.24.), all branches instr (B.21.) jumps to PC + 4ximm, so we need to convert absolute address to relative address 16 | if self.arch != keystone.KS_ARCH_SPARC: 17 | return code 18 | result = "" 19 | lineno = 0 20 | for line in code.splitlines(): 21 | line = line.strip() 22 | if ( 23 | line.startswith(".") 24 | or line.startswith("#") 25 | or line == "" 26 | or line.endswith(":") 27 | ): 28 | result += line + "\n" 29 | continue 30 | if re.match(r"(call|b|ba) 0x[0-9a-fA-F]+", line): 31 | instr, addr = line.split(" ") 32 | addr = int(addr, 16) 33 | disp = addr - base 34 | imm = disp >> 2 35 | logger.debug( 36 | f"converting {line} to {instr} {hex(imm)} (base: {hex(base)})" 37 | ) 38 | if instr == "call": 39 | result += f"{instr} {hex(base + imm - lineno)}\n" 40 | elif instr == "b" or instr == "ba": 41 | result += f"{instr} {hex(addr - (4 * lineno))}\n" 42 | else: 43 | result += line + "\n" 44 | lineno += 1 45 | return result 46 | -------------------------------------------------------------------------------- /src/patcherex2/components/assemblers/ppc_vle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import subprocess 5 | import tempfile 6 | 7 | from ..assets.assets import Assets 8 | from .assembler import Assembler 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class PpcVle(Assembler): 14 | def __init__(self, p) -> None: 15 | super().__init__(p) 16 | self.assets_path = Assets("ppc_vle").path 17 | 18 | def _assemble(self, code: str, base=0, **kwargs) -> bytes: 19 | code = re.subn(r"\br(\d+)\b", r"\1", code)[0] 20 | 21 | if base is not None: 22 | # produce a list of {instr_offset: instr} pairs 23 | branch_instrs = {} 24 | instr_count = 0 25 | for line in code.splitlines(): 26 | line = line.strip() 27 | if ( 28 | line.startswith(".") 29 | or line.startswith("#") 30 | or line == "" 31 | or line.endswith(":") 32 | ): 33 | continue 34 | # if line matches "b 0x*" or "bl 0x*", add it to the branch_instrs dict 35 | if re.match(r"b[a-z]* 0x[0-9a-fA-F]+", line): 36 | branch_instrs[instr_count] = line 37 | instr_count += 1 38 | 39 | for i in range(instr_count): 40 | if i in branch_instrs: 41 | branch_instrs[i] = ( 42 | branch_instrs[i].split(" ")[0] 43 | + " " 44 | + hex(int(branch_instrs[i].split(" ")[1], 16) - base - 4 * i) 45 | ) 46 | 47 | instr_count = 0 48 | for line_count, line in enumerate(code.splitlines()): 49 | if ( 50 | line.startswith(".") 51 | or line.startswith("#") 52 | or line == "" 53 | or line.endswith(":") 54 | ): 55 | continue 56 | if instr_count in branch_instrs: 57 | code = code.splitlines() 58 | code[line_count] = branch_instrs[instr_count] 59 | code = "\n".join(code) 60 | instr_count += 1 61 | 62 | # set base address 63 | if base is not None: 64 | code = f".org {hex(base)}\n" + code 65 | 66 | with tempfile.TemporaryDirectory() as td: 67 | with open(os.path.join(td, "code.s"), "w") as f: 68 | f.write(code) 69 | try: 70 | subprocess.run( 71 | [ 72 | os.path.join(self.assets_path, "powerpc-eabivle-as"), 73 | "-o", 74 | os.path.join(td, "obj.o"), 75 | os.path.join(td, "code.s"), 76 | ], 77 | check=True, 78 | capture_output=True, 79 | ) 80 | except subprocess.CalledProcessError as e: 81 | logger.error(e.stderr.decode("utf-8")) 82 | raise e 83 | try: 84 | subprocess.run( 85 | [ 86 | os.path.join(self.assets_path, "powerpc-eabivle-objcopy"), 87 | "-O", 88 | "binary", 89 | "-j", 90 | ".text", 91 | os.path.join(td, "obj.o"), 92 | os.path.join(td, "obj.bin"), 93 | ], 94 | check=True, 95 | capture_output=True, 96 | ) 97 | except subprocess.CalledProcessError as e: 98 | logger.error(e.stderr.decode("utf-8")) 99 | raise e 100 | with open(os.path.join(td, "obj.bin"), "rb") as f: 101 | if base != 0: 102 | f.seek(base) 103 | binary = f.read() 104 | logger.debug(f"Assembled bytes: {bytes(binary).hex()}") 105 | return bytes(binary) 106 | -------------------------------------------------------------------------------- /src/patcherex2/components/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !assets.py 4 | -------------------------------------------------------------------------------- /src/patcherex2/components/assets/assets.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import tarfile 4 | import tempfile 5 | from pathlib import Path 6 | 7 | import requests 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Assets: 13 | ASSETS_DIR = Path(__file__).parent 14 | ASSETS = { 15 | "bcc": { 16 | "url": "https://assets.patcherex.pursec.han.ac/bcc-2.2.4-gcc-linux64.tar.xz", 17 | "path": ASSETS_DIR / "bcc" / "bcc-2.2.4-gcc" / "bin", 18 | }, 19 | "ppc_vle": { 20 | "url": "https://assets.patcherex.pursec.han.ac/powerpc-eabivle.tgz", 21 | "path": ASSETS_DIR / "ppc_vle" / "bin", 22 | }, 23 | "llvm_recomp": { 24 | "url": "https://assets.patcherex.pursec.han.ac/llvm_recomp.tgz", 25 | "path": ASSETS_DIR / "llvm_recomp", 26 | }, 27 | } 28 | 29 | def __init__(self, name: str) -> None: 30 | self.name = name 31 | self.url = self.ASSETS[name]["url"] 32 | self.path = self.ASSETS[name]["path"] 33 | if not os.path.exists(self.ASSETS_DIR / self.name): 34 | logger.info(f"{self.name} not found, downloading...") 35 | self.download() 36 | 37 | def download(self) -> None: 38 | r = requests.get(self.url) 39 | with tempfile.TemporaryDirectory() as td: 40 | with open(os.path.join(td, "asset.tgz"), "wb") as f: 41 | f.write(r.content) 42 | with tarfile.open(os.path.join(td, "asset.tgz")) as tar: 43 | # FIXME: better use filter here but it requires > py3.12. all tarball are manually verified to be safe so it's fine for now 44 | tar.extractall(path=self.ASSETS_DIR / self.name) 45 | -------------------------------------------------------------------------------- /src/patcherex2/components/binary_analyzers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/binary_analyzers/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/binary_analyzers/binary_analyzer.py: -------------------------------------------------------------------------------- 1 | class BinaryAnalyzer: 2 | pass 3 | -------------------------------------------------------------------------------- /src/patcherex2/components/binary_analyzers/ghidra.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import tempfile 5 | 6 | from .binary_analyzer import BinaryAnalyzer 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Ghidra(BinaryAnalyzer): 12 | def __init__(self, binary_path: str, **kwargs): 13 | import pyhidra 14 | 15 | self.temp_proj_dir_ctx = tempfile.TemporaryDirectory() 16 | self.temp_proj_dir = self.temp_proj_dir_ctx.__enter__() 17 | 18 | self.pyhidra_ctx = pyhidra.open_program(binary_path, self.temp_proj_dir) 19 | self.flatapi = self.pyhidra_ctx.__enter__() 20 | self.currentProgram = self.flatapi.getCurrentProgram() 21 | 22 | import ghidra 23 | 24 | self.ghidra = ghidra 25 | 26 | self.bbm = self.ghidra.program.model.block.BasicBlockModel(self.currentProgram) 27 | 28 | def shutdown(self): 29 | self.pyhidra_ctx.__exit__(None, None, None) 30 | self.temp_proj_dir_ctx.__exit__(None, None, None) 31 | 32 | def normalize_addr(self, addr): 33 | addr = addr.getOffset() 34 | if self.currentProgram.getRelocationTable().isRelocatable(): 35 | addr -= self.currentProgram.getImageBase().getOffset() 36 | return addr 37 | 38 | def denormalize_addr(self, addr): 39 | if self.currentProgram.getRelocationTable().isRelocatable(): 40 | addr += self.currentProgram.getImageBase().getOffset() 41 | return self.flatapi.toAddr(hex(addr)) 42 | 43 | def mem_addr_to_file_offset(self, addr: int) -> int: 44 | addr = self.denormalize_addr(addr) 45 | try: 46 | return ( 47 | self.currentProgram.getMemory() 48 | .getAddressSourceInfo(addr) 49 | .getFileOffset() 50 | ) 51 | except Exception: 52 | raise Exception("Can't get file offset for addr") from None 53 | 54 | def get_basic_block(self, addr: int) -> dict[str, int | list[int]]: 55 | logger.info(f"getting basic block at 0x{addr} with ghidra") 56 | addr = self.denormalize_addr(addr) 57 | 58 | block = self.bbm.getFirstCodeBlockContaining( 59 | addr, self.ghidra.util.task.TaskMonitor.DUMMY 60 | ) 61 | if block is None: 62 | raise Exception(f"Cannot find block containing address 0x{addr}") 63 | instrs = [] 64 | ii = self.currentProgram.getListing().getInstructions(block, True) 65 | for i in ii: 66 | instrs.append(self.normalize_addr(i.getAddress())) 67 | return { 68 | "start": self.normalize_addr(block.getMinAddress()), 69 | "end": self.normalize_addr(block.getMinAddress()) + block.getNumAddresses(), 70 | "size": block.getNumAddresses(), 71 | "instruction_addrs": instrs, 72 | } 73 | 74 | def get_instr_bytes_at(self, addr: int, num_instr=1): 75 | addr = self.denormalize_addr(addr) 76 | instr = self.currentProgram.getListing().getInstructionContaining(addr) 77 | if instr is None: 78 | return None 79 | b = instr.getBytes() 80 | for _i in range(1, num_instr): 81 | instr = instr.getNext() 82 | b = b"".join([b, instr.getBytes()]) 83 | logger.info( 84 | f"got instr bytes of length {len(b)} for {num_instr} instrs at 0x{addr} with ghidra" 85 | ) 86 | return b 87 | 88 | def get_unused_funcs(self) -> list[dict[str, int]]: 89 | logger.info("getting unused funcs with ghidra") 90 | fi = self.currentProgram.getListing().getFunctions(True) 91 | unused_funcs = [] 92 | for f in fi: 93 | if not f.getSymbol().hasReferences(): 94 | b = f.getBody() 95 | unused_funcs.append( 96 | { 97 | "addr": self.normalize_addr(b.getMinAddress()), 98 | "size": b.getNumAddresses(), 99 | } 100 | ) 101 | return unused_funcs 102 | 103 | def get_all_symbols(self) -> dict[str, int]: 104 | logger.info("getting all symbols with ghidra") 105 | symbols = {} 106 | # si = self.currentProgram.getSymbolTable().getAllSymbols(True) 107 | # for s in si: 108 | # if not s.isPrimary(): 109 | # continue 110 | # symbols[s.getName()] = self.normalize_addr( 111 | # s.getAddress().getOffset()) 112 | fi = self.currentProgram.getListing().getFunctions(True) 113 | for f in fi: 114 | if f.getName() in symbols.keys(): 115 | continue 116 | symbols[f.getName()] = self.normalize_addr(f.getEntryPoint()) 117 | if self.is_thumb(symbols[f.getName()]): 118 | symbols[f.getName()] += 1 119 | return symbols 120 | 121 | def get_function(self, name_or_addr: int | str) -> dict[str, int] | None: 122 | if isinstance(name_or_addr, int): 123 | name_or_addr = self.denormalize_addr(name_or_addr) 124 | func = self.currentProgram.getListing().getFunctionContaining(name_or_addr) 125 | if func is None: 126 | return None 127 | elif isinstance(name_or_addr, str): 128 | funcs = self.currentProgram.getListing().getGlobalFunctions(name_or_addr) 129 | if len(funcs) == 0: 130 | return None 131 | func = funcs[0] 132 | else: 133 | raise Exception("Invalid type for argument") 134 | 135 | b = func.getBody() 136 | return { 137 | "addr": self.normalize_addr(b.getMinAddress()), 138 | "size": b.getNumAddresses(), 139 | } 140 | 141 | def is_thumb(self, addr: int) -> bool: 142 | addr = self.denormalize_addr(addr) 143 | r = self.currentProgram.getRegister("TMode") 144 | if r is None: 145 | return False 146 | v = self.currentProgram.getProgramContext().getRegisterValue(r, addr) 147 | t = v.unsignedValueIgnoreMask.intValue() == 1 148 | logger.info(f"address 0x{addr} {'is' if t else 'is not'} thumb from ghidra") 149 | return t 150 | -------------------------------------------------------------------------------- /src/patcherex2/components/binary_analyzers/ida.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from headless_ida import HeadlessIdaRemote 4 | 5 | from .binary_analyzer import BinaryAnalyzer 6 | 7 | 8 | class Ida(BinaryAnalyzer): 9 | _DEFAULT_LOAD_BASE = 0x0 10 | 11 | def __init__(self, binary_path: str, **kwargs) -> None: 12 | self.binary_path = binary_path 13 | self.kwargs = kwargs 14 | self.ida_server_host = ( 15 | kwargs["ida_server_host"] if "ida_server_host" in kwargs else "localhost" 16 | ) 17 | self.ida_server_port = ( 18 | kwargs["ida_server_port"] if "ida_server_port" in kwargs else 1337 19 | ) 20 | self._headlessida = HeadlessIdaRemote( 21 | self.ida_server_host, self.ida_server_port, self.binary_path 22 | ) 23 | ida_libs = [ 24 | "idc", 25 | "idautils", 26 | "idaapi", 27 | "ida_funcs", 28 | "ida_xref", 29 | "ida_nalt", 30 | "ida_auto", 31 | "ida_hexrays", 32 | "ida_name", 33 | "ida_expr", 34 | "ida_struct", 35 | "ida_typeinf", 36 | "ida_loader", 37 | "ida_lines", 38 | "ida_segment", 39 | "ida_gdl", 40 | ] 41 | for lib in ida_libs: 42 | setattr(self, lib, self._headlessida.import_module(lib)) 43 | 44 | def mem_addr_to_file_offset(self, addr: int) -> int: 45 | return self.ida_loader.get_fileregion_offset(addr) 46 | 47 | def get_basic_block(self, addr: int) -> dict[str, int | list[int]]: 48 | func = self.ida_funcs.get_func(addr) 49 | instr_addrs = list(func.code_items()) 50 | assert addr in instr_addrs, "Invalid address" 51 | flowchart = self.ida_gdl.FlowChart(f=func, flags=self.ida_gdl.FC_PREDS) 52 | 53 | for block in flowchart: 54 | if block.start_ea <= addr < block.end_ea: 55 | return { 56 | "start": block.start_ea, 57 | "end": block.end_ea, 58 | "size": block.end_ea - block.start_ea, 59 | "instruction_addrs": [ 60 | ea for ea in instr_addrs if block.start_ea <= ea < block.end_ea 61 | ], 62 | } 63 | 64 | def get_instr_bytes_at(self, addr: int): 65 | return self.p.factory.block(addr, num_inst=1).bytes 66 | -------------------------------------------------------------------------------- /src/patcherex2/components/binfmt_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/binfmt_tools/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/binfmt_tools/binary.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import logging 5 | 6 | from .binfmt_tool import BinFmtTool 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Binary(BinFmtTool): 12 | def __init__(self, p, binary_path: str) -> None: 13 | super().__init__(p, binary_path) 14 | self._file = open(binary_path, "rb") 15 | self.file_size = self._file.seek(0, io.SEEK_END) 16 | self._file.seek(0) 17 | self.file_updates = [] 18 | 19 | def __del__(self) -> None: 20 | self._file.close() 21 | 22 | def _init_memory_analysis(self) -> None: 23 | pass 24 | 25 | def finalize(self) -> None: 26 | pass 27 | 28 | def save_binary(self, filename: str | None = None) -> None: 29 | if filename is None: 30 | filename = f"{self.binary_path}.patched" 31 | with open(filename, "wb") as f: 32 | self._file.seek(0) 33 | f.write(self._file.read()) 34 | # apply the updates 35 | for update in self.file_updates: 36 | f.seek(update["offset"]) 37 | f.write(update["content"]) 38 | 39 | def update_binary_content(self, offset: int, new_content: bytes) -> None: 40 | logger.debug( 41 | f"Updating offset {hex(offset)} with content ({len(new_content)} bytes) {new_content.hex()}" 42 | ) 43 | for update in self.file_updates: 44 | if offset >= update["offset"] and offset < update["offset"] + len( 45 | update["content"] 46 | ): 47 | raise ValueError( 48 | f"Cannot update offset {hex(offset)} with content {new_content}, it overlaps with a previous update" 49 | ) 50 | self.file_updates.append({"offset": offset, "content": new_content}) 51 | if offset + len(new_content) > self.file_size: 52 | self.file_size = offset + len(new_content) 53 | 54 | def get_binary_content(self, offset: int, size: int) -> bytes: 55 | # FIXME: content partially in the file and partially in the updates (check other binfmt tools as well) 56 | # check if it's in the file updates 57 | for update in self.file_updates: 58 | if offset >= update["offset"] and offset + size <= update["offset"] + len( 59 | update["content"] 60 | ): 61 | return update["content"][ 62 | offset - update["offset"] : offset - update["offset"] + size 63 | ] 64 | # otherwise read from the file 65 | self._file.seek(offset) 66 | return self._file.read(size) 67 | 68 | def append_to_binary_content(self, new_content: bytes) -> None: 69 | self.file_updates.append({"offset": self.file_size, "content": new_content}) 70 | self.file_size += len(new_content) 71 | -------------------------------------------------------------------------------- /src/patcherex2/components/binfmt_tools/binfmt_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class BinFmtTool: 7 | def __init__(self, p, binary_path: str) -> None: 8 | self.p = p 9 | self.binary_path = binary_path 10 | 11 | def _init_memory_analysis(self) -> None: 12 | raise NotImplementedError() 13 | 14 | def save_binary(self, filename=None) -> None: 15 | raise NotImplementedError() 16 | 17 | def update_binary_content(self, offset: str, new_content: bytes) -> None: 18 | raise NotImplementedError() 19 | 20 | def append_to_binary_content(self, new_content: bytes) -> None: 21 | raise NotImplementedError() 22 | -------------------------------------------------------------------------------- /src/patcherex2/components/binfmt_tools/ihex.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import logging 5 | 6 | import intelhex 7 | 8 | from .binfmt_tool import BinFmtTool 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class IHex(BinFmtTool): 14 | def __init__(self, p, binary_path: str) -> None: 15 | super().__init__(p, binary_path) 16 | self._file = open(binary_path, "rb") 17 | self._ihex = intelhex.IntelHex(binary_path) 18 | self.file_size = self._ihex.maxaddr() + 1 19 | self.file_updates = [] 20 | 21 | def __del__(self) -> None: 22 | self._file.close() 23 | 24 | def _init_memory_analysis(self) -> None: 25 | pass 26 | 27 | def finalize(self) -> None: 28 | pass 29 | 30 | def save_binary(self, filename: str | None = None) -> None: 31 | for update in self.file_updates: 32 | self._ihex.puts(update["offset"], update["content"]) 33 | if filename is None: 34 | filename = f"{self.binary_path}.patched" 35 | sio = io.StringIO() 36 | self._ihex.write_hex_file(sio, byte_count=0x20) 37 | final_content = sio.getvalue() 38 | sio.close() 39 | entry_point = "" 40 | final = "" 41 | for line in final_content.splitlines(): 42 | if line.startswith(":04000005"): 43 | entry_point = line 44 | elif line == ":00000001FF": 45 | final += entry_point + "\n" 46 | final += line + "\n" 47 | else: 48 | final += line + "\n" 49 | 50 | with open(filename, "w") as f: 51 | f.write(final) 52 | 53 | def update_binary_content(self, offset: int, new_content: bytes) -> None: 54 | logger.debug( 55 | f"Updating offset {hex(offset)} with content ({len(new_content)} bytes) {new_content.hex()}" 56 | ) 57 | for update in self.file_updates: 58 | if offset >= update["offset"] and offset < update["offset"] + len( 59 | update["content"] 60 | ): 61 | raise ValueError( 62 | f"Cannot update offset {hex(offset)} with content {new_content}, it overlaps with a previous update" 63 | ) 64 | self.file_updates.append({"offset": offset, "content": new_content}) 65 | if offset + len(new_content) > self.file_size: 66 | self.file_size = offset + len(new_content) 67 | 68 | def get_binary_content(self, offset: int, size: int) -> bytes: 69 | # check if it's in the file updates 70 | for update in self.file_updates: 71 | if offset >= update["offset"] and offset + size <= update["offset"] + len( 72 | update["content"] 73 | ): 74 | return update["content"][ 75 | offset - update["offset"] : offset - update["offset"] + size 76 | ] 77 | # otherwise read from the file 78 | return self._ihex.tobinarray(start=offset, size=size) 79 | 80 | def append_to_binary_content(self, new_content: bytes) -> None: 81 | self.file_updates.append({"offset": self.file_size, "content": new_content}) 82 | self.file_size += len(new_content) 83 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/compilers/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/bcc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from ..assets.assets import Assets 5 | from .compiler import Compiler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Bcc(Compiler): 11 | def __init__(self, p) -> None: 12 | super().__init__(p) 13 | self.assets_path = Assets("bcc").path 14 | self._compiler = os.path.join(self.assets_path, "sparc-gaisler-elf-gcc") 15 | self._linker = os.path.join(self.assets_path, "sparc-gaisler-elf-ld") 16 | self._compiler_flags = ["-qbsp=gr712rc", "-mcpu=leon3", "-mfix-gr712rc"] 17 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/clang.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from .compiler import Compiler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Clang(Compiler): 11 | def __init__( 12 | self, p, clang_version=15, compiler_flags: list[str] | None = None 13 | ) -> None: 14 | super().__init__(p) 15 | self.preserve_none = clang_version >= 19 16 | if compiler_flags is None: 17 | compiler_flags = [] 18 | self._compiler = f"clang-{clang_version}" 19 | self._linker = f"ld.lld-{clang_version}" 20 | self._compiler_flags = compiler_flags 21 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/clang_arm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from .compiler import Compiler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class ClangArm(Compiler): 11 | def __init__( 12 | self, p, clang_version=15, compiler_flags: list[str] | None = None 13 | ) -> None: 14 | super().__init__(p) 15 | if compiler_flags is None: 16 | compiler_flags = [] 17 | self._compiler = f"clang-{clang_version}" 18 | self._linker = f"ld.lld-{clang_version}" 19 | self._compiler_flags = compiler_flags 20 | 21 | def compile( 22 | self, 23 | code: str, 24 | base=0, 25 | symbols: dict[str, int] | None = None, 26 | extra_compiler_flags: list[str] | None = None, 27 | is_thumb=False, 28 | **kwargs, 29 | ) -> bytes: 30 | if symbols is None: 31 | symbols = {} 32 | if extra_compiler_flags is None: 33 | extra_compiler_flags = [] 34 | if is_thumb: 35 | extra_compiler_flags += ["-mthumb"] 36 | else: 37 | extra_compiler_flags += ["-mno-thumb"] 38 | compiled = super().compile( 39 | code, 40 | base=base, 41 | symbols=symbols, 42 | extra_compiler_flags=extra_compiler_flags, 43 | **kwargs, 44 | ) 45 | 46 | # FIXME: damn this is too hacky 47 | _symbols = {} 48 | _symbols.update(self.p.symbols) 49 | _symbols.update(self.p.binary_analyzer.get_all_symbols()) 50 | _symbols.update(symbols) 51 | symbols = _symbols 52 | disasm = self.p.disassembler.disassemble(compiled, base=base, is_thumb=is_thumb) 53 | reassembled = b"" 54 | for instr in disasm: 55 | if ( 56 | is_thumb 57 | and instr["mnemonic"] == "bl" 58 | and int(instr["op_str"][1:], 0) in symbols.values() 59 | ): 60 | disasm_str = ( 61 | self.p.disassembler.to_asm_string(instr).replace("bl", "blx") + "\n" 62 | ) 63 | reassembled += self.p.assembler.assemble( 64 | disasm_str, base=instr["address"], is_thumb=is_thumb 65 | ) 66 | elif ( 67 | is_thumb 68 | and instr["mnemonic"] == "blx" 69 | and (int(instr["op_str"][1:], 0) + 1) in symbols.values() 70 | ): 71 | disasm_str = ( 72 | self.p.disassembler.to_asm_string(instr).replace("blx", "bl") + "\n" 73 | ) 74 | reassembled += self.p.assembler.assemble( 75 | disasm_str, base=instr["address"], is_thumb=is_thumb 76 | ) 77 | elif ( 78 | not is_thumb 79 | and instr["mnemonic"] == "bl" 80 | and (int(instr["op_str"][1:], 0) + 1) in symbols.values() 81 | ): 82 | disasm_str = ( 83 | self.p.disassembler.to_asm_string(instr).replace("bl", "blx") + "\n" 84 | ) 85 | reassembled += self.p.assembler.assemble( 86 | disasm_str, base=instr["address"], is_thumb=is_thumb 87 | ) 88 | elif ( 89 | not is_thumb 90 | and instr["mnemonic"] == "blx" 91 | and int(instr["op_str"][1:], 0) in symbols.values() 92 | ): 93 | disasm_str = ( 94 | self.p.disassembler.to_asm_string(instr).replace("blx", "bl") + "\n" 95 | ) 96 | reassembled += self.p.assembler.assemble( 97 | disasm_str, base=instr["address"], is_thumb=is_thumb 98 | ) 99 | else: 100 | reassembled += compiled[ 101 | instr["address"] - base : instr["address"] - base + instr["size"] 102 | ] 103 | compiled = reassembled + compiled[len(reassembled) :] 104 | if len(compiled) % 2 != 0: 105 | compiled += b"\x00" 106 | return compiled 107 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/compiler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import subprocess 6 | import tempfile 7 | 8 | import cle 9 | from elftools.elf.elffile import ELFFile 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Compiler: 15 | def __init__(self, p) -> None: 16 | self.p = p 17 | # preserve_none is a special attribute flag to allow us to control more registers as input to a C function 18 | # This feature is used for a C instruction patch 19 | self.preserve_none = False 20 | 21 | def compile( 22 | self, 23 | code: str, 24 | base=0, 25 | symbols: dict[str, int] | None = None, 26 | extra_compiler_flags: list[str] | None = None, 27 | **kwargs, 28 | ) -> bytes: 29 | if symbols is None: 30 | symbols = {} 31 | if extra_compiler_flags is None: 32 | extra_compiler_flags = [] 33 | with tempfile.TemporaryDirectory() as td: 34 | # source file 35 | with open(os.path.join(td, "code.c"), "w") as f: 36 | f.write(code) 37 | 38 | # compile to object file 39 | try: 40 | args = ( 41 | [self._compiler] 42 | + self._compiler_flags 43 | + extra_compiler_flags 44 | + [ 45 | "-c", 46 | os.path.join(td, "code.c"), 47 | "-o", 48 | os.path.join(td, "obj.o"), 49 | ] 50 | ) 51 | subprocess.run(args, check=True, capture_output=True) 52 | except subprocess.CalledProcessError as e: 53 | logger.error(e.stderr.decode("utf-8")) 54 | raise e 55 | 56 | # linker script 57 | _symbols = {} 58 | _symbols.update(self.p.symbols) 59 | _symbols.update(self.p.binary_analyzer.get_all_symbols()) 60 | _symbols.update(symbols) 61 | 62 | # TODO: shouldn't put .rodata in .text, but otherwise switch case jump table won't work 63 | # Note that even we don't include .rodata here, cle might still include it if there is 64 | # no gap between .text and .rodata 65 | with open(os.path.join(td, "obj.o"), "rb") as f: 66 | linker_script_rodata_sections = " ".join( 67 | [ 68 | f". = ALIGN({section['sh_addralign']}); *({section.name})" 69 | for section in ELFFile(f).iter_sections() 70 | if section.name.startswith(".rodata") 71 | ] 72 | ) 73 | linker_script_symbols = "".join( 74 | f"{name} = {hex(addr)};" for name, addr in _symbols.items() 75 | ) 76 | 77 | linker_script = f"SECTIONS {{ .patcherex2 : SUBALIGN(0) {{ . = {hex(base)}; *(.text) {linker_script_rodata_sections} {linker_script_symbols} }} }}" 78 | with open(os.path.join(td, "linker.ld"), "w") as f: 79 | f.write(linker_script) 80 | 81 | # link object file 82 | try: 83 | args = [self._linker] + [ 84 | "-relocatable", 85 | os.path.join(td, "obj.o"), 86 | "-T", 87 | os.path.join(td, "linker.ld"), 88 | "-o", 89 | os.path.join(td, "obj_linked.o"), 90 | ] 91 | subprocess.run(args, check=True, capture_output=True) 92 | except subprocess.CalledProcessError as e: 93 | logger.error(e.stderr.decode("utf-8")) 94 | raise e 95 | 96 | # extract compiled code 97 | ld = cle.Loader( 98 | os.path.join(td, "obj_linked.o"), main_opts={"base_addr": 0x0} 99 | ) 100 | 101 | patcherex2_section = next( 102 | (s for s in ld.main_object.sections if s.name == ".patcherex2"), None 103 | ) 104 | compiled_start = ld.all_objects[0].entry + base 105 | 106 | compiled = ld.memory.load( 107 | compiled_start, 108 | patcherex2_section.memsize - compiled_start, 109 | ) 110 | return compiled 111 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/llvm_recomp_arm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from .llvm_recomp import LLVMRecomp 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class LLVMRecompArm(LLVMRecomp): 11 | def compile( 12 | self, 13 | code: str, 14 | base=0, 15 | symbols: dict[str, int] | None = None, 16 | extra_compiler_flags: list[str] | None = None, 17 | is_thumb=False, 18 | **kwargs, 19 | ) -> bytes: 20 | if symbols is None: 21 | symbols = {} 22 | if extra_compiler_flags is None: 23 | extra_compiler_flags = [] 24 | if is_thumb: 25 | extra_compiler_flags += ["-mthumb"] 26 | else: 27 | extra_compiler_flags += ["-mno-thumb"] 28 | compiled = super().compile( 29 | code, 30 | base=base, 31 | symbols=symbols, 32 | extra_compiler_flags=extra_compiler_flags, 33 | **kwargs, 34 | ) 35 | 36 | # FIXME: damn this is too hacky 37 | _symbols = {} 38 | _symbols.update(self.p.symbols) 39 | _symbols.update(self.p.binary_analyzer.get_all_symbols()) 40 | _symbols.update(symbols) 41 | symbols = _symbols 42 | disasm = self.p.disassembler.disassemble(compiled, base=base, is_thumb=is_thumb) 43 | reassembled = b"" 44 | for instr in disasm: 45 | if ( 46 | is_thumb 47 | and instr["mnemonic"] == "bl" 48 | and int(instr["op_str"][1:], 0) in symbols.values() 49 | ): 50 | disasm_str = ( 51 | self.p.disassembler.to_asm_string(instr).replace("bl", "blx") + "\n" 52 | ) 53 | reassembled += self.p.assembler.assemble( 54 | disasm_str, base=instr["address"], is_thumb=is_thumb 55 | ) 56 | elif ( 57 | is_thumb 58 | and instr["mnemonic"] == "blx" 59 | and (int(instr["op_str"][1:], 0) + 1) in symbols.values() 60 | ): 61 | disasm_str = ( 62 | self.p.disassembler.to_asm_string(instr).replace("blx", "bl") + "\n" 63 | ) 64 | reassembled += self.p.assembler.assemble( 65 | disasm_str, base=instr["address"], is_thumb=is_thumb 66 | ) 67 | elif ( 68 | not is_thumb 69 | and instr["mnemonic"] == "bl" 70 | and (int(instr["op_str"][1:], 0) + 1) in symbols.values() 71 | ): 72 | disasm_str = ( 73 | self.p.disassembler.to_asm_string(instr).replace("bl", "blx") + "\n" 74 | ) 75 | reassembled += self.p.assembler.assemble( 76 | disasm_str, base=instr["address"], is_thumb=is_thumb 77 | ) 78 | elif ( 79 | not is_thumb 80 | and instr["mnemonic"] == "blx" 81 | and int(instr["op_str"][1:], 0) in symbols.values() 82 | ): 83 | disasm_str = ( 84 | self.p.disassembler.to_asm_string(instr).replace("blx", "bl") + "\n" 85 | ) 86 | reassembled += self.p.assembler.assemble( 87 | disasm_str, base=instr["address"], is_thumb=is_thumb 88 | ) 89 | else: 90 | reassembled += compiled[ 91 | instr["address"] - base : instr["address"] - base + instr["size"] 92 | ] 93 | compiled = reassembled + compiled[len(reassembled) :] 94 | if len(compiled) % 2 != 0: 95 | compiled += b"\x00" 96 | return compiled 97 | -------------------------------------------------------------------------------- /src/patcherex2/components/compilers/ppc_vle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from ..assets.assets import Assets 5 | from .compiler import Compiler 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class PpcVle(Compiler): 11 | def __init__(self, p) -> None: 12 | super().__init__(p) 13 | self.assets_path = Assets("ppc_vle").path 14 | self._compiler = os.path.join(self.assets_path, "powerpc-eabivle-gcc") 15 | self._linker = os.path.join(self.assets_path, "powerpc-eabivle-ld") 16 | self._compiler_flags = ["-mno-vle"] 17 | -------------------------------------------------------------------------------- /src/patcherex2/components/disassemblers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/disassemblers/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/components/disassemblers/capstone.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import capstone 4 | 5 | from .disassembler import Disassembler 6 | 7 | 8 | class Capstone(Disassembler): 9 | def __init__(self, arch: int, mode: int) -> None: 10 | self.cs = capstone.Cs(arch, mode) 11 | 12 | def disassemble(self, input: bytes, base=0, **kwargs) -> list[dict[str, int | str]]: 13 | cs_insns = self.cs.disasm(input, base) 14 | result = [] 15 | for insn in cs_insns: 16 | result.append( 17 | { 18 | "address": insn.address, 19 | "size": insn.size, 20 | "mnemonic": insn.mnemonic, 21 | "op_str": insn.op_str, 22 | } 23 | ) 24 | return result 25 | -------------------------------------------------------------------------------- /src/patcherex2/components/disassemblers/capstone_arm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import capstone 4 | 5 | from .disassembler import Disassembler 6 | 7 | 8 | class CapstoneArm(Disassembler): 9 | def __init__(self, p) -> None: 10 | super().__init__(p) 11 | self.cs_arm = capstone.Cs( 12 | capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM + capstone.CS_MODE_LITTLE_ENDIAN 13 | ) 14 | self.cs_thumb = capstone.Cs( 15 | capstone.CS_ARCH_ARM, 16 | capstone.CS_MODE_THUMB + capstone.CS_MODE_LITTLE_ENDIAN, 17 | ) 18 | 19 | def disassemble( 20 | self, input: bytes, base=0, is_thumb=False, **kwargs 21 | ) -> list[dict[str, int | str]]: 22 | cs = self.cs_thumb if is_thumb else self.cs_arm 23 | cs_insns = cs.disasm(input, base) 24 | result = [] 25 | for insn in cs_insns: 26 | result.append( 27 | { 28 | "address": insn.address, 29 | "size": insn.size, 30 | "mnemonic": insn.mnemonic, 31 | "op_str": insn.op_str, 32 | } 33 | ) 34 | return result 35 | -------------------------------------------------------------------------------- /src/patcherex2/components/disassemblers/disassembler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Disassembler: 5 | def __init__(self, p) -> None: 6 | self.p = p 7 | 8 | def disassemble(self, input: bytes, base=0, **kwargs) -> None: 9 | raise NotImplementedError() 10 | 11 | def to_asm_string(self, insn: dict[str, int | str]) -> str: 12 | return "{} {}".format(insn["mnemonic"], insn["op_str"]) 13 | -------------------------------------------------------------------------------- /src/patcherex2/components/disassemblers/ppc_vle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import re 6 | import subprocess 7 | import tempfile 8 | 9 | from ..assets.assets import Assets 10 | from .disassembler import Disassembler 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class PpcVle(Disassembler): 16 | def __init__(self, p) -> None: 17 | self.p = p 18 | self.assets_path = Assets("ppc_vle").path 19 | 20 | def disassemble(self, input: bytes, base=0, **kwargs) -> list[dict[str, str | int]]: 21 | if isinstance(input, str): 22 | input = bytes(map(ord, input)) 23 | with tempfile.TemporaryDirectory() as td: 24 | with open(os.path.join(td, "code.bin"), "wb") as f: 25 | f.write(input) 26 | 27 | try: 28 | proc = subprocess.run( 29 | f"{os.path.join(self.assets_path, 'powerpc-eabivle-objdump')} -D -b binary --adjust-vma={hex(base)} -m powerpc:common -EB {os.path.join(td, 'code')}.bin | tail +8", 30 | shell=True, 31 | check=True, 32 | capture_output=True, 33 | ) 34 | str_result = proc.stdout.decode("utf-8") 35 | except subprocess.CalledProcessError as e: 36 | logger.error(e.stderr.decode("utf-8")) 37 | raise e 38 | 39 | result = [] 40 | for line in str_result.splitlines(): 41 | m = re.match( 42 | r"\s+(?P
[0-9a-f]+):\s+(?P([0-9a-f]{2}\s)+)\s+(?P.+?)\s+(?P.+?)$", 43 | line, 44 | ) 45 | if m: 46 | instr = m.groupdict() 47 | result.append( 48 | { 49 | "address": int(instr["address"], 16), 50 | "size": len(bytes.fromhex(instr["bytes"])), 51 | "mnemonic": re.sub(r"\s+", "", instr["mnemonic"]), 52 | "op_str": re.sub( 53 | r"\s+", "", instr["op_str"].split(";")[0] 54 | ).replace(",", ", "), 55 | } 56 | ) 57 | return result 58 | -------------------------------------------------------------------------------- /src/patcherex2/components/patch_managers/__init__.py: -------------------------------------------------------------------------------- 1 | from .builtin import BuiltIn 2 | from .imp import Imp 3 | from .patch_manager import PatchManager 4 | 5 | __all__ = ["BuiltIn", "Imp", "PatchManager"] 6 | -------------------------------------------------------------------------------- /src/patcherex2/components/patch_managers/builtin.py: -------------------------------------------------------------------------------- 1 | from .patch_manager import PatchManager 2 | 3 | 4 | class BuiltIn(PatchManager): 5 | pass 6 | -------------------------------------------------------------------------------- /src/patcherex2/components/patch_managers/imp.py: -------------------------------------------------------------------------------- 1 | from .patch_manager import PatchManager 2 | 3 | 4 | class Imp(PatchManager): 5 | pass 6 | -------------------------------------------------------------------------------- /src/patcherex2/components/patch_managers/patch_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from patcherex2.patches import Patch 4 | 5 | 6 | class PatchManager: 7 | def __init__(self) -> None: 8 | self.patches = [] 9 | self.analyzed = False 10 | 11 | def add_patch(self, patch: Patch) -> None: 12 | self.analyzed = False 13 | self.patches.append(patch) 14 | 15 | def add_patches(self, patches: list[Patch]) -> None: 16 | for patch in patches: 17 | self.add_patch(patch) 18 | 19 | def export_patches(self, filename: str) -> None: 20 | raise NotImplementedError() 21 | 22 | def import_patches(self, filename: str) -> None: 23 | raise NotImplementedError() 24 | 25 | def analyze_patches(self, ignore_conflicts=False) -> None: 26 | raise NotImplementedError() 27 | 28 | def apply_patches(self, best_effort=False) -> None: 29 | raise NotImplementedError() 30 | -------------------------------------------------------------------------------- /src/patcherex2/components/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/src/patcherex2/components/utils/__init__.py -------------------------------------------------------------------------------- /src/patcherex2/patcherex.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F403, F405 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from .components.binary_analyzers.ghidra import Ghidra 7 | from .patches import * 8 | from .patches import __all__ as all_patches 9 | from .targets import Target 10 | 11 | logging.Logger.manager.loggerDict["patcherex"] = logging.Logger.manager.loggerDict[ 12 | "patcherex2" 13 | ] 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class Patcherex: 18 | """ 19 | The main class of the library. This is how you are intended to interact with patches. 20 | """ 21 | 22 | def __init__( 23 | self, 24 | binary_path: str, 25 | target_cls: type[Target] | None = None, 26 | target_opts: dict[str, str] | None = None, 27 | components_opts: dict[str, dict[str, str]] | None = None, 28 | ) -> None: 29 | """ 30 | Constructor. 31 | 32 | :param binary_path: The path of the binary to patch. 33 | :param target_cls: Specified architecture class to use, otherwise it is automatically detected, defaults to None 34 | :param target_opts: Options to specify components for the target, defaults to None 35 | :param components_opts: Options for configuring each component for the target, defaults to None 36 | """ 37 | if target_opts is None: 38 | target_opts = {} 39 | if components_opts is None: 40 | components_opts = {} 41 | self.binary_path = binary_path 42 | if target_cls is None: 43 | self.target = Target.detect_target(self, binary_path) 44 | else: 45 | self.target = target_cls(self, binary_path) 46 | 47 | self.symbols = {} 48 | self.sypy_info = {"patcherex_added_functions": []} 49 | self.patches = [] 50 | 51 | # Initialize components 52 | components = [ 53 | "assembler", 54 | "disassembler", 55 | "compiler", 56 | "binary_analyzer", 57 | "allocation_manager", 58 | "binfmt_tool", 59 | "utils", 60 | "archinfo", 61 | ] 62 | for component in components: 63 | setattr( 64 | self, 65 | component, 66 | self.target.get_component( 67 | component, 68 | target_opts.get(component), 69 | components_opts.get(component), 70 | ), 71 | ) 72 | 73 | # Chosen patch order, making sure all are accounted for 74 | self.patch_order = ( 75 | ModifyRawBytesPatch, 76 | RemoveDataPatch, 77 | InsertDataPatch, 78 | ModifyDataPatch, 79 | RemoveLabelPatch, 80 | ModifyLabelPatch, 81 | InsertLabelPatch, 82 | RemoveInstructionPatch, 83 | InsertInstructionPatch, 84 | ModifyInstructionPatch, 85 | RemoveFunctionPatch, 86 | InsertFunctionPatch, 87 | ModifyFunctionPatch, 88 | ) 89 | assert len(self.patch_order) == len(all_patches) 90 | 91 | def shutdown(self): 92 | """ 93 | Shuts down any resources used by Patcherex2. 94 | This needs to be called when using Ghidra as the binary analyzer when done patching. 95 | """ 96 | if isinstance(self.binary_analyzer, Ghidra): 97 | self.binary_analyzer.shutdown() 98 | 99 | def apply_patches(self) -> None: 100 | """ 101 | Applies all added patches to the binary. Call this when you have added all the patches you want. 102 | """ 103 | # TODO: sort patches properly 104 | # self.patches.sort(key=lambda x: self.patch_order.index(type(x))) 105 | self.patches.sort( 106 | key=lambda x: not isinstance( 107 | x, (ModifyDataPatch, InsertDataPatch, RemoveDataPatch) 108 | ) 109 | ) 110 | logger.debug(f"Applying patches: {self.patches}") 111 | for patch in self.patches: 112 | patch.apply(self) 113 | self.binfmt_tool.finalize() 114 | 115 | def save_binary(self, filename: str = None) -> None: 116 | """ 117 | Save the patched binary to a file. 118 | 119 | :param filename: Name of file to save to, defaults to '.patched' 120 | """ 121 | self.binfmt_tool.save_binary(filename) 122 | -------------------------------------------------------------------------------- /src/patcherex2/patches/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_patches import InsertDataPatch, ModifyDataPatch, RemoveDataPatch 2 | from .dummy_patches import InsertLabelPatch, ModifyLabelPatch, RemoveLabelPatch 3 | from .function_patches import ( 4 | InsertFunctionPatch, 5 | ModifyFunctionPatch, 6 | RemoveFunctionPatch, 7 | ) 8 | from .instruction_patches import ( 9 | InsertInstructionPatch, 10 | ModifyInstructionPatch, 11 | RemoveInstructionPatch, 12 | ) 13 | from .patch import Patch 14 | from .raw_patches import ModifyRawBytesPatch 15 | 16 | __all__ = [ 17 | "ModifyDataPatch", 18 | "InsertDataPatch", 19 | "RemoveDataPatch", 20 | "InsertLabelPatch", 21 | "ModifyLabelPatch", 22 | "RemoveLabelPatch", 23 | "ModifyFunctionPatch", 24 | "InsertFunctionPatch", 25 | "RemoveFunctionPatch", 26 | "ModifyInstructionPatch", 27 | "InsertInstructionPatch", 28 | "RemoveInstructionPatch", 29 | "ModifyRawBytesPatch", 30 | ] 31 | 32 | 33 | # Other Patches 34 | class ModifyEntryPointPatch(Patch): 35 | def __init__(self, addr: int, parent=None) -> None: 36 | self.addr = addr 37 | super().__init__(parent) 38 | 39 | 40 | # Complex Patches 41 | class InsertFunctionWrapperPatch(Patch): 42 | def __init__(self, addr: int, wrapper_code: str, parent=None) -> None: 43 | self.addr = addr 44 | self.wrapper_code = wrapper_code 45 | super().__init__(parent) 46 | -------------------------------------------------------------------------------- /src/patcherex2/patches/data_patches.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains patches that modify the binary at the data level. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import TYPE_CHECKING 8 | 9 | from ..components.allocation_managers.allocation_manager import MemoryFlag 10 | from .patch import Patch 11 | from .raw_patches import ModifyRawBytesPatch 12 | 13 | if TYPE_CHECKING: 14 | from ..patcherex import Patcherex 15 | 16 | 17 | class ModifyDataPatch(ModifyRawBytesPatch): 18 | """ 19 | Extends ModifyRawBytesPatch to only be used for memory addresses, used for modifying data in binary. 20 | """ 21 | 22 | def __init__(self, addr: int, new_bytes: bytes) -> None: 23 | """ 24 | Same as ModifyRawBytesPatch constructor, but address type of memory is assumed. 25 | """ 26 | super().__init__(addr, new_bytes, addr_type="mem") 27 | 28 | 29 | class InsertDataPatch(Patch): 30 | """ 31 | Patch that inserts data into the binary. 32 | """ 33 | 34 | def __init__(self, addr_or_name: int | str, data: bytes) -> None: 35 | """ 36 | Constructor. 37 | 38 | :param addr_or_name: If an integer, data is inserted at the address. 39 | If a string, it is placed in a free spot in the binary and added as a symbol (with this as its name). 40 | :param data: New data to place in binary. 41 | """ 42 | self.addr = None 43 | self.name = None 44 | if isinstance(addr_or_name, int): 45 | self.addr = addr_or_name 46 | elif isinstance(addr_or_name, str): 47 | self.name = addr_or_name 48 | self.data = data 49 | 50 | def apply(self, p: Patcherex) -> None: 51 | """ 52 | Applies the patch to the binary, intended to be called by a Patcherex instance. 53 | 54 | :param p: Patcherex instance. 55 | """ 56 | if self.addr: 57 | p.binfmt_tool.update_binary_content(self.addr, self.data) 58 | elif self.name: 59 | block = p.allocation_manager.allocate(len(self.data), flag=MemoryFlag.RW) 60 | p.symbols[self.name] = block.mem_addr 61 | p.binfmt_tool.update_binary_content(block.file_addr, self.data) 62 | 63 | 64 | class RemoveDataPatch(ModifyRawBytesPatch): 65 | """ 66 | Extends ModifyRawBytesPatch for removing data in the binary (fills it with null bytes starting at address given). 67 | Expects a memory address. 68 | """ 69 | 70 | def __init__(self, addr: int, size: int) -> None: 71 | """ 72 | Same as ModifyRawBytes Patch constructor, but adds size parameter and assumes memory address. 73 | 74 | :param size: The number of bytes to remove. 75 | """ 76 | super().__init__(addr, b"\x00" * size, addr_type="mem") 77 | -------------------------------------------------------------------------------- /src/patcherex2/patches/dummy_patches.py: -------------------------------------------------------------------------------- 1 | from .patch import Patch 2 | 3 | 4 | class InsertLabelPatch(Patch): 5 | def __init__(self, addr: int) -> None: 6 | self.addr = addr 7 | raise NotImplementedError() 8 | 9 | 10 | class ModifyLabelPatch(Patch): 11 | def __init__(self, addr: int) -> None: 12 | self.addr = addr 13 | raise NotImplementedError() 14 | 15 | 16 | class RemoveLabelPatch(Patch): 17 | def __init__(self, addr: int) -> None: 18 | self.addr = addr 19 | raise NotImplementedError() 20 | -------------------------------------------------------------------------------- /src/patcherex2/patches/patch.py: -------------------------------------------------------------------------------- 1 | class Patch: 2 | """ 3 | Base class for patches. Not instantiated direactly. 4 | """ 5 | 6 | def __init__(self, parent=None) -> None: 7 | self.parent = parent 8 | 9 | def apply(self, p): 10 | raise NotImplementedError() 11 | -------------------------------------------------------------------------------- /src/patcherex2/patches/raw_patches.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains patches that modify the binary at the byte level. 3 | """ 4 | 5 | import logging 6 | 7 | from .patch import Patch 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ModifyRawBytesPatch(Patch): 13 | """ 14 | Patch that modifies bytes of the binary. 15 | """ 16 | 17 | def __init__(self, addr: int, new_bytes: bytes, addr_type="mem") -> None: 18 | """ 19 | Constructor. 20 | 21 | :param addr: Starting address of bytes you want to change. 22 | :param new_bytes: New bytes to replace original ones. 23 | :param addr_type: The type of address given, "mem" (memory address) or "raw" (file address), defaults to "mem" 24 | """ 25 | self.addr = addr 26 | self.new_bytes = new_bytes 27 | self.addr_type = addr_type 28 | 29 | def apply(self, p) -> None: 30 | """ 31 | Applies the patch to the binary, intended to be called by a Patcherex instance. 32 | 33 | :param p: Patcherex instance. 34 | :raises NotImplementedError: Raised if an address type other than "raw" or "mem" is specified. 35 | """ 36 | if self.addr_type == "raw": 37 | offset = self.addr 38 | elif self.addr_type == "mem": 39 | offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr) 40 | if not offset: 41 | logger.warning( 42 | "failed to convert mem addr to file offset, will just default to raw addr" 43 | ) 44 | offset = self.addr 45 | else: 46 | raise NotImplementedError() 47 | p.binfmt_tool.update_binary_content(offset, self.new_bytes) 48 | -------------------------------------------------------------------------------- /src/patcherex2/targets/__init__.py: -------------------------------------------------------------------------------- 1 | from .bin_arm_bare import BinArmBare 2 | from .elf_aarch64_linux import ElfAArch64Linux 3 | from .elf_amd64_linux import ElfAmd64Linux 4 | from .elf_amd64_linux_recomp import ElfAmd64LinuxRecomp 5 | from .elf_arm_bare import ElfArmBare 6 | from .elf_arm_linux import ElfArmLinux 7 | from .elf_arm_linux_recomp import ElfArmLinuxRecomp 8 | from .elf_arm_mimxrt1052 import ElfArmMimxrt1052 9 | from .elf_leon3_bare import ElfLeon3Bare 10 | from .elf_mips64_linux import ElfMips64Linux 11 | from .elf_mips64el_linux import ElfMips64elLinux 12 | from .elf_mips_linux import ElfMipsLinux 13 | from .elf_mipsel_linux import ElfMipselLinux 14 | from .elf_ppc64_linux import ElfPpc64Linux 15 | from .elf_ppc64le_linux import ElfPpc64leLinux 16 | from .elf_ppc_linux import ElfPpcLinux 17 | from .elf_s390x_linux import ElfS390xLinux 18 | from .elf_x86_linux import ElfX86Linux 19 | from .ihex_ppc_bare import IHexPPCBare 20 | from .target import Target 21 | 22 | __all__ = [ 23 | "BinArmBare", 24 | "ElfAArch64Linux", 25 | "ElfAmd64Linux", 26 | "ElfAmd64LinuxRecomp", 27 | "ElfArmBare", 28 | "ElfArmLinux", 29 | "ElfArmLinuxRecomp", 30 | "ElfArmMimxrt1052", 31 | "ElfLeon3Bare", 32 | "ElfMips64Linux", 33 | "ElfMips64elLinux", 34 | "ElfMipsLinux", 35 | "ElfMipselLinux", 36 | "ElfPpc64Linux", 37 | "ElfPpc64leLinux", 38 | "ElfPpcLinux", 39 | "ElfS390xLinux", 40 | "ElfX86Linux", 41 | "IHexPPCBare", 42 | "Target", 43 | ] 44 | -------------------------------------------------------------------------------- /src/patcherex2/targets/bin_arm_bare.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ..components.allocation_managers.allocation_manager import AllocationManager 4 | from ..components.archinfo.arm import ArmInfo 5 | from ..components.assemblers.keystone_arm import KeystoneArm 6 | from ..components.binary_analyzers.angr import Angr 7 | from ..components.binfmt_tools.binary import Binary 8 | from ..components.compilers.clang_arm import ClangArm 9 | from ..components.disassemblers.capstone_arm import CapstoneArm 10 | from ..components.utils.utils import Utils 11 | from .target import Target 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class BinArmBare(Target): 17 | @staticmethod 18 | def detect_target(binary_path): 19 | return False 20 | 21 | def get_assembler(self, assembler): 22 | assembler = assembler or "keystone" 23 | if assembler == "keystone": 24 | return KeystoneArm(self.p) 25 | raise NotImplementedError() 26 | 27 | def get_allocation_manager(self, allocation_manager): 28 | allocation_manager = allocation_manager or "default" 29 | if allocation_manager == "default": 30 | return AllocationManager(self.p) 31 | raise NotImplementedError() 32 | 33 | def get_compiler(self, compiler): 34 | compiler = compiler or "clang" 35 | if compiler == "clang": 36 | return ClangArm(self.p, compiler_flags=["-target", "arm-linux-gnueabihf"]) 37 | elif compiler == "clang19": 38 | return ClangArm( 39 | self.p, 40 | compiler_flags=["-target", "arm-linux-gnueabihf"], 41 | clang_version=19, 42 | ) 43 | raise NotImplementedError() 44 | 45 | def get_disassembler(self, disassembler): 46 | disassembler = disassembler or "capstone" 47 | if disassembler == "capstone": 48 | return CapstoneArm(self.p) 49 | raise NotImplementedError() 50 | 51 | def get_binfmt_tool(self, binfmt_tool): 52 | binfmt_tool = binfmt_tool or "default" 53 | if binfmt_tool == "default": 54 | return Binary(self.p, self.binary_path) 55 | raise NotImplementedError() 56 | 57 | def get_binary_analyzer(self, binary_analyzer): 58 | binary_analyzer = binary_analyzer or "angr" 59 | if binary_analyzer == "angr": 60 | return Angr( 61 | self.binary_path, 62 | angr_kwargs={ 63 | "arch": "ARMEL", 64 | "auto_load_libs": False, 65 | }, 66 | angr_cfg_kwargs={ 67 | "normalize": True, 68 | "data_references": True, 69 | }, 70 | ) 71 | raise NotImplementedError() 72 | 73 | def get_utils(self, utils): 74 | utils = utils or "default" 75 | if utils == "default": 76 | return Utils(self.p, self.binary_path) 77 | raise NotImplementedError() 78 | 79 | def get_archinfo(self, archinfo): 80 | archinfo = archinfo or "default" 81 | if archinfo == "default": 82 | return ArmInfo() 83 | raise NotImplementedError() 84 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_aarch64_linux.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ..components.allocation_managers.allocation_manager import AllocationManager 4 | from ..components.archinfo.aarch64 import Aarch64Info 5 | from ..components.assemblers.keystone import Keystone, keystone 6 | from ..components.binary_analyzers.angr import Angr 7 | from ..components.binary_analyzers.ghidra import Ghidra 8 | from ..components.binfmt_tools.elf import ELF 9 | from ..components.compilers.clang import Clang 10 | from ..components.disassemblers.capstone import Capstone, capstone 11 | from ..components.utils.utils import Utils 12 | from .target import Target 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class ElfAArch64Linux(Target): 18 | @staticmethod 19 | def detect_target(binary_path): 20 | with open(binary_path, "rb") as f: 21 | magic = f.read(0x14) 22 | if magic.startswith(b"\x7fELF") and magic.startswith( 23 | b"\xb7\x00", 0x12 24 | ): # EM_AARCH64 25 | return True 26 | return False 27 | 28 | def get_assembler(self, assembler): 29 | assembler = assembler or "keystone" 30 | if assembler == "keystone": 31 | return Keystone( 32 | self.p, keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN 33 | ) 34 | raise NotImplementedError() 35 | 36 | def get_allocation_manager(self, allocation_manager): 37 | allocation_manager = allocation_manager or "default" 38 | if allocation_manager == "default": 39 | return AllocationManager(self.p) 40 | raise NotImplementedError() 41 | 42 | def get_compiler(self, compiler): 43 | compiler = compiler or "clang" 44 | if compiler == "clang": 45 | return Clang(self.p, compiler_flags=["-target", "aarch64-linux-gnu"]) 46 | elif compiler == "clang19": 47 | return Clang( 48 | self.p, 49 | compiler_flags=["-target", "aarch64-linux-gnu"], 50 | clang_version=19, 51 | ) 52 | raise NotImplementedError() 53 | 54 | def get_disassembler(self, disassembler): 55 | disassembler = disassembler or "capstone" 56 | if disassembler == "capstone": 57 | return Capstone(capstone.CS_ARCH_ARM64, capstone.CS_MODE_LITTLE_ENDIAN) 58 | raise NotImplementedError() 59 | 60 | def get_binfmt_tool(self, binfmt_tool): 61 | binfmt_tool = binfmt_tool or "pyelftools" 62 | if binfmt_tool == "pyelftools": 63 | return ELF(self.p, self.binary_path) 64 | raise NotImplementedError() 65 | 66 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 67 | binary_analyzer = binary_analyzer or "angr" 68 | if binary_analyzer == "angr": 69 | return Angr(self.binary_path, **kwargs) 70 | if binary_analyzer == "ghidra": 71 | return Ghidra(self.binary_path, **kwargs) 72 | raise NotImplementedError() 73 | 74 | def get_utils(self, utils): 75 | utils = utils or "default" 76 | if utils == "default": 77 | return Utils(self.p, self.binary_path) 78 | raise NotImplementedError() 79 | 80 | def get_archinfo(self, archinfo): 81 | archinfo = archinfo or "default" 82 | if archinfo == "default": 83 | return Aarch64Info() 84 | raise NotImplementedError() 85 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_amd64_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.amd64 import Amd64Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfAmd64Linux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x3e\x00", 0x12 20 | ): # EM_X86_64 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_X86, 30 | keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_64, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p) 44 | elif compiler == "clang19": 45 | return Clang(self.p, clang_version=19) 46 | raise NotImplementedError() 47 | 48 | def get_disassembler(self, disassembler): 49 | disassembler = disassembler or "capstone" 50 | if disassembler == "capstone": 51 | return Capstone( 52 | capstone.CS_ARCH_X86, 53 | capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_64, 54 | ) 55 | raise NotImplementedError() 56 | 57 | def get_binfmt_tool(self, binfmt_tool): 58 | binfmt_tool = binfmt_tool or "pyelftools" 59 | if binfmt_tool == "pyelftools": 60 | return ELF(self.p, self.binary_path) 61 | raise NotImplementedError() 62 | 63 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 64 | binary_analyzer = binary_analyzer or "angr" 65 | if binary_analyzer == "angr": 66 | return Angr(self.binary_path, **kwargs) 67 | if binary_analyzer == "ghidra": 68 | return Ghidra(self.binary_path, **kwargs) 69 | raise NotImplementedError() 70 | 71 | def get_utils(self, utils): 72 | utils = utils or "default" 73 | if utils == "default": 74 | return Utils(self.p, self.binary_path) 75 | raise NotImplementedError() 76 | 77 | def get_archinfo(self, archinfo): 78 | archinfo = archinfo or "default" 79 | if archinfo == "default": 80 | return Amd64Info() 81 | raise NotImplementedError() 82 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_amd64_linux_recomp.py: -------------------------------------------------------------------------------- 1 | from ..components.binary_analyzers.angr import Angr 2 | from ..components.binary_analyzers.ghidra import Ghidra 3 | from ..components.compilers.llvm_recomp import LLVMRecomp 4 | from .elf_amd64_linux import ElfAmd64Linux 5 | 6 | 7 | class ElfAmd64LinuxRecomp(ElfAmd64Linux): 8 | @staticmethod 9 | def detect_target(binary_path): 10 | return False 11 | 12 | def get_compiler(self, compiler): 13 | compiler = compiler or "llvm_recomp" 14 | if compiler == "llvm_recomp": 15 | return LLVMRecomp(self.p) 16 | raise NotImplementedError() 17 | 18 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 19 | binary_analyzer = binary_analyzer or "angr" 20 | if binary_analyzer == "angr": 21 | return Angr(self.binary_path, **kwargs) 22 | if binary_analyzer == "ghidra": 23 | return Ghidra(self.binary_path, **kwargs) 24 | raise NotImplementedError() 25 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_arm_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.arm import ArmInfo 3 | from ..components.assemblers.keystone_arm import KeystoneArm 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang_arm import ClangArm 8 | from ..components.disassemblers.capstone_arm import CapstoneArm 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfArmLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x28\x00", 0x12 20 | ): # EM_ARM 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return KeystoneArm(self.p) 28 | raise NotImplementedError() 29 | 30 | def get_allocation_manager(self, allocation_manager): 31 | allocation_manager = allocation_manager or "default" 32 | if allocation_manager == "default": 33 | return AllocationManager(self.p) 34 | raise NotImplementedError() 35 | 36 | def get_compiler(self, compiler): 37 | compiler = compiler or "clang" 38 | if compiler == "clang": 39 | return ClangArm(self.p, compiler_flags=["-target", "arm-linux-gnueabihf"]) 40 | elif compiler == "clang19": 41 | return ClangArm( 42 | self.p, 43 | compiler_flags=["-target", "arm-linux-gnueabihf"], 44 | clang_version=19, 45 | ) 46 | raise NotImplementedError() 47 | 48 | def get_disassembler(self, disassembler): 49 | disassembler = disassembler or "capstone" 50 | if disassembler == "capstone": 51 | return CapstoneArm(self.p) 52 | raise NotImplementedError() 53 | 54 | def get_binfmt_tool(self, binfmt_tool): 55 | binfmt_tool = binfmt_tool or "pyelftools" 56 | if binfmt_tool == "pyelftools": 57 | return ELF(self.p, self.binary_path) 58 | raise NotImplementedError() 59 | 60 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 61 | binary_analyzer = binary_analyzer or "angr" 62 | if binary_analyzer == "angr": 63 | return Angr(self.binary_path, **kwargs) 64 | if binary_analyzer == "ghidra": 65 | return Ghidra(self.binary_path, **kwargs) 66 | raise NotImplementedError() 67 | 68 | def get_utils(self, utils): 69 | utils = utils or "default" 70 | if utils == "default": 71 | return Utils(self.p, self.binary_path) 72 | raise NotImplementedError() 73 | 74 | def get_archinfo(self, archinfo): 75 | archinfo = archinfo or "default" 76 | if archinfo == "default": 77 | return ArmInfo() 78 | raise NotImplementedError() 79 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_arm_linux_recomp.py: -------------------------------------------------------------------------------- 1 | from ..components.binary_analyzers.angr import Angr 2 | from ..components.binary_analyzers.ghidra import Ghidra 3 | from ..components.compilers.llvm_recomp_arm import LLVMRecompArm 4 | from .elf_arm_linux import ElfArmLinux 5 | 6 | 7 | class ElfArmLinuxRecomp(ElfArmLinux): 8 | @staticmethod 9 | def detect_target(binary_path): 10 | return False 11 | 12 | def get_compiler(self, compiler): 13 | compiler = compiler or "llvm_recomp" 14 | if compiler == "llvm_recomp": 15 | return LLVMRecompArm(self.p) 16 | raise NotImplementedError() 17 | 18 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 19 | binary_analyzer = binary_analyzer or "angr" 20 | if binary_analyzer == "angr": 21 | return Angr(self.binary_path, **kwargs) 22 | if binary_analyzer == "ghidra": 23 | return Ghidra(self.binary_path, **kwargs) 24 | raise NotImplementedError() 25 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_arm_mimxrt1052.py: -------------------------------------------------------------------------------- 1 | from elftools.construct.lib import Container 2 | 3 | from ..components.allocation_managers.allocation_manager import ( 4 | FileBlock, 5 | MemoryBlock, 6 | ) 7 | from ..components.binfmt_tools.elf import ELF 8 | from .elf_arm_linux import ElfArmLinux 9 | 10 | 11 | class CustomElf(ELF): 12 | def _init_memory_analysis(self): 13 | """ 14 | Information from NXP's MCUXpresso IDE: 15 | Flash is code, RAM4 is where data being loaded to 16 | 17 | * For additional code, we can just put them in the free flash space 18 | * For additional data, we also put them in the free flash space, but 19 | we need to update ResetISR to copy them to RAM4 20 | 21 | * The flasher (LinkServer) seems only care about segment headers, so 22 | we can safely ignore the section headers. 23 | 24 | * The IDE will strip the binary then call the flasher, and strip will 25 | remove the segment we added, so we need to add a fake corresponding 26 | section to make sure the segment is not removed. 27 | 28 | Type | Name | Alias | Location | Size 29 | -------|---------------|-------|------------|---------- 30 | Flash | BOARD_FLASH | Flash | 0x60000000 | 0x4000000 31 | RAM | SRAM_DTC | RAM | 0x20000000 | 0x20000 32 | RAM | SRAM_ITC | RAM2 | 0x0 | 0x20000 33 | RAM | SRAM_OC | RAM3 | 0x20200000 | 0x20000 34 | RAM | BOARD_SDRAM | RAM4 | 0x80000000 | 0x1e00000 35 | RAM | NCACHE_REGION | RAM5 | 0x81e00000 | 0x200000 36 | """ 37 | 38 | # add free flash space to allocation manager 39 | flash_start = 0x60000000 40 | flash_end = 0x64000000 41 | highest_flash_addr = 0x60000000 42 | highest_file_offset = 0 43 | for segment in self._segments: 44 | seg_start = segment["p_paddr"] 45 | seg_end = segment["p_paddr"] + segment["p_memsz"] 46 | if ( 47 | flash_start <= seg_start < flash_end 48 | and flash_start <= seg_end < flash_end 49 | and seg_end > highest_flash_addr 50 | ): 51 | highest_flash_addr = seg_end 52 | 53 | if segment["p_offset"] + segment["p_filesz"] > highest_file_offset: 54 | highest_file_offset = segment["p_offset"] + segment["p_filesz"] 55 | 56 | highest_file_offset = (highest_file_offset + 0xFFFF) & ~0xFFFF 57 | block = FileBlock(highest_file_offset, -1) 58 | self.p.allocation_manager.add_block(block) 59 | block = MemoryBlock(highest_flash_addr, -1) 60 | self.p.allocation_manager.add_block(block) 61 | 62 | return 63 | 64 | def finalize(self): 65 | self.p.allocation_manager.finalize() 66 | if len(self.p.allocation_manager.new_mapped_blocks) == 0: 67 | return 68 | 69 | max_align = max([segment["p_align"] for segment in self._segments] + [0]) 70 | 71 | # create new load segment for each new mapped block 72 | for block in self.p.allocation_manager.new_mapped_blocks: 73 | self._segments.append( 74 | Container( 75 | **{ 76 | "p_type": "PT_LOAD", 77 | "p_offset": block.file_addr, 78 | "p_filesz": block.size, 79 | "p_vaddr": block.mem_addr, 80 | "p_paddr": block.mem_addr, 81 | "p_memsz": block.size, 82 | "p_flags": block.flag, 83 | "p_align": max_align, 84 | } 85 | ) 86 | ) 87 | 88 | self._sections.append( 89 | Container( 90 | **{ 91 | "sh_name": 0, 92 | "sh_type": "SHT_PROGBITS", 93 | "sh_flags": 2, 94 | "sh_addr": block.mem_addr, 95 | "sh_offset": block.file_addr, 96 | "sh_size": block.size, 97 | "sh_link": 0, 98 | "sh_info": 0, 99 | "sh_addralign": max_align, 100 | "sh_entsize": 0, 101 | } 102 | ) 103 | ) 104 | 105 | # sort segments by p_offset 106 | self._segments = sorted(self._segments, key=lambda x: x["p_offset"]) 107 | 108 | # try to merge load segments if they are adjacent and have the same flags and same alignment 109 | # new size = sum of sizes of the two segments + gap between them 110 | while True: 111 | new_segments = [] 112 | i = 0 113 | while i < len(self._segments) - 1: 114 | prev_seg = self._segments[i] 115 | next_seg = self._segments[i + 1] 116 | if ( 117 | prev_seg["p_type"] == next_seg["p_type"] == "PT_LOAD" 118 | and prev_seg["p_offset"] + prev_seg["p_filesz"] 119 | == next_seg["p_offset"] 120 | and prev_seg["p_vaddr"] + prev_seg["p_memsz"] == next_seg["p_vaddr"] 121 | and prev_seg["p_flags"] == next_seg["p_flags"] 122 | and prev_seg["p_align"] == next_seg["p_align"] 123 | ): 124 | new_segments.append( 125 | Container( 126 | **{ 127 | "p_type": "PT_LOAD", 128 | "p_offset": prev_seg["p_offset"], 129 | "p_filesz": prev_seg["p_filesz"] 130 | + next_seg["p_filesz"] 131 | + ( 132 | next_seg["p_offset"] 133 | - (prev_seg["p_offset"] + prev_seg["p_filesz"]) 134 | ), 135 | "p_vaddr": prev_seg["p_vaddr"], 136 | "p_paddr": prev_seg["p_paddr"], 137 | "p_memsz": prev_seg["p_memsz"] 138 | + next_seg["p_memsz"] 139 | + ( 140 | next_seg["p_vaddr"] 141 | - (prev_seg["p_vaddr"] + prev_seg["p_memsz"]) 142 | ), 143 | "p_flags": prev_seg["p_flags"], 144 | "p_align": prev_seg["p_align"], 145 | } 146 | ) 147 | ) 148 | i += 2 149 | else: 150 | new_segments.append(prev_seg) 151 | i += 1 152 | if i == len(self._segments) - 1: 153 | new_segments.append(self._segments[i]) 154 | if new_segments == self._segments: 155 | break 156 | self._segments = new_segments 157 | 158 | # generate new phdr at end of the file and update ehdr 159 | last_seg = sorted(self._segments, key=lambda x: x["p_offset"])[-1] 160 | phdr_start = last_seg["p_offset"] + last_seg["p_filesz"] 161 | new_phdr = b"" 162 | for segment in self._segments: 163 | new_phdr += self._elf.structs.Elf_Phdr.build(segment) 164 | self.p.binfmt_tool.update_binary_content(phdr_start, new_phdr) 165 | 166 | ehdr = self._elf.header 167 | ehdr["e_phnum"] = len(self._segments) 168 | ehdr["e_phoff"] = phdr_start 169 | 170 | # generate new shdr at end of the file and update ehdr 171 | shdr_start = phdr_start + len(new_phdr) 172 | new_shdr = b"" 173 | for section in self._sections: 174 | new_shdr += self._elf.structs.Elf_Shdr.build(section) 175 | self.p.binfmt_tool.update_binary_content(shdr_start, new_shdr) 176 | 177 | ehdr["e_shnum"] = len(self._sections) 178 | ehdr["e_shoff"] = shdr_start 179 | new_ehdr = self._elf.structs.Elf_Ehdr.build(ehdr) 180 | self.p.binfmt_tool.update_binary_content(0, new_ehdr) 181 | 182 | 183 | class ElfArmMimxrt1052(ElfArmLinux): 184 | @staticmethod 185 | def detect_target(binary_path): 186 | return False 187 | 188 | def get_binfmt_tool(self, binfmt_tool): 189 | binfmt_tool = binfmt_tool or "default" 190 | if binfmt_tool == "default": 191 | return CustomElf(self.p, self.binary_path) 192 | raise NotImplementedError() 193 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_leon3_bare.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ..components.allocation_managers.allocation_manager import ( 4 | AllocationManager, 5 | MappedBlock, 6 | MemoryFlag, 7 | ) 8 | from ..components.archinfo.sparc import SparcInfo 9 | from ..components.assemblers.bcc import Bcc as BccAssembler 10 | from ..components.assemblers.keystone_sparc import KeystoneSparc, keystone 11 | from ..components.binary_analyzers.angr import Angr 12 | from ..components.binary_analyzers.ghidra import Ghidra 13 | from ..components.binfmt_tools.elf import ELF 14 | from ..components.compilers.bcc import Bcc as BccCompiler 15 | from ..components.disassemblers.capstone import Capstone, capstone 16 | from ..components.utils.utils import Utils 17 | from .target import Target 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class CustomElf(ELF): 23 | def _init_memory_analysis(self): 24 | # remove all non-RWX segments 25 | self._segments = [s for s in self._segments if s["p_flags"] & 0b111 == 0b111] 26 | block = MappedBlock( 27 | self._segments[0]["p_offset"], 28 | self._segments[0]["p_vaddr"], 29 | self._segments[0]["p_memsz"], 30 | is_free=False, 31 | flag=MemoryFlag.RWX, 32 | ) 33 | self.p.allocation_manager.add_block(block) 34 | 35 | 36 | class ElfLeon3Bare(Target): 37 | @staticmethod 38 | def detect_target(binary_path): 39 | return False 40 | 41 | def get_assembler(self, assembler): 42 | assembler = assembler or "keystone" 43 | if assembler == "keystone": 44 | return KeystoneSparc( 45 | self.p, 46 | keystone.KS_ARCH_SPARC, 47 | keystone.KS_MODE_SPARC32 + keystone.KS_MODE_BIG_ENDIAN, 48 | ) 49 | elif assembler == "bcc": 50 | return BccAssembler(self.p) 51 | raise NotImplementedError() 52 | 53 | def get_allocation_manager(self, allocation_manager): 54 | allocation_manager = allocation_manager or "default" 55 | if allocation_manager == "default": 56 | return AllocationManager(self.p) 57 | raise NotImplementedError() 58 | 59 | def get_compiler(self, compiler): 60 | compiler = compiler or "bcc" 61 | if compiler == "bcc": 62 | return BccCompiler(self.p) 63 | raise NotImplementedError() 64 | 65 | def get_disassembler(self, disassembler): 66 | disassembler = disassembler or "capstone" 67 | if disassembler == "capstone": 68 | return Capstone(capstone.CS_ARCH_SPARC, capstone.CS_MODE_BIG_ENDIAN) 69 | raise NotImplementedError() 70 | 71 | def get_binfmt_tool(self, binfmt_tool): 72 | binfmt_tool = binfmt_tool or "custom" 73 | if binfmt_tool == "custom": 74 | return CustomElf(self.p, self.binary_path) 75 | raise NotImplementedError() 76 | 77 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 78 | binary_analyzer = binary_analyzer or "angr" 79 | if binary_analyzer == "angr": 80 | return Angr(self.binary_path, **kwargs) 81 | if binary_analyzer == "ghidra": 82 | return Ghidra(self.binary_path, **kwargs) 83 | raise NotImplementedError() 84 | 85 | def get_utils(self, utils): 86 | utils = utils or "default" 87 | if utils == "default": 88 | return Utils(self.p, self.binary_path) 89 | raise NotImplementedError() 90 | 91 | def get_archinfo(self, archinfo): 92 | archinfo = archinfo or "default" 93 | if archinfo == "default": 94 | return SparcInfo() 95 | raise NotImplementedError() 96 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_mips64_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.mips64 import Mips64Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfMips64Linux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF\x02") and magic.startswith( 19 | b"\x00\x08", 0x12 20 | ): # EM_MIPS 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_MIPS, 30 | keystone.KS_MODE_BIG_ENDIAN + keystone.KS_MODE_64, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["--target=mips64-linux-gnuabi64"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | return Capstone( 50 | capstone.CS_ARCH_MIPS, 51 | capstone.CS_MODE_BIG_ENDIAN + capstone.CS_MODE_MIPS64, 52 | ) 53 | raise NotImplementedError() 54 | 55 | def get_binfmt_tool(self, binfmt_tool): 56 | binfmt_tool = binfmt_tool or "pyelftools" 57 | if binfmt_tool == "pyelftools": 58 | return ELF(self.p, self.binary_path) 59 | raise NotImplementedError() 60 | 61 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 62 | binary_analyzer = binary_analyzer or "angr" 63 | if binary_analyzer == "angr": 64 | return Angr(self.binary_path, **kwargs) 65 | if binary_analyzer == "ghidra": 66 | return Ghidra(self.binary_path, **kwargs) 67 | raise NotImplementedError() 68 | 69 | def get_utils(self, utils): 70 | utils = utils or "default" 71 | if utils == "default": 72 | return Utils(self.p, self.binary_path) 73 | raise NotImplementedError() 74 | 75 | def get_archinfo(self, archinfo): 76 | archinfo = archinfo or "default" 77 | if archinfo == "default": 78 | return Mips64Info() 79 | raise NotImplementedError() 80 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_mips64el_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.mips64 import Mips64Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfMips64elLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF\x02") and magic.startswith( 19 | b"\x08\x00", 0x12 20 | ): # EM_MIPS 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_MIPS, 30 | keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_64, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["--target=mips64el-linux-gnuabi64"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | return Capstone( 50 | capstone.CS_ARCH_MIPS, 51 | capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_MIPS64, 52 | ) 53 | raise NotImplementedError() 54 | 55 | def get_binfmt_tool(self, binfmt_tool): 56 | binfmt_tool = binfmt_tool or "pyelftools" 57 | if binfmt_tool == "pyelftools": 58 | return ELF(self.p, self.binary_path) 59 | raise NotImplementedError() 60 | 61 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 62 | binary_analyzer = binary_analyzer or "angr" 63 | if binary_analyzer == "angr": 64 | return Angr(self.binary_path, **kwargs) 65 | if binary_analyzer == "ghidra": 66 | return Ghidra(self.binary_path, **kwargs) 67 | raise NotImplementedError() 68 | 69 | def get_utils(self, utils): 70 | utils = utils or "default" 71 | if utils == "default": 72 | return Utils(self.p, self.binary_path) 73 | raise NotImplementedError() 74 | 75 | def get_archinfo(self, archinfo): 76 | archinfo = archinfo or "default" 77 | if archinfo == "default": 78 | return Mips64Info() 79 | raise NotImplementedError() 80 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_mips_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.mips import MipsInfo 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfMipsLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF\x01") and magic.startswith( 19 | b"\x00\x08", 0x12 20 | ): # EM_MIPS 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_MIPS, 30 | keystone.KS_MODE_BIG_ENDIAN + keystone.KS_MODE_MIPS32, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["--target=mips-linux-gnu"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | return Capstone( 50 | capstone.CS_ARCH_MIPS, 51 | capstone.CS_MODE_BIG_ENDIAN + capstone.CS_MODE_MIPS32, 52 | ) 53 | raise NotImplementedError() 54 | 55 | def get_binfmt_tool(self, binfmt_tool): 56 | binfmt_tool = binfmt_tool or "pyelftools" 57 | if binfmt_tool == "pyelftools": 58 | return ELF(self.p, self.binary_path) 59 | raise NotImplementedError() 60 | 61 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 62 | binary_analyzer = binary_analyzer or "angr" 63 | if binary_analyzer == "angr": 64 | return Angr(self.binary_path, **kwargs) 65 | if binary_analyzer == "ghidra": 66 | return Ghidra(self.binary_path, **kwargs) 67 | raise NotImplementedError() 68 | 69 | def get_utils(self, utils): 70 | utils = utils or "default" 71 | if utils == "default": 72 | return Utils(self.p, self.binary_path) 73 | raise NotImplementedError() 74 | 75 | def get_archinfo(self, archinfo): 76 | archinfo = archinfo or "default" 77 | if archinfo == "default": 78 | return MipsInfo() 79 | raise NotImplementedError() 80 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_mipsel_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.mips import MipsInfo 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfMipselLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF\x01") and magic.startswith( 19 | b"\x08\x00", 0x12 20 | ): # EM_MIPS 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_MIPS, 30 | keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_MIPS32, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["--target=mipsel-linux-gnu"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | return Capstone( 50 | capstone.CS_ARCH_MIPS, 51 | capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_MIPS32, 52 | ) 53 | raise NotImplementedError() 54 | 55 | def get_binfmt_tool(self, binfmt_tool): 56 | binfmt_tool = binfmt_tool or "pyelftools" 57 | if binfmt_tool == "pyelftools": 58 | return ELF(self.p, self.binary_path) 59 | raise NotImplementedError() 60 | 61 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 62 | binary_analyzer = binary_analyzer or "angr" 63 | if binary_analyzer == "angr": 64 | return Angr(self.binary_path, **kwargs) 65 | if binary_analyzer == "ghidra": 66 | return Ghidra(self.binary_path, **kwargs) 67 | raise NotImplementedError() 68 | 69 | def get_utils(self, utils): 70 | utils = utils or "default" 71 | if utils == "default": 72 | return Utils(self.p, self.binary_path) 73 | raise NotImplementedError() 74 | 75 | def get_archinfo(self, archinfo): 76 | archinfo = archinfo or "default" 77 | if archinfo == "default": 78 | return MipsInfo() 79 | raise NotImplementedError() 80 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_ppc64_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.ppc64 import Ppc64Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfPpc64Linux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x00\x15", 0x12 20 | ): # EM_PPC64 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_PPC, 30 | keystone.KS_MODE_BIG_ENDIAN + keystone.KS_MODE_PPC64, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["-target", "powerpc64-linux-gnu"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | cs = Capstone( 50 | capstone.CS_ARCH_PPC, capstone.CS_MODE_BIG_ENDIAN + capstone.CS_MODE_64 51 | ) 52 | # NOTE: Doing this because keystone expects registers to just be numbers 53 | cs.cs.syntax = capstone.CS_OPT_SYNTAX_NOREGNAME 54 | return cs 55 | raise NotImplementedError() 56 | 57 | def get_binfmt_tool(self, binfmt_tool): 58 | binfmt_tool = binfmt_tool or "pyelftools" 59 | if binfmt_tool == "pyelftools": 60 | return ELF(self.p, self.binary_path) 61 | raise NotImplementedError() 62 | 63 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 64 | binary_analyzer = binary_analyzer or "angr" 65 | if binary_analyzer == "angr": 66 | return Angr(self.binary_path, **kwargs) 67 | if binary_analyzer == "ghidra": 68 | return Ghidra(self.binary_path, **kwargs) 69 | raise NotImplementedError() 70 | 71 | def get_utils(self, utils): 72 | utils = utils or "default" 73 | if utils == "default": 74 | return Utils(self.p, self.binary_path) 75 | raise NotImplementedError() 76 | 77 | def get_archinfo(self, archinfo): 78 | archinfo = archinfo or "default" 79 | if archinfo == "default": 80 | return Ppc64Info() 81 | raise NotImplementedError() 82 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_ppc64le_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.ppc64 import Ppc64Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfPpc64leLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x15\x00", 0x12 20 | ): # EM_PPC64 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_PPC, 30 | keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_PPC64, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["-target", "powerpc64le-linux-gnu"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | cs = Capstone( 50 | capstone.CS_ARCH_PPC, 51 | capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_64, 52 | ) 53 | # NOTE: Doing this because keystone expects registers to just be numbers 54 | cs.cs.syntax = capstone.CS_OPT_SYNTAX_NOREGNAME 55 | return cs 56 | raise NotImplementedError() 57 | 58 | def get_binfmt_tool(self, binfmt_tool): 59 | binfmt_tool = binfmt_tool or "pyelftools" 60 | if binfmt_tool == "pyelftools": 61 | return ELF(self.p, self.binary_path) 62 | raise NotImplementedError() 63 | 64 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 65 | binary_analyzer = binary_analyzer or "angr" 66 | if binary_analyzer == "angr": 67 | return Angr(self.binary_path, **kwargs) 68 | if binary_analyzer == "ghidra": 69 | return Ghidra(self.binary_path, **kwargs) 70 | raise NotImplementedError() 71 | 72 | def get_utils(self, utils): 73 | utils = utils or "default" 74 | if utils == "default": 75 | return Utils(self.p, self.binary_path) 76 | raise NotImplementedError() 77 | 78 | def get_archinfo(self, archinfo): 79 | archinfo = archinfo or "default" 80 | if archinfo == "default": 81 | return Ppc64Info() 82 | raise NotImplementedError() 83 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_ppc_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.ppc import PpcInfo 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfPpcLinux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x00\x14", 0x12 20 | ): # EM_PPC 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_PPC, 30 | keystone.KS_MODE_BIG_ENDIAN + keystone.KS_MODE_PPC32, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["-target", "powerpc-linux-gnu"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | cs = Capstone( 50 | capstone.CS_ARCH_PPC, capstone.CS_MODE_BIG_ENDIAN + capstone.CS_MODE_32 51 | ) 52 | # NOTE: Doing this because keystone expects registers to just be numbers 53 | cs.cs.syntax = capstone.CS_OPT_SYNTAX_NOREGNAME 54 | return cs 55 | raise NotImplementedError() 56 | 57 | def get_binfmt_tool(self, binfmt_tool): 58 | binfmt_tool = binfmt_tool or "pyelftools" 59 | if binfmt_tool == "pyelftools": 60 | return ELF(self.p, self.binary_path) 61 | raise NotImplementedError() 62 | 63 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 64 | binary_analyzer = binary_analyzer or "angr" 65 | if binary_analyzer == "angr": 66 | return Angr(self.binary_path, **kwargs) 67 | if binary_analyzer == "ghidra": 68 | return Ghidra(self.binary_path, **kwargs) 69 | raise NotImplementedError() 70 | 71 | def get_utils(self, utils): 72 | utils = utils or "default" 73 | if utils == "default": 74 | return Utils(self.p, self.binary_path) 75 | raise NotImplementedError() 76 | 77 | def get_archinfo(self, archinfo): 78 | archinfo = archinfo or "default" 79 | if archinfo == "default": 80 | return PpcInfo() 81 | raise NotImplementedError() 82 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_s390x_linux.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ..components.allocation_managers.allocation_manager import AllocationManager 4 | from ..components.archinfo.s390x import S390xInfo 5 | from ..components.assemblers.keystone import Keystone, keystone 6 | from ..components.binary_analyzers.angr import Angr 7 | from ..components.binary_analyzers.ghidra import Ghidra 8 | from ..components.binfmt_tools.elf import ELF 9 | from ..components.compilers.clang import Clang 10 | from ..components.disassemblers.capstone import Capstone, capstone 11 | from ..components.utils.utils import Utils 12 | from .target import Target 13 | 14 | 15 | class S390xAssembler(Keystone): 16 | def _assemble(self, code: str, base=0, **kwargs) -> bytes: 17 | if base is not None: 18 | new_code = "" 19 | for line in code.splitlines(): 20 | line = line.strip() 21 | rounded_base = base - (base % 0x1000000) 22 | if re.match(r"j 0x[0-9a-fA-F]+", line): 23 | new_code += f"j {hex(int(line.split(' ')[1], 16) - rounded_base)}\n" 24 | else: 25 | new_code += line + "\n" 26 | code = new_code 27 | return super()._assemble(code, base, **kwargs) 28 | 29 | 30 | class ElfS390xLinux(Target): 31 | @staticmethod 32 | def detect_target(binary_path): 33 | with open(binary_path, "rb") as f: 34 | magic = f.read(0x14) 35 | if magic.startswith(b"\x7fELF") and magic.startswith( 36 | b"\x00\x16", 0x12 37 | ): # EM_S390 38 | return True 39 | return False 40 | 41 | def get_assembler(self, assembler): 42 | assembler = assembler or "keystone" 43 | if assembler == "keystone": 44 | return S390xAssembler( 45 | self.p, keystone.KS_ARCH_SYSTEMZ, keystone.KS_MODE_BIG_ENDIAN 46 | ) 47 | raise NotImplementedError() 48 | 49 | def get_allocation_manager(self, allocation_manager): 50 | allocation_manager = allocation_manager or "default" 51 | if allocation_manager == "default": 52 | return AllocationManager(self.p) 53 | raise NotImplementedError() 54 | 55 | def get_compiler(self, compiler): 56 | compiler = compiler or "clang" 57 | if compiler == "clang": 58 | # NOTE: There are some issue with ld.lld in older versions of clang, use version >= 17 59 | return Clang( 60 | self.p, compiler_flags=["-target", "s390x-linux-gnu"], clang_version=19 61 | ) 62 | raise NotImplementedError() 63 | 64 | def get_disassembler(self, disassembler): 65 | disassembler = disassembler or "capstone" 66 | if disassembler == "capstone": 67 | return Capstone(capstone.CS_ARCH_SYSZ, capstone.CS_MODE_BIG_ENDIAN) 68 | raise NotImplementedError() 69 | 70 | def get_binfmt_tool(self, binfmt_tool): 71 | binfmt_tool = binfmt_tool or "pyelftools" 72 | if binfmt_tool == "pyelftools": 73 | return ELF(self.p, self.binary_path) 74 | raise NotImplementedError() 75 | 76 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 77 | binary_analyzer = binary_analyzer or "angr" 78 | if binary_analyzer == "angr": 79 | return Angr(self.binary_path, **kwargs) 80 | if binary_analyzer == "ghidra": 81 | return Ghidra(self.binary_path, **kwargs) 82 | raise NotImplementedError() 83 | 84 | def get_utils(self, utils): 85 | utils = utils or "default" 86 | if utils == "default": 87 | return Utils(self.p, self.binary_path) 88 | raise NotImplementedError() 89 | 90 | def get_archinfo(self, archinfo): 91 | archinfo = archinfo or "default" 92 | if archinfo == "default": 93 | return S390xInfo() 94 | raise NotImplementedError() 95 | -------------------------------------------------------------------------------- /src/patcherex2/targets/elf_x86_linux.py: -------------------------------------------------------------------------------- 1 | from ..components.allocation_managers.allocation_manager import AllocationManager 2 | from ..components.archinfo.x86 import X86Info 3 | from ..components.assemblers.keystone import Keystone, keystone 4 | from ..components.binary_analyzers.angr import Angr 5 | from ..components.binary_analyzers.ghidra import Ghidra 6 | from ..components.binfmt_tools.elf import ELF 7 | from ..components.compilers.clang import Clang 8 | from ..components.disassemblers.capstone import Capstone, capstone 9 | from ..components.utils.utils import Utils 10 | from .target import Target 11 | 12 | 13 | class ElfX86Linux(Target): 14 | @staticmethod 15 | def detect_target(binary_path): 16 | with open(binary_path, "rb") as f: 17 | magic = f.read(0x14) 18 | if magic.startswith(b"\x7fELF") and magic.startswith( 19 | b"\x03\x00", 0x12 20 | ): # EM_386 21 | return True 22 | return False 23 | 24 | def get_assembler(self, assembler): 25 | assembler = assembler or "keystone" 26 | if assembler == "keystone": 27 | return Keystone( 28 | self.p, 29 | keystone.KS_ARCH_X86, 30 | keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_32, 31 | ) 32 | raise NotImplementedError() 33 | 34 | def get_allocation_manager(self, allocation_manager): 35 | allocation_manager = allocation_manager or "default" 36 | if allocation_manager == "default": 37 | return AllocationManager(self.p) 38 | raise NotImplementedError() 39 | 40 | def get_compiler(self, compiler): 41 | compiler = compiler or "clang" 42 | if compiler == "clang": 43 | return Clang(self.p, compiler_flags=["-m32"]) 44 | raise NotImplementedError() 45 | 46 | def get_disassembler(self, disassembler): 47 | disassembler = disassembler or "capstone" 48 | if disassembler == "capstone": 49 | return Capstone( 50 | capstone.CS_ARCH_X86, 51 | capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_32, 52 | ) 53 | raise NotImplementedError() 54 | 55 | def get_binfmt_tool(self, binfmt_tool): 56 | binfmt_tool = binfmt_tool or "pyelftools" 57 | if binfmt_tool == "pyelftools": 58 | return ELF(self.p, self.binary_path) 59 | raise NotImplementedError() 60 | 61 | def get_binary_analyzer(self, binary_analyzer, **kwargs): 62 | binary_analyzer = binary_analyzer or "angr" 63 | if binary_analyzer == "angr": 64 | return Angr(self.binary_path, **kwargs) 65 | if binary_analyzer == "ghidra": 66 | return Ghidra(self.binary_path, **kwargs) 67 | raise NotImplementedError() 68 | 69 | def get_utils(self, utils): 70 | utils = utils or "default" 71 | if utils == "default": 72 | return Utils(self.p, self.binary_path) 73 | raise NotImplementedError() 74 | 75 | def get_archinfo(self, archinfo): 76 | archinfo = archinfo or "default" 77 | if archinfo == "default": 78 | return X86Info() 79 | raise NotImplementedError() 80 | -------------------------------------------------------------------------------- /src/patcherex2/targets/ihex_ppc_bare.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import archinfo 4 | 5 | from ..components.allocation_managers.allocation_manager import AllocationManager 6 | from ..components.archinfo.ppc_vle import PpcVleInfo 7 | from ..components.assemblers.ppc_vle import PpcVle as PpcVleAssembler 8 | from ..components.binary_analyzers.angr import Angr 9 | from ..components.binfmt_tools.ihex import IHex 10 | from ..components.compilers.ppc_vle import PpcVle as PpcVleCompiler 11 | from ..components.disassemblers.ppc_vle import PpcVle as PpcVleDisassembler 12 | from ..components.utils.utils import Utils 13 | from .target import Target 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class IHexPPCBare(Target): 19 | @staticmethod 20 | def detect_target(binary_path): 21 | return False 22 | 23 | def get_assembler(self, assembler): 24 | assembler = assembler or "default" 25 | if assembler == "default": 26 | return PpcVleAssembler(self.p) 27 | raise NotImplementedError() 28 | 29 | def get_allocation_manager(self, allocation_manager): 30 | allocation_manager = allocation_manager or "default" 31 | if allocation_manager == "default": 32 | return AllocationManager(self.p) 33 | raise NotImplementedError() 34 | 35 | def get_compiler(self, compiler): 36 | compiler = compiler or "default" 37 | if compiler == "default": 38 | return PpcVleCompiler(self.p) 39 | raise NotImplementedError() 40 | 41 | def get_disassembler(self, disassembler): 42 | disassembler = disassembler or "default" 43 | if disassembler == "default": 44 | return PpcVleDisassembler(self.p) 45 | raise NotImplementedError() 46 | 47 | def get_binfmt_tool(self, binfmt_tool): 48 | binfmt_tool = binfmt_tool or "default" 49 | if binfmt_tool == "default": 50 | return IHex(self.p, self.binary_path) 51 | raise NotImplementedError() 52 | 53 | def get_binary_analyzer(self, binary_analyzer): 54 | binary_analyzer = binary_analyzer or "angr" 55 | if binary_analyzer == "angr": 56 | return Angr( 57 | self.binary_path, 58 | angr_kwargs={ 59 | "arch": archinfo.ArchPcode("PowerPC:BE:32:MPC8270"), 60 | "auto_load_libs": False, 61 | "load_debug_info": True, 62 | }, 63 | angr_cfg_kwargs={ 64 | "normalize": True, 65 | "data_references": True, 66 | "force_smart_scan": False, 67 | "force_complete_scan": False, 68 | }, 69 | ) 70 | raise NotImplementedError() 71 | 72 | def get_utils(self, utils): 73 | utils = utils or "default" 74 | if utils == "default": 75 | return Utils(self.p, self.binary_path) 76 | raise NotImplementedError() 77 | 78 | def get_archinfo(self, archinfo): 79 | archinfo = archinfo or "default" 80 | if archinfo == "default": 81 | return PpcVleInfo() 82 | raise NotImplementedError() 83 | -------------------------------------------------------------------------------- /src/patcherex2/targets/target.py: -------------------------------------------------------------------------------- 1 | class Target: 2 | target_classes = [] 3 | 4 | def __init__(self, p, binary_path): 5 | self.binary_path = binary_path 6 | self.p = p 7 | 8 | def __init_subclass__(cls, **kwargs): 9 | super().__init_subclass__(**kwargs) 10 | cls.target_classes.append(cls) 11 | 12 | @classmethod 13 | def detect_target(cls, p, binary_path): 14 | for target_class in cls.target_classes: 15 | if target_class.detect_target(binary_path): 16 | return target_class(p, binary_path) 17 | raise ValueError("Unknown target") 18 | 19 | def get_component(self, component_type, component_name, component_opts=None): 20 | if component_opts is None: 21 | component_opts = {} 22 | return getattr(self, f"get_{component_type}")(component_name, **component_opts) 23 | -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/iip_c -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint32_t add_2_then_double(uint32_t n) { 5 | uint32_t ret = n + 2; 6 | ret *= 2; 7 | return ret; 8 | } 9 | 10 | int main(int argc, char *argv[]) { 11 | uint32_t num = add_2_then_double(4); 12 | printf("%d", num); 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c_asm_header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/iip_c_asm_header -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c_asm_header.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void print_areas(float *radii, float *areas, int num_circles) { 4 | for (int i = 0; i < num_circles; i++) { 5 | printf("The area of the circle with radius %f is %f\n", radii[i], areas[i]); 6 | } 7 | } 8 | 9 | void compute_areas(float *radii, int num_radii) { 10 | float areas[num_radii]; 11 | for (int i = 0; i < num_radii; i++) { 12 | // Error is here! We should have multiplied by another radius, but we forgot :( 13 | areas[i] = 3.14 * radii[i]; 14 | } 15 | print_areas(radii, areas, num_radii); 16 | } 17 | 18 | void main() { 19 | float radii[3] = { 1.5, 2.0, 4.3 }; 20 | compute_areas(radii, 3); 21 | } -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c_float: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/iip_c_float -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/iip_c_float.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | float magnitude2(float x, float y, float z) { 4 | return x * x + y * y; 5 | } 6 | 7 | void test(float x, float y, float z) { 8 | float mag2 = magnitude2(x, y, z); 9 | printf("The square magnitude of the vector (%f, %f, %f) is %f\n", x, y, z, mag2); 10 | } 11 | 12 | void main() { 13 | test(0.0f, 0.0f, 0.0f); 14 | test(1.0f, 2.0f, 3.0f); 15 | test(-20.0f, 33.2f, 5.2f); 16 | test(3.0f, 4.0f, 0.0f); 17 | } -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/aarch64/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/aarch64/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/iip_c -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint32_t add_2_then_double(uint32_t n) { 5 | uint32_t ret = n + 2; 6 | ret *= 2; 7 | return ret; 8 | } 9 | 10 | int main(int argc, char *argv[]) { 11 | uint32_t num = add_2_then_double(4); 12 | printf("%d", num); 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c_asm_header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/iip_c_asm_header -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c_asm_header.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void print_areas(float *radii, float *areas, int num_circles) { 4 | for (int i = 0; i < num_circles; i++) { 5 | printf("The area of the circle with radius %f is %f\n", radii[i], areas[i]); 6 | } 7 | } 8 | 9 | void compute_areas(float *radii, int num_radii) { 10 | float areas[num_radii]; 11 | for (int i = 0; i < num_radii; i++) { 12 | // Error is here! We should have multiplied by another radius, but we forgot :( 13 | areas[i] = 3.14 * radii[i]; 14 | } 15 | print_areas(radii, areas, num_radii); 16 | } 17 | 18 | void main() { 19 | float radii[3] = { 1.5, 2.0, 4.3 }; 20 | compute_areas(radii, 3); 21 | } -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c_float: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/iip_c_float -------------------------------------------------------------------------------- /tests/test_binaries/amd64/iip_c_float.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | float magnitude2(float x, float y, float z) { 4 | return x * x + y * y; 5 | } 6 | 7 | void test(float x, float y, float z) { 8 | float mag2 = magnitude2(x, y, z); 9 | printf("The square magnitude of the vector (%f, %f, %f) is %f\n", x, y, z, mag2); 10 | } 11 | 12 | void main() { 13 | test(0.0f, 0.0f, 0.0f); 14 | test(1.0f, 2.0f, 3.0f); 15 | test(-20.0f, 33.2f, 5.2f); 16 | test(3.0f, 4.0f, 0.0f); 17 | } -------------------------------------------------------------------------------- /tests/test_binaries/amd64/issue8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/issue8 -------------------------------------------------------------------------------- /tests/test_binaries/amd64/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/amd64/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/amd64/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/amd64/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/armhf/iip_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/armhf/iip_c -------------------------------------------------------------------------------- /tests/test_binaries/armhf/iip_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uint32_t add_2_then_double(uint32_t n) { 5 | uint32_t ret = n + 2; 6 | ret *= 2; 7 | return ret; 8 | } 9 | 10 | int main(int argc, char *argv[]) { 11 | uint32_t num = add_2_then_double(4); 12 | printf("%d", num); 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_binaries/armhf/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/armhf/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/armhf/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/armhf/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/armhf/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/armhf/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/mips/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/mips/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/mips/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/mips64/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/mips64/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/mips64/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/mips64el/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64el/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/mips64el/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64el/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/mips64el/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mips64el/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/mipsel/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mipsel/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/mipsel/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mipsel/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/mipsel/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/mipsel/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/ppc/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/ppc/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/ppc64/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/ppc64/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/ppc64/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/ppc64le/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64le/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/ppc64le/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64le/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/ppc64le/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/ppc64le/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/s390x/printf.c: -------------------------------------------------------------------------------- 1 | #include 2 | int main() { 3 | printf("%s", "Hi"); 4 | return 0; 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_binaries/s390x/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/s390x/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/s390x/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/s390x/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/s390x/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/s390x/replace_function_patch -------------------------------------------------------------------------------- /tests/test_binaries/s390x/replace_function_patch.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int max(int, int); 4 | int add(int, int); 5 | int subtract(int, int); 6 | int multiply(int, int); 7 | int divide(int, int); 8 | int print_n_times(int, int); 9 | 10 | int max(int a, int b){ return a >= b ? a : b; } 11 | int add(int a, int b){ for(;; b--, a++) if(b <= 0) return a; } 12 | int subtract(int a, int b){ for(;; b--, a--) if(b <= 0) return a; } 13 | int multiply(int a, int b){ for(int c = 0;; b = subtract(b, 1), c = add(c, a)) if(b <= 0) return c; } 14 | int divide(int a, int b){ for(int c = 0;; a = subtract(a, b), c = add(c, 1)) if (a < b) return c; } 15 | int print_n_times(int a, int n){ for(; n > 0; n--) printf("%d", a); } 16 | 17 | void main(){ print_n_times(multiply(add(1, 2), subtract(9, 2)), divide(10, max(2, 5))); } 18 | -------------------------------------------------------------------------------- /tests/test_binaries/x86/printf_nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/x86/printf_nopie -------------------------------------------------------------------------------- /tests/test_binaries/x86/printf_pie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/x86/printf_pie -------------------------------------------------------------------------------- /tests/test_binaries/x86/replace_function_patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purseclab/Patcherex2/5a985fd534e58368dfedaa4d2fc0692721f941a5/tests/test_binaries/x86/replace_function_patch -------------------------------------------------------------------------------- /tests/test_ppc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # ruff: noqa 4 | import logging 5 | import os 6 | import shutil 7 | import subprocess 8 | import tempfile 9 | 10 | import pytest 11 | 12 | from patcherex2 import * 13 | 14 | logging.getLogger("patcherex2").setLevel("DEBUG") 15 | 16 | 17 | class Tests: 18 | @pytest.fixture(autouse=True, scope="class", params=["angr", "ghidra"]) 19 | def setup(self, request): 20 | request.cls.bin_location = str( 21 | os.path.join( 22 | os.path.dirname(os.path.realpath(__file__)), 23 | "./test_binaries/ppc", 24 | ) 25 | ) 26 | request.cls.binary_analyzer = request.param 27 | 28 | def test_raw_file_patch(self): 29 | self.run_one( 30 | "printf_nopie", 31 | [ModifyRawBytesPatch(0x6A4, b"No", addr_type="raw")], 32 | expected_output=b"No", 33 | expected_returnCode=0, 34 | ) 35 | 36 | def test_raw_mem_patch(self): 37 | self.run_one( 38 | "printf_nopie", 39 | [ModifyRawBytesPatch(0x100006A4, b"No")], 40 | expected_output=b"No", 41 | expected_returnCode=0, 42 | ) 43 | 44 | def test_modify_instruction_patch(self): 45 | self.run_one( 46 | "printf_nopie", 47 | [ 48 | ModifyInstructionPatch(0x10000514, "addi 4, 9, 0x6ac"), 49 | ], 50 | expected_output=b"%s", 51 | expected_returnCode=0, 52 | ) 53 | 54 | def test_insert_instruction_patch(self): 55 | instrs = """ 56 | li 0, 0x4 57 | li 3, 1 58 | lis 9, 0x1000 59 | addi 4, 9, 0x6a4 60 | li 5, 0x3 61 | sc 62 | """ 63 | self.run_one( 64 | "printf_nopie", 65 | [InsertInstructionPatch(0x10000528, instrs)], 66 | expected_output=b"Hi\x00Hi", 67 | expected_returnCode=0, 68 | ) 69 | 70 | def test_insert_instruction_patch_2(self): 71 | instrs = """ 72 | li 3, 0x32 73 | li 0, 0x1 74 | sc 75 | """ 76 | self.run_one( 77 | "printf_nopie", 78 | [ 79 | InsertInstructionPatch("return_0x32", instrs), 80 | ModifyInstructionPatch(0x10000528, "b {return_0x32}"), 81 | ], 82 | expected_returnCode=0x32, 83 | ) 84 | 85 | def test_remove_instruction_patch(self): 86 | self.run_one( 87 | "printf_nopie", 88 | [ 89 | RemoveInstructionPatch(0x100006A5, num_bytes=4), 90 | ], 91 | expected_output=b"H\x60", 92 | expected_returnCode=0, 93 | ) 94 | 95 | def test_modify_data_patch(self): 96 | self.run_one( 97 | "printf_nopie", 98 | [ModifyDataPatch(0x100006A4, b"No")], 99 | expected_output=b"No", 100 | expected_returnCode=0, 101 | ) 102 | 103 | def test_insert_data_patch(self, tlen=5): 104 | p1 = InsertDataPatch("added_data", b"A" * tlen) 105 | instrs = """ 106 | li 0, 0x4 107 | li 3, 0x1 108 | lis 9, {added_data}@h 109 | addi 4, 9, {added_data}@l 110 | li 5, %s 111 | sc 112 | """ % hex(tlen) 113 | p2 = InsertInstructionPatch(0x10000528, instrs) 114 | self.run_one( 115 | "printf_nopie", 116 | [p1, p2], 117 | expected_output=b"A" * tlen + b"Hi", 118 | expected_returnCode=0, 119 | ) 120 | 121 | def test_remove_data_patch(self): 122 | self.run_one( 123 | "printf_nopie", 124 | [RemoveDataPatch(0x100006A5, 1)], 125 | expected_output=b"H", 126 | expected_returnCode=0, 127 | ) 128 | 129 | def test_replace_function_patch(self): 130 | code = """ 131 | int add(int a, int b){ for(;; b--, a+=2) if(b <= 0) return a; } 132 | """ 133 | self.run_one( 134 | "replace_function_patch", 135 | [ModifyFunctionPatch(0x100004FC, code)], 136 | expected_output=b"70707070", 137 | expected_returnCode=0, 138 | ) 139 | 140 | @pytest.mark.skip(reason="waiting for cle relocation support") 141 | def test_replace_function_patch_with_function_reference(self): 142 | code = """ 143 | extern int add(int, int); 144 | extern int subtract(int, int); 145 | int multiply(int a, int b){ for(int c = 0;; b = subtract(b, 1), c = subtract(c, a)) if(b <= 0) return c; } 146 | """ 147 | self.run_one( 148 | "replace_function_patch", 149 | [ModifyFunctionPatch(0x100005AC, code)], 150 | expected_output=b"-21-21", 151 | expected_returnCode=0, 152 | ) 153 | 154 | @pytest.mark.skip(reason="waiting for cle relocation support") 155 | def test_replace_function_patch_with_function_reference_and_rodata(self): 156 | code = """ 157 | extern int printf(const char *format, ...); 158 | int multiply(int a, int b){ printf("%sWorld %s %s %s %d\\n", "Hello ", "Hello ", "Hello ", "Hello ", a * b);printf("%sWorld\\n", "Hello "); return a * b; } 159 | """ 160 | self.run_one( 161 | "replace_function_patch", 162 | [ModifyFunctionPatch(0x100005AC, code)], 163 | expected_output=b"Hello World Hello Hello Hello 21\nHello World\n2121", 164 | expected_returnCode=0, 165 | ) 166 | 167 | @pytest.mark.skip(reason="waiting for cle relocation support") 168 | def test_insert_function_patch(self): 169 | insert_code = """ 170 | int min(int a, int b) { return (a < b) ? a : b; } 171 | """ 172 | replace_code = """ 173 | extern int min(int, int); 174 | int max(int a, int b) { return min(a, b); } 175 | """ 176 | self.run_one( 177 | "replace_function_patch", 178 | [ 179 | InsertFunctionPatch("min", insert_code), 180 | ModifyFunctionPatch(0x100006A8, replace_code), 181 | ], 182 | expected_output=b"2121212121", 183 | expected_returnCode=0, 184 | ) 185 | 186 | def run_one( 187 | self, 188 | filename, 189 | patches, 190 | set_oep=None, 191 | inputvalue=None, 192 | expected_output=None, 193 | expected_returnCode=None, 194 | ): 195 | filepath = os.path.join(self.bin_location, filename) 196 | pipe = subprocess.PIPE 197 | 198 | with tempfile.TemporaryDirectory() as td: 199 | tmp_file = os.path.join(td, "patched") 200 | p = Patcherex( 201 | filepath, target_opts={"binary_analyzer": self.binary_analyzer} 202 | ) 203 | for patch in patches: 204 | p.patches.append(patch) 205 | p.apply_patches() 206 | p.save_binary(tmp_file) 207 | p.shutdown() 208 | # os.system(f"readelf -hlS {tmp_file}") 209 | 210 | p = subprocess.Popen( 211 | ["qemu-ppc", "-L", "/usr/powerpc-linux-gnu", tmp_file], 212 | stdin=pipe, 213 | stdout=pipe, 214 | stderr=pipe, 215 | ) 216 | res = p.communicate(inputvalue) 217 | if expected_output: 218 | if res[0] != expected_output: 219 | pytest.fail( 220 | f"AssertionError: {res[0]} != {expected_output}, binary dumped: {self.dump_file(tmp_file)}" 221 | ) 222 | # self.assertEqual(res[0], expected_output) 223 | if expected_returnCode: 224 | if p.returncode != expected_returnCode: 225 | pytest.fail( 226 | f"AssertionError: {p.returncode} != {expected_returnCode}, binary dumped: {self.dump_file(tmp_file)}" 227 | ) 228 | # self.assertEqual(p.returncode, expected_returnCode) 229 | 230 | def dump_file(self, file): 231 | shutil.copy(file, "/tmp/patcherex_failed_binary") 232 | return "/tmp/patcherex_failed_binary" 233 | --------------------------------------------------------------------------------