├── eyepatch ├── iboot │ ├── __init__.py │ ├── errors.py │ ├── types.py │ └── iboot64.py ├── errors.py ├── __init__.py ├── aarch64.py ├── arm.py └── base.py ├── install.sh ├── .pre-commit-config.yaml ├── pyproject.toml ├── LICENSE ├── README.md ├── .gitignore └── uv.lock /eyepatch/iboot/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import * # noqa: F403 2 | from .iboot64 import iBoot64Patcher # noqa: F401 3 | from .types import iBootStage, iBootVersion # noqa: F401 4 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Sanity check 4 | if ! which python3 > /dev/null; then 5 | echo "[ERROR] Python 3 is not installed." 6 | exit 1 7 | fi 8 | 9 | # Install package 10 | python3 -m pip install $(dirname "$0") -------------------------------------------------------------------------------- /eyepatch/iboot/errors.py: -------------------------------------------------------------------------------- 1 | import eyepatch 2 | 3 | 4 | class iBootError(eyepatch.EyepatchError): 5 | pass 6 | 7 | 8 | class InvalidStage(iBootError): 9 | pass 10 | 11 | 12 | class InvalidPlatform(iBootError): 13 | pass 14 | -------------------------------------------------------------------------------- /eyepatch/errors.py: -------------------------------------------------------------------------------- 1 | class EyepatchError(Exception): 2 | pass 3 | 4 | 5 | class DisassemblyError(EyepatchError): 6 | pass 7 | 8 | 9 | class SearchError(DisassemblyError): 10 | pass 11 | 12 | 13 | class AssemblyError(EyepatchError): 14 | pass 15 | 16 | 17 | class InsnError(EyepatchError): 18 | pass 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.13.3 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --unsafe-fixes] 7 | - id: ruff-format 8 | - repo: https://github.com/astral-sh/uv-pre-commit 9 | rev: 0.8.22 10 | hooks: 11 | - id: uv-lock -------------------------------------------------------------------------------- /eyepatch/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | from loguru import logger as _logger 4 | 5 | from .aarch64 import Patcher as AArch64Patcher # noqa: F401 6 | from .arm import Patcher as ARMPatcher # noqa: F401 7 | from .errors import * # noqa: F403 8 | 9 | __version__ = version(__package__) 10 | 11 | _logger.disable('eyepatch') 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "eyepatch" 3 | dynamic = ["version"] 4 | description = "An *OS bootchain patching library." 5 | authors = [{ name = "m1stadev", email = "adamhamdi31@gmail.com" }] 6 | license = { file = "LICENSE" } 7 | readme = "README.md" 8 | requires-python = ">=3.9" 9 | dependencies = [ 10 | "capstone>=5.0.1", 11 | "keystone-engine>=0.9.2", 12 | "typing-extensions>=4.0.0", 13 | "loguru>=0.7.2" 14 | ] 15 | 16 | [project.urls] 17 | Repository = "https://github.com/m1stadev/eyepatch" 18 | "Bug Tracker" = "https://github.com/m1stadev/eyepatch/issues" 19 | 20 | [build-system] 21 | requires = ["hatchling", "uv-dynamic-versioning"] 22 | build-backend = "hatchling.build" 23 | 24 | [tool.hatch.version] 25 | source = "uv-dynamic-versioning" 26 | 27 | [tool.uv-dynamic-versioning] 28 | vcs = "git" 29 | style = "semver" 30 | dirty = true 31 | 32 | 33 | [tool.ruff] 34 | target-version = "py39" 35 | 36 | [tool.ruff.lint] 37 | extend-select = ["I"] 38 | ignore = ["E722"] 39 | 40 | [tool.ruff.format] 41 | quote-style = "single" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 m1sta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eyepatch/iboot/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | class iBootStage(Enum): 6 | STAGE_1 = 1 7 | STAGE_2 = 2 8 | 9 | 10 | @dataclass 11 | class iBootVersion: 12 | major: int 13 | minor: int 14 | patch: int 15 | 16 | def __repr__(self) -> str: 17 | return f'iBoot-{self.major}.{self.minor}.{self.patch}' 18 | 19 | def __gt__(self, other: 'iBootVersion') -> bool: 20 | if not isinstance(other, iBootVersion): 21 | raise TypeError(f'Cannot compare iBootVersion with {type(other)}') 22 | 23 | return (self.major, self.minor, self.patch) > ( 24 | other.major, 25 | other.minor, 26 | other.patch, 27 | ) 28 | 29 | def __lt__(self, other: 'iBootVersion') -> bool: 30 | if not isinstance(other, iBootVersion): 31 | raise TypeError(f'Cannot compare iBootVersion with {type(other)}') 32 | 33 | return (self.major, self.minor, self.patch) < ( 34 | other.major, 35 | other.minor, 36 | other.patch, 37 | ) 38 | 39 | def __eq__(self, other: 'iBootVersion') -> bool: 40 | if not isinstance(other, iBootVersion): 41 | raise TypeError(f'Cannot compare iBootVersion with {type(other)}') 42 | 43 | return (self.major, self.minor, self.patch) == ( 44 | other.major, 45 | other.minor, 46 | other.patch, 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
5 |
6 |
15 |
18 | An *OS bootchain patching library, written in Python. 19 |
20 | 21 | ## Features 22 | - Supports 32-bit and 64-bit ARM 23 | - Attempts to provide a Pythonic API 24 | - Provides convenience functions, like identifying strings & cross-references 25 | 26 | ## Usage 27 | - The `eyepatch` module provides `AArch64Patcher` and `ARMPatcher` classes for 64-bit and 32-bit patching, respectively. 28 | - A good example for the API is in [`eyepatch/iboot`](https://github.com/m1stadev/eyepatch/blob/master/eyepatch/iboot/iboot64.py). 29 | 30 | ## Requirements 31 | - Python 3.9 or higher 32 | 33 | ## Installation 34 | - Local installation: 35 | - `./install.sh` 36 | 37 | ## TODO 38 | - Write documentation 39 | - Add logging 40 | - Add more modules for different patchers 41 | - Add a CLI tool 42 | - Push to PyPI 43 | 44 | ## Support 45 | 46 | For any questions/issues you have, [open an issue](https://github.com/m1stadev/eyepatch/issues). 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | bin/ 163 | .idea/ 164 | .DS_Store 165 | -------------------------------------------------------------------------------- /eyepatch/aarch64.py: -------------------------------------------------------------------------------- 1 | from sys import version_info 2 | from typing import Optional 3 | 4 | from capstone import ( 5 | CS_ARCH_ARM64, 6 | CS_MODE_ARM, 7 | Cs, 8 | ) 9 | from capstone.arm64_const import ( 10 | ARM64_GRP_JUMP, 11 | ARM64_INS_ADD, 12 | ARM64_INS_STP, 13 | ARM64_INS_SUB, 14 | ARM64_OP_IMM, 15 | ARM64_REG_SP, 16 | ARM64_REG_X29, 17 | ARM64_SFT_LSL, 18 | ) 19 | from keystone import KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN, Ks 20 | 21 | import eyepatch 22 | import eyepatch.base 23 | 24 | if version_info >= (3, 11): 25 | from typing import Self 26 | else: 27 | from typing_extensions import Self 28 | 29 | 30 | class ByteString(eyepatch.base._ByteString): 31 | pass 32 | 33 | 34 | class Insn(eyepatch.base._Insn): 35 | def follow_call(self) -> Self: 36 | if self.info.group(ARM64_GRP_JUMP): 37 | op = self.info.operands[-1] 38 | if op.type == ARM64_OP_IMM: 39 | return next(self.patcher.disasm(op.imm + self.offset)) 40 | 41 | raise eyepatch.InsnError('Instruction is not a call') 42 | 43 | def function_begin(self) -> Self: 44 | disasm = self.patcher.disasm(self.offset, reverse=True) 45 | while True: 46 | try: 47 | insn = next(disasm) 48 | except StopIteration: 49 | raise eyepatch.DisassemblyError('Failed to find beginning of function') 50 | 51 | if insn.info.id != ARM64_INS_ADD: 52 | continue 53 | 54 | if [op.reg for op in insn.info.operands[:2]] != [ 55 | ARM64_REG_X29, 56 | ARM64_REG_SP, 57 | ]: 58 | continue 59 | 60 | if (insn := next(disasm)).info.id != ARM64_INS_STP: 61 | continue 62 | 63 | while insn.info.id in (ARM64_INS_STP, ARM64_INS_SUB): 64 | insn = next(disasm) 65 | 66 | return next(self.patcher.disasm(insn.offset + 4)) 67 | 68 | 69 | class Patcher(eyepatch.base._Patcher): 70 | _insn = Insn 71 | _string = ByteString 72 | 73 | def __init__(self, data: bytes): 74 | # TODO: Change arch to CS_ARCH_AARCH64 when Capstone 6.0 releases 75 | super().__init__( 76 | data=data, 77 | asm=Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN), 78 | disasm=Cs(CS_ARCH_ARM64, CS_MODE_ARM), 79 | ) 80 | 81 | def search_xref(self, offset: int, skip: int = 0) -> Optional[_insn]: # noqa: F821 82 | for insn in self.disasm(0x0): 83 | if insn.info.mnemonic in ( 84 | 'b', 85 | 'bl', 86 | 'cbnz', 87 | 'cbz', 88 | 'adr', 89 | 'tbz', 90 | 'tbnz', 91 | 'ldr', 92 | ): 93 | op = insn.info.operands[-1] 94 | if op.type == ARM64_OP_IMM and (op.imm + insn.offset) == offset: 95 | if skip == 0: 96 | return insn 97 | 98 | skip -= 1 99 | 100 | raise eyepatch.SearchError(f'Failed to find xrefs to offset: 0x{offset:x}') 101 | 102 | def search_imm(self, imm: int, offset: int = 0, skip: int = 0) -> _insn: 103 | for insn in self.disasm(offset): 104 | if len(insn.info.operands) == 0: 105 | continue 106 | 107 | val = insn.info.operands[-1].imm 108 | if insn.info.mnemonic == 'mov': 109 | movk = next(insn) 110 | while movk.info.mnemonic == 'movk': 111 | shift = movk.info.operands[-1].shift 112 | if shift.type == ARM64_SFT_LSL: 113 | val |= movk.info.operands[-1].imm << shift.value 114 | else: 115 | val |= movk.info.operands[-1].imm 116 | 117 | movk = next(movk) 118 | 119 | if val == imm: 120 | if skip == 0: 121 | return insn 122 | 123 | skip -= 1 124 | 125 | raise eyepatch.SearchError( 126 | f'Failed to find instruction with immediate value: {hex(imm)}' 127 | ) 128 | -------------------------------------------------------------------------------- /eyepatch/arm.py: -------------------------------------------------------------------------------- 1 | from struct import pack, unpack 2 | from sys import version_info 3 | from typing import Generator, Optional 4 | 5 | from capstone import ( 6 | CS_ARCH_ARM, 7 | CS_MODE_ARM, 8 | CS_MODE_LITTLE_ENDIAN, 9 | CS_MODE_THUMB, 10 | Cs, 11 | CsError, 12 | ) 13 | from capstone.arm_const import ARM_GRP_JUMP, ARM_OP_IMM, ARM_OP_MEM, ARM_REG_PC 14 | from keystone import KS_ARCH_ARM, KS_MODE_ARM, KS_MODE_THUMB, Ks, KsError 15 | 16 | import eyepatch 17 | import eyepatch.base 18 | 19 | if version_info >= (3, 11): 20 | from typing import Self 21 | else: 22 | from typing_extensions import Self 23 | 24 | 25 | class ByteString(eyepatch.base._ByteString): 26 | pass 27 | 28 | 29 | class Insn(eyepatch.base._Insn): 30 | def follow_call(self) -> Self: 31 | if self.info.group(ARM_GRP_JUMP): 32 | op = self.info.operands[-1] 33 | if op.type == ARM_OP_IMM: 34 | return next(self.patcher.disasm(op.imm + self.offset)) 35 | 36 | raise eyepatch.InsnError('Instruction is not a call') 37 | 38 | def patch(self, insn: str) -> None: 39 | if self.info._cs.mode & CS_MODE_THUMB: 40 | data = self.patcher.asm_thumb(insn) 41 | else: 42 | data = self.patcher.asm(insn) 43 | 44 | if len(data) != len(self.data): 45 | raise ValueError( 46 | 'New instruction must be the same size as the current instruction' 47 | ) 48 | 49 | self._data = bytearray(data) 50 | self.patcher._data[self.offset : self.offset + len(data)] = data 51 | self._info = next(self.patcher.disasm(self.offset)).info 52 | 53 | 54 | class Patcher(eyepatch.base._Patcher): 55 | _insn = Insn 56 | _string = ByteString 57 | 58 | def __init__(self, data: bytes): 59 | super().__init__( 60 | data=data, 61 | asm=Ks(KS_ARCH_ARM, KS_MODE_ARM), 62 | disasm=Cs(CS_ARCH_ARM, CS_MODE_ARM + CS_MODE_LITTLE_ENDIAN), 63 | ) 64 | 65 | self._thumb_asm = Ks(KS_ARCH_ARM, KS_MODE_THUMB) 66 | self._thumb_disasm = Cs(CS_ARCH_ARM, CS_MODE_THUMB + CS_MODE_LITTLE_ENDIAN) 67 | self._thumb_disasm.detail = True 68 | 69 | def asm_thumb(self, insn: str) -> bytes: 70 | try: 71 | asm, _ = self._thumb_asm.asm(insn, as_bytes=True) 72 | except KsError: 73 | raise eyepatch.AssemblyError( 74 | f'Failed to assemble ARM thumb instruction: {insn}' 75 | ) 76 | 77 | return asm 78 | 79 | def disasm( 80 | self, offset: int, reverse: bool = False 81 | ) -> Generator[_insn, None, None]: 82 | if reverse: 83 | loop = offset > 0 84 | else: 85 | loop = offset < len(self._data) 86 | 87 | while loop: 88 | # disassemble as 16-bit thumb insn 89 | if reverse: 90 | if (offset - 2) == 0: 91 | raise ValueError('Offset is outside of data range') 92 | 93 | data = self._data[offset - 2 : offset] 94 | 95 | else: 96 | if (offset + 2) > len(self._data): 97 | raise ValueError('Offset is outside of data range') 98 | 99 | data = self._data[offset : offset + 2] 100 | 101 | try: 102 | insn = next(self._thumb_disasm.disasm(code=data, offset=0)) 103 | 104 | if reverse: 105 | offset -= 2 106 | 107 | yield self._insn(offset, data, insn, self) 108 | 109 | if not reverse: 110 | offset += 2 111 | 112 | continue 113 | 114 | except (CsError, StopIteration): 115 | pass 116 | 117 | # disassemble as 32-bit thumb insn 118 | if reverse: 119 | if (offset - 4) == 0: 120 | raise ValueError('Offset is outside of data range') 121 | 122 | data = self._data[offset - 4 : offset] 123 | 124 | else: 125 | if (offset + 4) > len(self._data): 126 | raise ValueError('Offset is outside of data range') 127 | 128 | data = self._data[offset : offset + 4] 129 | 130 | try: 131 | insn = next(self._thumb_disasm.disasm(code=data, offset=0)) 132 | 133 | if reverse: 134 | offset -= 4 135 | 136 | yield self._insn(offset, data, insn, self) 137 | 138 | if not reverse: 139 | offset += 4 140 | 141 | continue 142 | 143 | except (CsError, StopIteration): 144 | pass 145 | 146 | # disassemble as 32-bit arm insn 147 | try: 148 | insn = next(self._disasm.disasm(code=data, offset=0)) 149 | 150 | if reverse: 151 | offset -= 4 152 | 153 | yield self._insn(offset, data, insn, self) 154 | 155 | if not reverse: 156 | offset += 4 157 | 158 | continue 159 | 160 | except (CsError, StopIteration): 161 | pass 162 | 163 | # all else fails, just increment offset by 2 164 | if reverse: 165 | offset -= 2 166 | else: 167 | offset += 2 168 | 169 | def search_imm(self, imm: int, offset: int = 0, skip: int = 0) -> _insn: 170 | for insn in self.disasm(offset): 171 | if len(insn.info.operands) == 0: 172 | continue 173 | 174 | op = insn.info.operands[-1] 175 | if op.type == ARM_OP_MEM: 176 | if op.mem.base != ARM_REG_PC: 177 | continue 178 | 179 | imm_offset = (insn.offset & ~3) + op.mem.disp + 0x4 180 | data = self.data[imm_offset : imm_offset + 4] 181 | insn_imm = unpack(' _insn: 201 | instructions = '\n'.join(insns) 202 | data = self.asm_thumb(instructions) 203 | offset = self.data.find(data, offset) 204 | if offset == -1: 205 | raise eyepatch.SearchError( 206 | f'Failed to find instructions: {instructions} at offset: {hex(offset)}' 207 | ) 208 | 209 | return next(self.disasm(offset)) 210 | 211 | def search_xref( 212 | self, offset: int, base_addr: int, skip: int = 0 213 | ) -> Optional[_insn]: 214 | packed = pack('= (3, 11): 10 | from typing import Self 11 | else: 12 | from typing_extensions import Self 13 | 14 | 15 | class _Assembler: 16 | def __init__(self, asm: Ks): 17 | self._asm = asm 18 | 19 | def asm(self, insn: str) -> bytes: 20 | try: 21 | asm, _ = self._asm.asm(insn, as_bytes=True) 22 | except KsError: 23 | raise eyepatch.AssemblyError(f'Failed to assemble instruction: {insn}') 24 | 25 | return asm 26 | 27 | 28 | class _Insn: 29 | def __init__( 30 | self, 31 | offset: int, 32 | data: bytes, 33 | info: CsInsn, 34 | patcher: '_Patcher', 35 | ): 36 | self._offset = offset 37 | self._data = bytearray(data) 38 | self._patcher = patcher 39 | self._info = info 40 | 41 | def __eq__(self, other) -> bool: 42 | if not isinstance(other, _Insn): 43 | return False 44 | 45 | return self.data == other.data 46 | 47 | def __next__(self) -> Self: 48 | return next(self.patcher.disasm(self.offset + len(self.data))) 49 | 50 | def __repr__(self) -> str: 51 | if self.info is not None: 52 | insn = f'{self.info.mnemonic} {self.info.op_str}' 53 | return f'0x{self.offset:x}: {self.info.mnemonic} {self.info.op_str}' 54 | else: 55 | insn = self.data.hex() 56 | 57 | return f'0x{self.offset:x}: {insn}' 58 | 59 | @property 60 | def info(self) -> CsInsn: 61 | return self._info 62 | 63 | @property 64 | def data(self) -> bytes: 65 | return bytes(self._data) 66 | 67 | @property 68 | def offset(self) -> int: 69 | return self._offset 70 | 71 | @property 72 | def patcher(self) -> '_Patcher': 73 | return self._patcher 74 | 75 | def patch(self, insn: str) -> None: 76 | data = self.patcher.asm(insn) 77 | if len(data) != len(self.data): 78 | raise ValueError( 79 | 'New instruction must be the same size as the current instruction' 80 | ) 81 | 82 | self._data = bytearray(data) 83 | self.patcher._data[self.offset : self.offset + len(data)] = data 84 | self._info = next(self.patcher.disasm(self.offset)).info 85 | 86 | 87 | class _ByteString: 88 | def __init__(self, offset: int, data: bytes, patcher: '_Patcher' = None): 89 | self._offset = offset 90 | self._data = bytearray(data) 91 | self._patcher = patcher 92 | 93 | def __repr__(self) -> str: 94 | return f'0x{self.offset:x}: "{self.string}"' 95 | 96 | @property 97 | def data(self) -> bytes: 98 | return bytes(self._data) 99 | 100 | @property 101 | def string(self) -> str: 102 | return self._data.decode() 103 | 104 | @property 105 | def offset(self) -> int: 106 | return self._offset 107 | 108 | @property 109 | def patcher(self) -> '_Patcher': 110 | return self._patcher 111 | 112 | def replace( 113 | self, 114 | oldvalue: Union[str, bytes], 115 | newvalue: Union[str, bytes], 116 | count: Optional[int] = None, 117 | ) -> None: 118 | if isinstance(oldvalue, str): 119 | oldvalue = oldvalue.encode() 120 | 121 | if isinstance(newvalue, str): 122 | newvalue = newvalue.encode() 123 | 124 | if oldvalue not in self._data: 125 | raise ValueError(f'"{oldvalue}" is not in string.') 126 | 127 | if len(oldvalue) > len(newvalue): 128 | oldvalue += b' ' * (len(newvalue) - len(oldvalue)) 129 | 130 | elif len(oldvalue) < len(newvalue): 131 | raise ValueError("New value can't be longer than old value.") 132 | 133 | self._data = self._data.replace(oldvalue, newvalue, count) 134 | self.patcher._data[self.offset : self.offset + len(self._data)] = self._data 135 | 136 | 137 | class _Disassembler: 138 | _insn = _Insn 139 | _string = _ByteString 140 | 141 | def __init__(self, data: bytes, disasm: Cs): 142 | self._data = bytearray(data) 143 | self._disasm = disasm 144 | self._disasm.detail = True 145 | 146 | @property 147 | def data(self) -> bytes: 148 | return bytes(self._data) 149 | 150 | def disasm( 151 | self, offset: int, reverse: bool = False 152 | ) -> Generator[_insn, None, None]: 153 | if reverse: 154 | len_check = offset - 4 > 0 155 | range_obj = range(offset, 0, -4) 156 | else: 157 | len_check = offset + 4 < len(self._data) 158 | range_obj = range(offset, len(self._data), 4) 159 | 160 | if not len_check: 161 | raise ValueError('Offset is outside of data range') 162 | 163 | for i in range_obj: 164 | if reverse: 165 | i -= 4 166 | 167 | data = self._data[i : i + 4] 168 | 169 | try: 170 | insn = next(self._disasm.disasm(code=data, offset=0)) 171 | yield self._insn(i, data, insn, self) 172 | except (CsError, StopIteration): 173 | pass 174 | 175 | def search_insn( 176 | self, insn_name: str, offset: int = 0, skip: int = 0, reverse: bool = False 177 | ) -> Optional[_insn]: 178 | for insn in self.disasm(offset, reverse): 179 | if insn.info.mnemonic == insn_name: 180 | if skip == 0: 181 | return insn 182 | 183 | skip -= 1 184 | 185 | raise eyepatch.SearchError(f'Failed to find instruction: {insn_name}') 186 | 187 | def search_imm(self, imm: int, offset: int = 0, skip: int = 0) -> _insn: 188 | for insn in self.disasm(offset): 189 | if any(imm == op.imm for op in insn.info.operands): 190 | if skip == 0: 191 | return insn 192 | 193 | skip -= 1 194 | 195 | raise eyepatch.SearchError( 196 | f'Failed to find instruction with immediate value: {hex(imm)}' 197 | ) 198 | 199 | def search_string( 200 | self, 201 | string: Optional[Union[str, bytes]] = None, 202 | offset: Optional[int] = None, 203 | skip: int = 0, 204 | exact: bool = False, 205 | ) -> _string: 206 | if string is not None: 207 | if isinstance(string, str): 208 | string = string.encode() 209 | 210 | if exact: 211 | str_begin = self._data.find(b'\0' + string + b'\0') + 1 212 | if str_begin == 0: 213 | raise eyepatch.SearchError(f'Failed to find string: {string}') 214 | 215 | str_end = str_begin + len(string) 216 | else: 217 | part_str = self._data.find(string) 218 | while skip > 0: 219 | part_str = self._data.find(string, part_str + 1) 220 | skip -= 1 221 | 222 | if part_str == -1: 223 | raise eyepatch.SearchError(f'Failed to find string: {string}') 224 | 225 | str_begin = self.data.rfind(b'\0', 0, part_str) + 1 226 | str_end = self.data.find(b'\0', part_str) 227 | 228 | elif offset is not None: 229 | # Assume if offset is provided, it points to the start 230 | # of the string 231 | str_begin = offset 232 | str_end = self.data.find(b'\0', str_begin) 233 | 234 | else: 235 | raise ValueError('Either string or offset must be provided.') 236 | 237 | return self._string(str_begin, self._data[str_begin:str_end], self) 238 | 239 | 240 | class _Patcher(_Assembler, _Disassembler): 241 | def __init__(self, data: bytes, asm: Ks, disasm: Cs): 242 | self._data = bytearray(data) 243 | 244 | self._asm = asm 245 | self._disasm = disasm 246 | self._disasm.detail = True 247 | 248 | def search_insns(self, *insns: str, offset: int = 0) -> _Insn: 249 | instructions = ';'.join(insns) 250 | data = self.asm(instructions) 251 | 252 | offset = self.data.find(data, offset) 253 | if offset == -1: 254 | raise eyepatch.SearchError( 255 | f'Failed to find instructions: {instructions} at offset: {hex(offset)}' 256 | ) 257 | 258 | return next(self.disasm(offset)) 259 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.9" 4 | 5 | [[package]] 6 | name = "capstone" 7 | version = "5.0.6" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/d5/b0/1f126035a4cbc6f488b97e4bd57a46a28b6ba29ca8b938cbda840601a18a/capstone-5.0.6.tar.gz", hash = "sha256:b11a87d67751b006b9b44428d59c99512e6d6c89cf7dff8cdd92d9065628b5a0", size = 2945704, upload-time = "2025-03-23T16:03:40.795Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/cd/9a/d9c11e090fa03dfc61a03a57ba44c6a07370f4ac5814f2a5804bfd40ee8b/capstone-5.0.6-py3-none-macosx_10_9_universal2.whl", hash = "sha256:0bca16e1c3ca1b928df6103b3889dcb6df7b05392d75a0b7af3508798148b899", size = 2177082, upload-time = "2025-03-23T16:03:27.815Z" }, 12 | { url = "https://files.pythonhosted.org/packages/66/28/72a0be2325e6ee459f27cdcd835d3eee6fed5136321b5f7be41b41dc8656/capstone-5.0.6-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:539191906a34ad1c573ec12b787b2caf154ea41175db6ded9def00aea8151099", size = 1180215, upload-time = "2025-03-23T16:03:29.409Z" }, 13 | { url = "https://files.pythonhosted.org/packages/54/93/7b8fb02661d47a2822d5b640df804ef310417144af02e6db3446f174c4b5/capstone-5.0.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0b87b283905e4fc43635ca04cf26f4a5d9e8375852e5464d38938f3a28c207a", size = 1192757, upload-time = "2025-03-23T16:03:30.966Z" }, 14 | { url = "https://files.pythonhosted.org/packages/ba/a2/d1bdb7260ade8165182979ea16098ef3a37c01316140511a611e549dbfe3/capstone-5.0.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa7892f0c89455078c18f07d2d309fb07baa53061b8f9a63db1ea00d41a46726", size = 1458413, upload-time = "2025-03-23T16:03:32.04Z" }, 15 | { url = "https://files.pythonhosted.org/packages/78/7f/ec0687bbe8f6b128f1d41d90ec7cedfd1aaaa4ecb1ae8d334acc7dad8013/capstone-5.0.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0358855773100bb91ae6681fabce7299df83156945ba943f6211061a592c54a6", size = 1481605, upload-time = "2025-03-23T16:03:33.526Z" }, 16 | { url = "https://files.pythonhosted.org/packages/fc/1d/77bb0f79e1dacdfdcc0679c747d9ca24cc621095e09bdb665e7dd0c580ae/capstone-5.0.6-py3-none-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:667d6466dab1522fa5e9659be5cf1aca83e4fc3da7d15d0e5e6047f71fb46c4a", size = 1480730, upload-time = "2025-03-23T16:03:34.601Z" }, 17 | { url = "https://files.pythonhosted.org/packages/72/63/07437972f68d0b2ba13e1705a6994404c9c961afbadc342c5b6fcf1de652/capstone-5.0.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:45c0e25500dd8d283d3b70f2e10cebfec93ab8bdaf6af9a763a0a999b4705891", size = 1457992, upload-time = "2025-03-23T16:03:35.75Z" }, 18 | { url = "https://files.pythonhosted.org/packages/0c/53/f371e86493a2ae659b5a493c3cc23122974e83a1f53d3a5638d7bb7ac371/capstone-5.0.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:22f1f2f118f8fa1d1c5c90bca90e75864d55e16349b3c03aaea0e86b4a45d2a9", size = 1484184, upload-time = "2025-03-23T16:03:37.05Z" }, 19 | { url = "https://files.pythonhosted.org/packages/df/c3/8b842ae32949c3570581164619c2f69001c6d8da566dc2e490372032b0d6/capstone-5.0.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bc23cf634f51d0e53bdd04ea53ccfff7fc9060dfe58dff1e1b260ce40e5538ff", size = 1485357, upload-time = "2025-03-23T16:03:38.133Z" }, 20 | { url = "https://files.pythonhosted.org/packages/da/72/ff7894c2fb5716d9a3ce9c27ba34b29d991a11d8442d2ef0fcdc5564ba7e/capstone-5.0.6-py3-none-win_amd64.whl", hash = "sha256:761c3deae00b22ac697081cdae1383bb90659dd0d79387a09cf5bdbb22b17064", size = 1271345, upload-time = "2025-03-23T16:03:39.649Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "colorama" 25 | version = "0.4.6" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "eyepatch" 34 | source = { editable = "." } 35 | dependencies = [ 36 | { name = "capstone" }, 37 | { name = "keystone-engine" }, 38 | { name = "loguru" }, 39 | { name = "typing-extensions" }, 40 | ] 41 | 42 | [package.metadata] 43 | requires-dist = [ 44 | { name = "capstone", specifier = ">=5.0.1" }, 45 | { name = "keystone-engine", specifier = ">=0.9.2" }, 46 | { name = "loguru", specifier = ">=0.7.2" }, 47 | { name = "typing-extensions", specifier = ">=4.0.0" }, 48 | ] 49 | 50 | [[package]] 51 | name = "keystone-engine" 52 | version = "0.9.2" 53 | source = { registry = "https://pypi.org/simple" } 54 | sdist = { url = "https://files.pythonhosted.org/packages/0a/65/3a2e7e55cc1db188869bbbacee60036828330e0ce57fc5f05a3167ab4b4d/keystone-engine-0.9.2.tar.gz", hash = "sha256:2f7af62dab0ce6c2732dbb4f31cfa2184a8a149e280b96b92ebc0db84c6e50f5", size = 2813059, upload-time = "2020-06-21T14:13:59.951Z" } 55 | wheels = [ 56 | { url = "https://files.pythonhosted.org/packages/1c/ad/a609493a534049cae43660689b2c5908117746e238f12dc76619d68a223a/keystone_engine-0.9.2-py2.py3-none-macosx_10_14_x86_64.whl", hash = "sha256:dafcc3d9450c239cbc54148855b79c4b387777099c6d054005c835768cf955f2", size = 2950758, upload-time = "2020-06-21T14:13:53.225Z" }, 57 | { url = "https://files.pythonhosted.org/packages/0b/cf/b8eb6956565e91a9a003b1c612765cfe007a1d0b1c6e667dc569ea519f51/keystone_engine-0.9.2-py2.py3-none-manylinux1_i686.whl", hash = "sha256:9e04dea5a2b50509b7b707abdb395de42772c40faa36131ea94482fba8dd5d9f", size = 1785427, upload-time = "2020-06-21T14:13:54.624Z" }, 58 | { url = "https://files.pythonhosted.org/packages/01/5c/40ffbec589262f49ff7c463d96ff0bfab0fbd98d9d869c370a70853a13fb/keystone_engine-0.9.2-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:5a5316a34323620b1bba31dcfe9e4b4ca6f0c030e82fc7a151da7c8fbe81a379", size = 1766182, upload-time = "2020-06-21T14:13:55.817Z" }, 59 | { url = "https://files.pythonhosted.org/packages/88/b9/a9d8b6837346b86bcdda56e5c3fe4ac51f98f4ed40bf71fb6bd8605516da/keystone_engine-0.9.2-py2.py3-none-win32.whl", hash = "sha256:9f81e480904a405ef008f1d9f0e4a05e37d2bd83c5218a27136e1a294b02c1f6", size = 1318239, upload-time = "2020-06-21T14:13:57.122Z" }, 60 | { url = "https://files.pythonhosted.org/packages/a4/8d/58471cb026de45397b29ba4b37ae3e20b434fae14c4b92fd3e9771a7bac8/keystone_engine-0.9.2-py2.py3-none-win_amd64.whl", hash = "sha256:c91db1ff16d9d094e00d1827107d1b4afd5e63ce19b491a0140e660635000e8b", size = 1370823, upload-time = "2020-06-21T14:13:58.476Z" }, 61 | ] 62 | 63 | [[package]] 64 | name = "loguru" 65 | version = "0.7.3" 66 | source = { registry = "https://pypi.org/simple" } 67 | dependencies = [ 68 | { name = "colorama", marker = "sys_platform == 'win32'" }, 69 | { name = "win32-setctime", marker = "sys_platform == 'win32'" }, 70 | ] 71 | sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } 72 | wheels = [ 73 | { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, 74 | ] 75 | 76 | [[package]] 77 | name = "typing-extensions" 78 | version = "4.15.0" 79 | source = { registry = "https://pypi.org/simple" } 80 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 81 | wheels = [ 82 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 83 | ] 84 | 85 | [[package]] 86 | name = "win32-setctime" 87 | version = "1.2.0" 88 | source = { registry = "https://pypi.org/simple" } 89 | sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, 92 | ] 93 | -------------------------------------------------------------------------------- /eyepatch/iboot/iboot64.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from struct import unpack 3 | 4 | from capstone.arm64_const import ARM64_REG_X1, ARM64_REG_XZR 5 | 6 | from eyepatch import AArch64Patcher, errors 7 | from eyepatch.aarch64 import Insn 8 | from eyepatch.iboot import types 9 | from eyepatch.iboot.errors import InvalidPlatform, InvalidStage 10 | 11 | 12 | class iBoot64Patcher(AArch64Patcher): 13 | def __init__(self, data: bytes): 14 | super().__init__(data) 15 | 16 | @cached_property 17 | def base(self) -> int: 18 | ldr = self.search_insn('ldr') 19 | addr = ldr.offset + ldr.info.operands[-1].imm 20 | return unpack('str: 24 | # While the build-style string exists in stage 1, it isn't referenced by anything else. 25 | if self.stage != types.iBootStage.STAGE_2: 26 | raise InvalidStage('build-style only available on stage 2 iBoot') 27 | 28 | bs_str = self.search_string('build-style') 29 | bs_xref = self.search_xref(bs_str.offset) 30 | 31 | es_func = self.search_insn('bl', bs_xref.offset) 32 | for insn in self.disasm(es_func.offset, reverse=True): 33 | if len(insn.info.operands) == 0: 34 | continue 35 | 36 | if insn.info.operands[0].reg != ARM64_REG_X1: 37 | continue 38 | 39 | offset = insn.offset + insn.info.operands[-1].imm 40 | if insn.info.mnemonic == 'ldr': 41 | offset = unpack('int: 50 | plat_str = self.search_string('platform-name') 51 | xref = self.search_xref(plat_str.offset) 52 | adr = self.search_insn('adr', xref.offset, skip=1) 53 | 54 | chip_id = self.search_string(offset=adr.offset + adr.info.operands[-1].imm) 55 | 56 | if chip_id.string.startswith('s5l'): 57 | return int(chip_id.string[3:-1], 16) 58 | elif chip_id.string.startswith('t') or chip_id.string.startswith('s'): 59 | return int(chip_id.string[1:], 16) 60 | 61 | raise InvalidPlatform(f'Unknown platform: "{chip_id.string}"') 62 | 63 | @cached_property 64 | def stage(self) -> types.iBootStage: 65 | for stage1 in ('iBootStage1', 'iBSS', 'LLB'): 66 | try: 67 | self.search_string(f'{stage1} for ') 68 | return types.iBootStage.STAGE_1 69 | except errors.SearchError: 70 | pass 71 | 72 | for stage2 in ('iBootStage2', 'iBEC', 'iBoot'): 73 | try: 74 | self.search_string(f'{stage2} for ') 75 | return types.iBootStage.STAGE_2 76 | except errors.SearchError: 77 | pass 78 | 79 | @cached_property 80 | def version(self) -> types.iBootVersion: 81 | version_str = self.search_string('iBoot-') 82 | major, minor, patch = version_str.string[6:].split('.', maxsplit=2) 83 | return types.iBootVersion(int(major), int(minor), int(patch)) 84 | 85 | @cached_property 86 | def ret0_gadget(self) -> Insn: 87 | try: 88 | insn = self.search_insns('mov w0, #0', 'ret') 89 | except errors.SearchError: 90 | # Failed to find "mov w0, #0" and "ret" instructions 91 | # Insert our own instructions into empty space 92 | asm_len = 2 * 0x4 93 | offset = self.data.find(b'\0' * asm_len) 94 | while offset != -1: 95 | if offset % 4 == 0: 96 | break 97 | 98 | offset = self.data.find(b'\0' * asm_len, offset + 1) 99 | 100 | else: 101 | raise ValueError('No area big enough to place instructions') 102 | 103 | asm = self.asm('mov w0, #0; ret') 104 | self._data[offset : offset + asm_len] = asm 105 | 106 | insn = next(self.disasm(offset)) 107 | 108 | return insn 109 | 110 | def patch_freshnonce(self) -> None: 111 | if self.stage != types.iBootStage.STAGE_2: 112 | raise InvalidStage('freshnonce patch only available on stage 2 iBoot') 113 | 114 | # Find "platform_get_usb_more_other_string" function 115 | nonc_str = self.search_string(' NONC:', exact=True) 116 | nonc_xref = self.search_xref(nonc_str.offset) 117 | 118 | # Find "platform_get_nonce" function 119 | cbz = self.search_insn('cbz', nonc_xref.offset, reverse=True) 120 | pgn_func = self.search_insn('bl', cbz.offset).follow_call() 121 | 122 | # Ensure "platform_consume_nonce" always gets called 123 | insn = self.search_insn('tbnz', pgn_func.offset) 124 | insn.patch('nop') 125 | 126 | def patch_security_allow_modes(self) -> None: 127 | # Find "security_allow_modes" function 128 | dbg_str = self.search_string('debug-enabled') 129 | dbg_xref = self.search_xref(dbg_str.offset) 130 | sam_func = self.search_insn('bl', dbg_xref.offset, 1).follow_call() 131 | 132 | # Patch to always return 1 133 | bne = self.search_insn('b.ne', sam_func.offset) 134 | bne.patch(f'b #{bne.info.operands[-1].imm}') 135 | 136 | mov = self.search_insn('mov', bne.offset + bne.info.operands[-1].imm) 137 | mov.patch('mov x0, #0x1') 138 | 139 | def patch_inject_print(self, string: str, insn: Insn) -> None: 140 | # Find "printf" function 141 | pst_str = self.search_string('power supply type') 142 | pst_xref = self.search_xref(pst_str.offset) 143 | printf_func = self.search_insn('bl', pst_xref.offset).follow_call() 144 | 145 | # Find enough space to inject string + code 146 | string = string.encode() + b'\0' 147 | data_len = len(string) + (8 * 0x4) 148 | 149 | offset = self.data.find(b'\0' * data_len) 150 | while offset != -1: 151 | if offset % 4 == 0: 152 | break 153 | 154 | offset = self.data.find(b'\0' * data_len, offset + 1) 155 | 156 | else: 157 | raise ValueError('No area big enough to place data + string') 158 | 159 | # Function that saves x0 to stack, calls printf, 160 | # restores x0, calls original instruction, then branches back to next instruction 161 | insns = [ 162 | 'sub sp, sp, #0x4', 163 | 'str x0, [sp, #0]', 164 | 'adr x0, #0x18', 165 | f'bl #{hex(printf_func.offset - offset)}', 166 | 'ldr x0, [sp, #0]', 167 | 'add sp, sp, #0x4', 168 | ] 169 | 170 | # If original insn is relative address, change to be relative to our code 171 | if insn.info.mnemonic in ('b', 'bl', 'cbnz', 'cbz', 'adr', 'tbz', 'tbnz') or ( 172 | insn.info.mnemonic == 'ldr' and len(insn.info.operands) == 2 173 | ): 174 | op_str = insn.info.op_str.replace( 175 | hex(insn.info.operands[-1].imm), 176 | hex(insn.info.operands[-1].imm + insn.offset - offset), 177 | ) 178 | else: 179 | op_str = insn.info.op_str 180 | 181 | insns.append(f'{insn.info.mnemonic} {op_str}') 182 | insns.append(f'b #{hex(next(insn).offset - offset)}') 183 | 184 | asm = self.asm(';'.join(insns)) 185 | 186 | self._data[offset : offset + data_len] = asm + string 187 | # Patch original instruction to branch to our function 188 | insn.patch(f'b #{hex(offset - insn.offset)}') 189 | 190 | def patch_nvram(self): 191 | if self.stage != types.iBootStage.STAGE_2: 192 | raise InvalidStage('NVRAM patch only available on stage 2 iBoot') 193 | 194 | # Find "env_blacklist_nvram" function 195 | dbg_str = self.search_string('debug-uarts') 196 | wl_offset = self.data.rfind( 197 | (dbg_str.offset + self.base).to_bytes(0x8, 'little') 198 | ) 199 | while True: 200 | data = self.data[wl_offset : wl_offset + 0x8] 201 | if unpack('