├── .coveragerc ├── .github ├── FUNDING.yml ├── svg │ ├── ktool2.png │ └── logo.png ├── tui.png └── workflows │ └── tests.yml ├── .gitignore ├── .legacy_setup.py ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── EXTERNAL_LICENSES ├── MACHOVIEW_CHAINEDFIXUP_APACHE_LICENSE └── README.md ├── LICENSE ├── README.md ├── dev_coverage.sh ├── dev_install.sh ├── docs ├── Makefile ├── ktool.1 ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── css │ │ └── custom.css │ ├── ktool2.png │ └── logo.png │ ├── conf.py │ ├── index.rst │ ├── ktool.rst │ ├── quickstart.rst │ └── structs.rst ├── pyproject.toml ├── src ├── ktool │ ├── __init__.py │ ├── codesign.py │ ├── exceptions.py │ ├── generator.py │ ├── headers.py │ ├── image.py │ ├── kcache.py │ ├── ktool.py │ ├── ktool_script.py │ ├── loader.py │ ├── macho.py │ ├── objc.py │ ├── structs.py │ ├── swift.py │ ├── util.py │ └── window.py ├── ktool_macho │ ├── __init__.py │ ├── base.py │ ├── binding.py │ ├── codesign.py │ ├── fixups.py │ ├── load_commands.py │ ├── mach_header.py │ └── structs.py ├── ktool_swift │ ├── __init__.py │ ├── demangle.py │ └── structs.py └── lib0cyn │ ├── __init__.py │ ├── kplistlib.py │ ├── log.py │ └── structs.py └── tests ├── build.ninja ├── src ├── libSystem.tbd ├── testbin1.m ├── testent.xml └── testlib1.m └── unit.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ./src 3 | omit = *migrations*, *tests* 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cxnder 4 | patreon: arm64e 5 | -------------------------------------------------------------------------------- /.github/svg/ktool2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0cyn/ktool/2e485160e42ddabb734956dd69dd4803795ce333/.github/svg/ktool2.png -------------------------------------------------------------------------------- /.github/svg/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0cyn/ktool/2e485160e42ddabb734956dd69dd4803795ce333/.github/svg/logo.png -------------------------------------------------------------------------------- /.github/tui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0cyn/ktool/2e485160e42ddabb734956dd69dd4803795ce333/.github/tui.png -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**/*.md' 7 | - '**/*.txt' 8 | branches: 9 | - master 10 | - next 11 | pull_request: 12 | paths-ignore: 13 | - '**/*.md' 14 | - '**/*.txt' 15 | branches: 16 | - master 17 | - next 18 | 19 | jobs: 20 | unit-tests: 21 | name: Unit Tests 22 | strategy: 23 | matrix: 24 | python-version: 25 | - '3.8' 26 | - '3.9' 27 | - '3.10' 28 | - '3.11' 29 | runs-on: macos-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up Python 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install dependencies 38 | run: | 39 | brew install ninja 40 | python -m pip install --upgrade pip 41 | pip install pytest poetry 42 | poetry build 43 | pip install $(ls dist/*.tar.gz | xargs) 44 | - name: Build test images 45 | run: ninja -C tests 46 | - name: Test with pytest 47 | run: | 48 | PYTHONPATH="./src" pytest -s tests/unit.py 49 | 50 | bin-tests: 51 | name: Bin Tests 52 | strategy: 53 | matrix: 54 | python-version: 55 | - '3.6' 56 | - '3.7' 57 | - '3.8' 58 | - '3.9' 59 | - '3.10' 60 | - '3.11' 61 | runs-on: macos-latest 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v3 65 | - name: Set up Python 66 | uses: actions/setup-python@v4 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - name: Install dependencies 70 | run: | 71 | brew install ninja 72 | python -m pip install --upgrade pip 73 | pip install poetry 74 | poetry build 75 | pip install $(ls dist/*.tar.gz | xargs) 76 | - name: Build test images 77 | run: ninja -C tests 78 | - name: Test insert 79 | run: | 80 | ktool insert --lc load --payload /your/mother.dylib --out tests/bins/testbin1.insert.test tests/bins/testbin1 81 | ktool list --linked tests/bins/testbin1.insert.test | grep your/mother 82 | - name: Test edit 83 | run: | 84 | ktool edit --iname your/mother.framework/Mother --out tests/bins/testlib1.dylib.edit.test tests/bins/testlib1.dylib 85 | ktool info tests/bins/testlib1.dylib.edit.test | grep Mother 86 | - name: Test lipo 87 | run: | 88 | ktool lipo --extract arm64 tests/bins/testbin1.fat 89 | ktool file tests/bins/testbin1.fat.arm64 90 | ktool lipo --extract x86_64 tests/bins/testbin1.fat 91 | ktool lipo --create --out tests/bins/testbin1.fat.lipo.test tests/bins/testbin1.fat.arm64 tests/bins/testbin1.fat.x86_64 92 | ktool file tests/bins/testbin1.fat.lipo.test | grep "0x11000" 93 | - name: Test dump 94 | run: | 95 | ktool dump --headers tests/bins/testbin1 | grep "char testPropertyTwo; // ivar: _testPropertyTwo" 96 | - name: Test dump with mmaped-IO enabled 97 | run: | 98 | ktool --mmap dump --headers tests/bins/testbin1 | grep "char testPropertyTwo; // ivar: _testPropertyTwo" 99 | - name: Test symbols 100 | run: | 101 | ktool symbols --imports tests/bins/testbin1 | grep _OBJC_CLASS_$_NSObject 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | venv/ 3 | .idea/ 4 | *.i64 5 | build/ 6 | dist/ 7 | tests/bins/ 8 | .ninja_log 9 | CHANGELOG.md 10 | *.lock 11 | *.egg-info 12 | /htmlcov/ 13 | /.coverage 14 | poetry.lock 15 | *.DS_Store 16 | *.ninja_log 17 | tests/bins/testbin1 18 | tests/bins/testbin1.fat 19 | tests/bins/testlib1.dylib 20 | /tests/.build/ 21 | -------------------------------------------------------------------------------- /.legacy_setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from pathlib import Path 3 | 4 | this_directory = Path(__file__).parent 5 | long_description = (this_directory / "README.md").read_text() 6 | 7 | setup( 8 | name = 'k2l', 9 | version = "2.1.1", 10 | description = 'Static MachO/ObjC Reverse Engineering Toolkit', 11 | long_description = long_description, 12 | long_description_content_type = 'text/markdown', 13 | python_requires = '>=3.6', 14 | author = 'cynder', 15 | license = 'MIT', 16 | url = 'https://github.com/cxnder/ktool', 17 | install_requires = [ 18 | 'pyaes', 19 | 'kimg4', 20 | 'Pygments' 21 | ], 22 | packages = ['ktool_macho', 'ktool', 'ktool_swift'], 23 | package_dir = { 24 | 'ktool_macho': 'src/ktool_macho', 25 | 'lib0cyn': 'src/ktool_macho', 26 | 'ktool': 'src/ktool', 27 | 'ktool_swift': 'src/ktool_swift' 28 | }, 29 | classifiers = [ 30 | 'Programming Language :: Python :: 3', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent' 33 | ], 34 | entry_points = {'console_scripts': [ 35 | 'ktool=ktool.ktool_script:main' 36 | ]}, 37 | project_urls = { 38 | 'Documentation': 'https://ktool.cynder.me/en/latest/ktool.html', 39 | 'Issue Tracker': 'https://github.com/cxnder/ktool/issues' 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/source/conf.py 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.8" 8 | 9 | python: 10 | install: 11 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | A really solid way to contribute right now is by breaking my program in any way you can and filing an issue or pinging me about it. 4 | 5 | Wanting to contribute code! Awesome! First, I'd recommend at least glancing through https://ktool.cynder.me/en/latest/ktool.html if you haven't already, as it documents the majority of the code flow. 6 | 7 | 8 | -------------------------------------------------------------------------------- /EXTERNAL_LICENSES/MACHOVIEW_CHAINEDFIXUP_APACHE_LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Vector 35 Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /EXTERNAL_LICENSES/README.md: -------------------------------------------------------------------------------- 1 | ### notes 2 | 3 | The Chained Fixup code within this project is based upon code I wrote for the MachO-View plugin 4 | within BinaryNinja, which at time of writing is located at https://github.com/Vector35/view-macho 5 | 6 | ### Image notes 7 | 8 | For the logo for this project, I used SF Pro for the font. 9 | 10 | The overlay for the text itself is this actually gorgeous painting by Martina Bulková: 11 | 12 | https://pixabay.com/illustrations/bird-sea-painting-art-ocean-storm-3342446/ 13 | 14 | The backing flower image is a cute little design from the author of this blog: https://lucianapappdesign.blogspot.com/ 15 | 16 | https://pixabay.com/illustrations/watercolour-flowers-watercolor-4262321/ 17 | 18 | You should check both of them out ^v^ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 _kritanta 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 |

6 |

7 | MachO/ObjC Analysis + Editing toolkit. 8 |

9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

21 | 22 |

23 | Library Documentation 24 |
25 |

26 | 27 | 28 | 29 | ### Installation 30 | 31 | ```shell 32 | # Installing 33 | pip3 install k2l 34 | 35 | # Updating 36 | pip3 install --upgrade k2l 37 | ``` 38 | 39 | ### Usage 40 | 41 | ktool is both a convenient CLI toolkit and a library that can be used 42 | in other tools. 43 | 44 | ##### CLI Usage 45 | ``` 46 | > $ ktool 47 | Usage: ktool [command] [filename] 48 | 49 | Commands: 50 | 51 | GUI (Still in active development) --- 52 | ktool open [filename] - Open the ktool command line GUI and browse a file 53 | 54 | MachO Analysis --- 55 | dump - Tools to reconstruct certain files (headers, .tbds) from compiled MachOs 56 | json - Dump image metadata as json 57 | cs - Codesigning info 58 | kcache - Kernel cache specific tools 59 | list - Print various lists (ObjC Classes, etc.) 60 | symbols - Print various tables (Symbols, imports, exports) 61 | info - Print misc info about the target mach-o 62 | 63 | MachO Editing --- 64 | insert - Utils for inserting load commands into MachO Binaries 65 | edit - Utils for editing MachO Binaries 66 | lipo - Utilities for combining/separating slices in fat MachO files. 67 | 68 | Misc Utilities --- 69 | file - Print very basic info about the MachO 70 | img4 - IMG4 Utilities 71 | 72 | Run `ktool [command]` for info/examples on using that command 73 | 74 | Global Flags: 75 | -f - Force Load (ignores malformations in the MachO and tries to load whatever it can) 76 | -v [-1 through 5] - Log verbosiy. -1 completely silences logging. 77 | -V - Print version string (`ktool -V | cat`) to disable the animation 78 | ``` 79 | 80 | ##### Library 81 | 82 | Library documentation is located [here](https://ktool.cynder.me/en/latest/ktool.html) 83 | 84 | --- 85 | 86 | written in pure, 100% python for the sake of platform independence when operating on static binaries and libraries. 87 | this should run on any and all implementations of python3. 88 | 89 | Tested on: 90 | * Windows/Windows on ARM64 91 | * MacOS x86/arm64 92 | * Linux/Linux ARM64 93 | * iOS (iSH, ssh) 94 | * Android (Termux) 95 | * WebAssembly 96 | * Brython 97 | 98 | #### Special thanks to 99 | 100 | JLevin and *OS Internals for existing 101 | 102 | arandomdev for guidance + code 103 | 104 | Blacktop for their amazing ipsw project: https://github.com/blacktop/ipsw 105 | 106 | Artists behind the images used in this project's logo: https://github.com/0cyn/ktool/tree/master/EXTERNAL_LICENSES#image-notes 107 | -------------------------------------------------------------------------------- /dev_coverage.sh: -------------------------------------------------------------------------------- 1 | PYTHONPATH="./src" coverage run --source=src -m pytest tests/unit.py; coverage html; open htmlcov/index.html -------------------------------------------------------------------------------- /dev_install.sh: -------------------------------------------------------------------------------- 1 | rm -rf dist/* 2 | poetry build 3 | python3 -m pip install $(ls dist/*.tar.gz | xargs) --break-system-packages 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/ktool.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" ktool.1 3 | .\" Copyright (c) 2021-present kat 4 | .\" 5 | .\" SPDX-License-Identifier: MIT 6 | .\" Created by TheRealKeto on 8/30/2021. 7 | .\" 8 | .Dd January 23, 2022 9 | .Dt KTOOL 1 10 | .Os 11 | .Sh NAME 12 | .Nm ktool 13 | .Nd Static binary analysis tool 14 | .Sh SYNOPSIS 15 | .Nm 16 | .Oo Ar dump | file | lipo | list | info Oc 17 | .Oo ... Oc 18 | .Op filename 19 | .Sh DESCRIPTION 20 | .Nm 21 | is a static Mach-O binary metadata analysis tool and information dumper. 22 | .Sh COMMANDS 23 | .Bl -tag -width indent 24 | .It Ar dump Oo options ... Oc 25 | Dump set of headers for a bin/framework 26 | .Bl -tag -width indent 27 | .It Fl -headers 28 | Specify that headers should be dumped from a bin/framework 29 | .It Fl -out Op path 30 | Dump a set of headers of a bin/framework to a specific path 31 | .It Fl -tbd 32 | Dump .tbd for a specified bin/framework 33 | .El 34 | .It Ar file 35 | Prints (very) basic info about a file 36 | .It Ar lipo Oo options ... Oc 37 | Interact with universal, multi-architecture files 38 | .Bl -tag -width indent 39 | .It Fl -extract Op slice 40 | Extract a slice from a fat binary 41 | .It Fl -create Op filenames 42 | Create a fat Mach-O binary from multiple thin binaries. 43 | This option must be used alongside the 44 | .Ar --out 45 | flag. 46 | .El 47 | .It Ar list Oo options ... Oc Op filename 48 | Print symbols, classes, protocols, or linked libraries of a binary 49 | .Bl -tag -width indent 50 | .It Fl -symbols 51 | Print the symbol table of a specified binary 52 | .It Fl -classes 53 | Print a list of classes of the specified binary 54 | .It Fl -protocols 55 | Print a list of protocols of the specified binary 56 | .It Fl -linked 57 | Print the list of linked libraries in a specified binary 58 | .El 59 | .It Ar info Oo options ... Oc Fl -slice Oo n | number | index Oc 60 | Print generic information about a Mach-O file 61 | .Bl -tag -width indent 62 | .It Fl h 63 | Prints a help message 64 | .It Fl -vm 65 | Print VM -> Slice -> File addressing mapping for a slice of a Mach-O file 66 | .It Fl -cmds 67 | Print a list of load commands from a specified binary 68 | .It Fl -binding 69 | Print binding actions for a file 70 | .El 71 | .El 72 | .Sh EXAMPLES 73 | To dump .tbd files for a framework 74 | .Dl "ktool dump --tbd [filename]" 75 | .Pp 76 | To print basic information of a binary 77 | .Dl "ktool file [filename]" 78 | .Pp 79 | To extract a slice from a fat binary 80 | .Dl "ktool lipo --extract [slicename] [filename]" 81 | .Sh HISTORY 82 | .Nm 83 | is an alternative to specific tools, such as 84 | .An lipo , 85 | and 86 | .An otool . 87 | For the sake of platform independence, it was written by 88 | .An kat 89 | with the Python Programming Language, preventing 90 | any hassles when operating on static binaries and libraries. 91 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==4.1.2 2 | sphinx_rtd_theme==0.5.2 3 | k2l==0.6.0 4 | furo==2022.2.23 5 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .related-information, .related-information a { 2 | opacity: 0.4; 3 | color: white !important; 4 | text-decoration: none !important; 5 | } 6 | .sidebar-drawer { 7 | width: calc(41% - 26em); 8 | } 9 | tbody tr:first-of-type td { 10 | font-weight: 700; 11 | padding-bottom: .5em; 12 | } -------------------------------------------------------------------------------- /docs/source/_static/ktool2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0cyn/ktool/2e485160e42ddabb734956dd69dd4803795ce333/docs/source/_static/ktool2.png -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0cyn/ktool/2e485160e42ddabb734956dd69dd4803795ce333/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath('../../')) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'ktool' 21 | copyright = '2023, 0cyn' 22 | author = 'cynder' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '2.0.0' 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ["sphinx.ext.autodoc" 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = [] 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = 'furo' 49 | 50 | html_theme_options = { 51 | "sidebar_hide_name": True, 52 | "navigation_with_keys": True, 53 | "light_logo": "ktool2.png", 54 | "dark_logo": "ktool2.png", 55 | 'navigation_depth': 4, 56 | } 57 | html_css_files = [ 58 | 'css/custom.css', 59 | ] 60 | 61 | # Add any paths that contain custom static files (such as style sheets) here, 62 | # relative to this directory. They are copied after the builtin static files, 63 | # so a file named "default.css" will overwrite the builtin "default.css". 64 | html_static_path = ['_static'] 65 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ktool 2 | ================================= 3 | 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: ktool Library: 8 | 9 | quickstart 10 | ktool 11 | 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: MachO Format Specification Docs: 16 | 17 | structs -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick-Start Guide 2 | --------------------- 3 | 4 | This is documentation for getting started with the library as a component of other python projects. 5 | 6 | Gonna try and speedrun this explanation so you can get up and running as soon as possible. 7 | 8 | Basic Concepts to understand: 9 | * There are a lot of subfiles and a few modules, but :python:`import ktool` will import all of the stuff you most likely need. 10 | * My struct system emulates C's. Or if you don't know C, it's like someone smashed together python structs and namedtuples. 11 | 12 | On the github, `src/ktool/ktool_script.py` is a fairly standard client for this library, and you can reference it to 13 | figure out how to do a lot of the basic stuff this library is capable of. 14 | 15 | Install The Library 16 | ======================= 17 | 18 | `python3 -m pip install k2l` 19 | 20 | To install new updates: 21 | 22 | `python3 -m pip install --upgrade k2l` 23 | 24 | 25 | Code Examples 26 | ======================= 27 | 28 | Ideally this library is fairly intuitive to use, and things just work how you expect. 29 | 30 | .. code-block:: python 31 | :caption: Load an image and dump the symbol list 32 | :emphasize-lines: 3 33 | 34 | import ktool 35 | 36 | image = ktool.load_image('my/file.dylib') 37 | for addr, symbol in image.symbols.items(): 38 | print(f'{symbol.name} => {addr}') 39 | 40 | .. code-block:: python 41 | :caption: Dump the classlist for an image 42 | :emphasize-lines: 4 43 | 44 | import ktool 45 | 46 | image = ktool.load_image('my/file.dylib') 47 | objc_image = ktool.load_objc_metadata(image) 48 | 49 | for objc_class in objc_image.classlist: 50 | print(f'{objc_class.name}') 51 | 52 | .. code-block:: python 53 | :caption: Loading and iterating the Mach-O Header. 54 | :emphasize-lines: 3,4 55 | 56 | import ktool 57 | 58 | image = ktool.load_image('my/file.dylib') 59 | for load_command in image.macho_header: # Using the MachOImageHeader __iter__ functionality 60 | if isinstance(load_command, dylinker_command): 61 | print('Dylinker cmd!') 62 | print(f'{load_command.render_indented(4)}') 63 | 64 | # OR, using the basic list iterator 65 | for load_command in image.macho_header.load_commands: 66 | if isinstance(load_command, dylinker_command): 67 | print('Dylinker cmd!') 68 | print(f'{load_command.render_indented(4)}') 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/source/structs.rst: -------------------------------------------------------------------------------- 1 | Structs 2 | --------------------- 3 | 4 | .. 5 | I wrote a generator to make creating these struct field definition tables easier 6 | 7 | .. 8 | https://gist.github.com/KritantaDev/11e2d0acfaacf26e6cf6016fc7b146cd 9 | 10 | 11 | MachO Structs 12 | ********************* 13 | 14 | fat_header 15 | ===================== 16 | 17 | Represents the first 8 bytes of a MachO File 18 | 19 | .. list-table:: 20 | :widths: 5 1 10 21 | 22 | * - Field 23 | - Size 24 | - Description 25 | * - magic 26 | - 4 27 | - File magic. For a fat file, will always be 0xCAFEBABE 28 | * - nfat_archs 29 | - 4 30 | - Number of fat_archs in the file 31 | 32 | 33 | fat_arch 34 | ===================== 35 | 36 | At the beginning of a MachO File, after the header, several fat_arch structs are located, containing information about the slices within the file. 37 | 38 | .. list-table:: 39 | :widths: 5 1 10 40 | 41 | * - Field 42 | - Size 43 | - Description 44 | * - cputype 45 | - 4 46 | - CPUType Item 47 | * - cpusubtype 48 | - 4 49 | - CPU Subtype Item 50 | * - offset 51 | - 4 52 | - Offset in file of the slice 53 | * - size 54 | - 4 55 | - Size in bytes of the slice 56 | * - align 57 | - 4 58 | - Address alignment of struct, as a power of 2 (2^align). 59 | 60 | 61 | Dyld Structs 62 | ********************* 63 | 64 | dyld_header 65 | ===================== 66 | 67 | First 32 bytes of a Slice/Thin MachO File 68 | 69 | .. list-table:: 70 | :widths: 5 1 10 71 | 72 | * - Field 73 | - Size 74 | - Description 75 | * - magic 76 | - 4 77 | - File magic (0xFEEDFACE/0xFEEDFACF) 78 | * - cputype 79 | - 4 80 | - CPU Type 81 | * - cpusubtype 82 | - 4 83 | - CPU Subtype 84 | * - filetype 85 | - 4 86 | - ? 87 | * - loadcnt 88 | - 4 89 | - Number of load commands 90 | * - loadsize 91 | - 4 92 | - Size of load commands 93 | * - flags 94 | - 4 95 | - ? 96 | * - void 97 | - 4 98 | - ? 99 | 100 | 101 | Load commands 102 | ********************* 103 | 104 | dylib_command 105 | ===================== 106 | 107 | Command that represents a dylib 108 | 109 | .. list-table:: 110 | :widths: 5 1 10 111 | 112 | * - Field 113 | - Size 114 | - Description 115 | * - cmd 116 | - 4 117 | - Load command 118 | * - cmdsize 119 | - 4 120 | - Size of load command (including string in dylib struct) 121 | * - dylib 122 | - 4 123 | - CPU Subtype 124 | * - filetype 125 | - 4 126 | - ? 127 | 128 | dylib 129 | ^^^^^^^^^^^^^^^^^^^^^ 130 | 131 | Struct representing a dylib 132 | 133 | .. list-table:: 134 | :widths: 5 1 10 135 | 136 | * - Field 137 | - Size 138 | - Description 139 | * - name 140 | - 4 141 | - lc_str Offset of the load command string from the beginning of the dylib_command struct 142 | * - timestamp 143 | - 4 144 | - ? 145 | * - current_version 146 | - 4 147 | - ? 148 | * - compatibility_version 149 | - 4 150 | - ? 151 | 152 | dylinker_command 153 | ===================== 154 | 155 | Name of the dynamic linker (/bin/dyld) 156 | 157 | .. list-table:: 158 | :widths: 5 1 10 159 | 160 | * - Field 161 | - Size 162 | - Description 163 | * - cmd 164 | - 4 165 | - Load Command 166 | * - cmdsize 167 | - 4 168 | - Size of Load Command 169 | * - name 170 | - 4 171 | - lc_str name of Linker (This will usually just be dyld) 172 | 173 | entry_point_command 174 | ===================== 175 | 176 | Command indicating the entry point of the binary 177 | 178 | .. list-table:: 179 | :widths: 5 1 10 180 | 181 | * - Field 182 | - Size 183 | - Description 184 | * - cmd 185 | - 4 186 | - Load Command 187 | * - cmdsize 188 | - 4 189 | - Size of load command 190 | * - entryoff 191 | - 8 192 | - Offset of the entry point in the file 193 | * - stacksize 194 | - 8 195 | - ? 196 | 197 | rpath_command 198 | ===================== 199 | 200 | Specifies the runtime search path (think iOS Apps, with `./Frameworks` directories) 201 | 202 | .. list-table:: 203 | :widths: 5 1 10 204 | 205 | * - Field 206 | - Size 207 | - Description 208 | * - cmd 209 | - 4 210 | - Load Command 211 | * - cmdsize 212 | - 4 213 | - Size of load command 214 | * - path 215 | - 4 216 | - lc_str Offset of the rpath string from the beginning of the load command 217 | 218 | 219 | dyld_info_command 220 | ===================== 221 | 222 | Contains the offsets of several dyld-related tables 223 | 224 | .. list-table:: 225 | :widths: 5 1 10 226 | 227 | * - Field 228 | - Size 229 | - Description 230 | * - cmd 231 | - 4 232 | - Load command 233 | * - cmdsize 234 | - 4 235 | - Size of load command 236 | * - rebase_off 237 | - 4 238 | - Offset of rebase commands 239 | * - rebase_size 240 | - 4 241 | - Size of rebase commands 242 | * - bind_off 243 | - 4 244 | - Offset of Binding commands 245 | * - bind_size 246 | - 4 247 | - Size of Binding commands 248 | * - weak_bind_off 249 | - 4 250 | - Offset of weak binding commands 251 | * - weak_bind_size 252 | - 4 253 | - Size of weak binding commands 254 | * - lazy_bind_off 255 | - 4 256 | - Offset of lazy binding commands 257 | * - lazy_bind_size 258 | - 4 259 | - Size of lazy binding commands 260 | * - export_off 261 | - 4 262 | - Export table offset 263 | * - export_size 264 | - 4 265 | - Export table size 266 | 267 | 268 | symtab_command 269 | ===================== 270 | 271 | Holds offsets of the symbol table and the string table it uses. 272 | 273 | .. list-table:: 274 | :widths: 5 1 10 275 | 276 | * - Field 277 | - Size 278 | - Description 279 | * - cmd 280 | - 4 281 | - Load Command 282 | * - cmdsize 283 | - 4 284 | - Size of load command 285 | * - symoff 286 | - 4 287 | - Offset of Symbol Table 288 | * - nsyms 289 | - 4 290 | - Number of entries in the symbol table 291 | * - stroff 292 | - 4 293 | - Offset of String Table 294 | * - strsize 295 | - 4 296 | - Size of String Table 297 | 298 | dysymtab_command 299 | ===================== 300 | 301 | TODO 302 | 303 | .. list-table:: 304 | :widths: 5 1 10 305 | 306 | * - Field 307 | - Size 308 | - Description 309 | * - cmd 310 | - 4 311 | - Load Command 312 | * - cmdsize 313 | - 4 314 | - Size of Load Command 315 | * - ilocalsym 316 | - 4 317 | - ? 318 | * - nlocalsym 319 | - 4 320 | - ? 321 | * - iextdefsym 322 | - 4 323 | - ? 324 | * - nextdefsym 325 | - 4 326 | - ? 327 | * - tocoff 328 | - 4 329 | - ? 330 | * - ntoc 331 | - 4 332 | - ? 333 | * - modtaboff 334 | - 4 335 | - ? 336 | * - nmodtab 337 | - 4 338 | - ? 339 | * - extrefsymoff 340 | - 4 341 | - ? 342 | * - nextrefsyms 343 | - 4 344 | - ? 345 | * - indirectsymoff 346 | - 4 347 | - Offset of indirect symbol table 348 | * - nindirectsyms 349 | - 4 350 | - Number of indirect symbols in table 351 | * - extreloff 352 | - 4 353 | - ? 354 | * - nextrel 355 | - 4 356 | - ? 357 | * - locreloff 358 | - 4 359 | - ? 360 | * - nlocrel 361 | - 4 362 | - ? 363 | 364 | uuid_command 365 | ===================== 366 | 367 | Contains the UUID of the library 368 | 369 | .. list-table:: 370 | :widths: 5 1 10 371 | 372 | * - Field 373 | - Size 374 | - Description 375 | * - cmd 376 | - 4 377 | - Load Command 378 | * - cmdsize 379 | - 4 380 | - Size of load command 381 | * - uuid 382 | - 16 383 | - UUID of the Library 384 | 385 | build_version_command 386 | ===================== 387 | 388 | Contains build version and versions of tools used to compile this library/bin 389 | 390 | .. list-table:: 391 | :widths: 5 1 10 392 | 393 | * - Field 394 | - Size 395 | - Description 396 | * - cmd 397 | - 4 398 | - Load Command 399 | * - cmdsize 400 | - 4 401 | - Size of load command 402 | * - platform 403 | - 4 404 | - (Enum) platform the library was compiled for 405 | * - minos 406 | - 4 407 | - Hex XX YY ZZZZ Version of the OS (xx.yy.zzzz) 408 | * - sdk 409 | - 4 410 | - Hex XX YY ZZZZ Version of the SDK used to compile 411 | * - ntools 412 | - 4 413 | - Number of tool commands following this command 414 | 415 | source_version_command 416 | ===================== 417 | 418 | .. list-table:: 419 | :widths: 5 1 10 420 | 421 | * - Field 422 | - Size 423 | - Description 424 | * - cmd 425 | - 4 426 | - Load command 427 | * - cmdsize 428 | - 4 429 | - Size of load command 430 | * - version 431 | - 8 432 | - ? 433 | 434 | sub_client_command 435 | ===================== 436 | 437 | Libraries can specify subclients indicating which binaries are allowed to link to this library 438 | 439 | A process not within this group will be killed if it tries to link this library 440 | 441 | .. list-table:: 442 | :widths: 5 1 10 443 | 444 | * - Field 445 | - Size 446 | - Description 447 | * - cmd 448 | - 4 449 | - Load Command 450 | * - cmdsize 451 | - 4 452 | - Size of load command 453 | * - offset 454 | - 4 455 | - lc_str Offset of Name of subclient from beginning of load command 456 | 457 | 458 | linkedit_data_command 459 | ===================== 460 | 461 | .. list-table:: 462 | :widths: 5 1 10 463 | 464 | * - Field 465 | - Size 466 | - Description 467 | * - cmd 468 | - 4 469 | - Load Command 470 | * - cmdsize 471 | - 4 472 | - Size of load command 473 | * - dataoff 474 | - 4 475 | - Offset of LINKEDIT data 476 | * - datasize 477 | - 4 478 | - Size of LINKEDIT data 479 | 480 | 481 | segment_command_64 482 | ===================== 483 | 484 | Represents a segment in the mach-o file 485 | 486 | .. list-table:: 487 | :widths: 5 1 10 488 | 489 | * - Field 490 | - Size 491 | - Description 492 | * - cmd 493 | - 4 494 | - Load Command 495 | * - cmdsize 496 | - 4 497 | - Size of load command including following segment_64 commands 498 | * - segname 499 | - 16 500 | - Null-byte terminated string within the struct, containing the name of the segment 501 | * - vmaddr 502 | - 8 503 | - Address in the virtual memory mapping of the segment 504 | * - vmsize 505 | - 8 506 | - Size of the segment in the Virtual Memory map 507 | * - fileoff 508 | - 8 509 | - Offset of the segment in the on-disk file 510 | * - filesize 511 | - 8 512 | - Size of the segment in the on-disk file 513 | * - maxprot 514 | - 4 515 | - ? 516 | * - initprot 517 | - 4 518 | - ? 519 | * - nsects 520 | - 4 521 | - Number of section_64 commands within this command 522 | * - flags 523 | - 4 524 | - ? 525 | 526 | section_64 527 | ===================== 528 | 529 | Represents a section in the segment 530 | 531 | .. list-table:: 532 | :widths: 5 1 10 533 | 534 | * - Field 535 | - Size 536 | - Description 537 | * - sectname 538 | - 16 539 | - null-terminated C string Name of the section 540 | * - segname 541 | - 16 542 | - null-terminated C string Name of the containing segment 543 | * - addr 544 | - 8 545 | - VM Address of the section 546 | * - size 547 | - 8 548 | - VM Size of the section 549 | * - offset 550 | - 4 551 | - File address of the section 552 | * - align 553 | - 4 554 | - ? 555 | * - reloff 556 | - 4 557 | - ? 558 | * - nreloc 559 | - 4 560 | - ? 561 | * - flags 562 | - 4 563 | - ? 564 | * - reserved1 565 | - 4 566 | - ? 567 | * - reserved2 568 | - 4 569 | - ? 570 | * - reserved3 571 | - 4 572 | - ? -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "k2l" 3 | version = "2.1.1" 4 | description = "Static MachO/ObjC Reverse Engineering Toolkit" 5 | authors = ["cynder "] 6 | license = "MIT" 7 | repository = "https://github.com/cxnder/ktool" 8 | readme = "README.md" 9 | packages = [ 10 | { include = "ktool", from="src" }, 11 | { include = "ktool_macho", from="src" }, 12 | { include = "ktool_swift", from="src" }, 13 | { include = "lib0cyn", from="src" } 14 | ] 15 | 16 | [tool.isort] 17 | profile = "black" 18 | 19 | [tool.poetry.scripts] 20 | ktool = "ktool.ktool_script:main" 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.6.2" 24 | Pygments = "^2.11.2" 25 | windows-curses = {version = "^2.3.1", platform = "win32"} 26 | 27 | [tool.poetry.dev-dependencies] 28 | pylint = "^2.12.2" 29 | pytest = "^7.0.1" 30 | 31 | [build-system] 32 | requires = ["poetry-core>=1.0.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /src/ktool/__init__.py: -------------------------------------------------------------------------------- 1 | from ktool.ktool import load_image, load_objc_metadata, generate_headers, generate_text_based_stub, load_macho_file, \ 2 | macho_verify, reload_image, macho_combine 3 | 4 | from ktool.objc import ObjCImage 5 | from ktool.loader import MachOImageLoader 6 | from ktool.image import Image 7 | from ktool.macho import Slice, MachOFile, MachOFileType, Segment, Section, MachOImageHeader 8 | 9 | try: 10 | from ktool.headers import HeaderGenerator, Header 11 | except ModuleNotFoundError: 12 | # Maybe pygments wasn't installed and we're running in some weird context 13 | # So let whatever works, work 14 | Header = None 15 | HeaderGenerator = None 16 | pass 17 | from ktool.util import KTOOL_VERSION, ignore, Table, detect_filetype, FileType 18 | 19 | from lib0cyn.log import LogLevel, log 20 | -------------------------------------------------------------------------------- /src/ktool/codesign.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # codesign.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | from typing import List 15 | 16 | from ktool_macho.base import Constructable 17 | from ktool_macho.structs import linkedit_data_command 18 | from ktool_macho.codesign import * 19 | from lib0cyn.log import log 20 | 21 | 22 | def swap_32(value: int): 23 | value = ((value >> 8) & 0x00ff00ff) | ((value << 8) & 0xff00ff00) 24 | value = ((value >> 16) & 0x0000ffff) | ((value << 16) & 0xffff0000) 25 | return value 26 | 27 | 28 | class CodesignInfo(Constructable): 29 | @classmethod 30 | def from_image(cls, image, codesign_cmd: linkedit_data_command): 31 | superblob: SuperBlob = image.read_struct(codesign_cmd.dataoff, SuperBlob) 32 | slots: List[BlobIndex] = [] 33 | off = codesign_cmd.dataoff + SuperBlob.size() 34 | 35 | req_dat = None 36 | 37 | entitlements = "" 38 | requirements = "" 39 | for i in range(swap_32(superblob.count)): 40 | blob_index = image.read_struct(off, BlobIndex) 41 | blob_index.type = swap_32(blob_index.type) 42 | blob_index.offset = swap_32(blob_index.offset) 43 | slots.append(blob_index) 44 | off += BlobIndex.size() 45 | 46 | for blob in slots: 47 | if blob.type == CSSLOT_ENTITLEMENTS: 48 | start = superblob.off + blob.offset 49 | ent_blob = image.read_struct(start, Blob) 50 | ent_blob.magic = swap_32(ent_blob.magic) 51 | ent_blob.length = swap_32(ent_blob.length) 52 | ent_size = ent_blob.length 53 | entitlements = image.read_fixed_len_str(start + Blob.size(), ent_size - Blob.size()) 54 | 55 | elif blob.type == CSSLOT_REQUIREMENTS: 56 | start = superblob.off + blob.offset 57 | req_blob = image.read_struct(start, Blob) 58 | req_blob.magic = swap_32(req_blob.magic) 59 | req_blob.length = swap_32(req_blob.length) 60 | req_dat = image.read_bytearray(start + Blob.size(), req_blob.length - Blob.size()) 61 | 62 | return cls(superblob, slots, entitlements=entitlements, req_dat=req_dat) 63 | 64 | @classmethod 65 | def from_values(cls, *args, **kwargs): 66 | pass 67 | 68 | def raw_bytes(self): 69 | pass 70 | 71 | def __init__(self, superblob, slots, entitlements=None, req_dat=None): 72 | self.superblob = superblob 73 | self.slots = slots 74 | self.entitlements = entitlements 75 | self.req_dat = req_dat 76 | -------------------------------------------------------------------------------- /src/ktool/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # exceptions.py 4 | # 5 | # Custom Exceptions for internal (and occasionally external) usage 6 | # 7 | # This does not include the exceptions used in the interrupt model in the GUI. 8 | # 9 | # This file is part of ktool. ktool is free software that 10 | # is made available under the MIT license. Consult the 11 | # file "LICENSE" that is distributed together with this file 12 | # for the exact licensing terms. 13 | # 14 | # Copyright (c) 0cyn 2021. 15 | # 16 | 17 | class MalformedMachOException(Exception): 18 | """ 19 | """ 20 | 21 | 22 | class MachOAlignmentError(Exception): 23 | """ 24 | """ 25 | 26 | 27 | class VMAddressingError(ValueError): 28 | """ 29 | """ 30 | 31 | 32 | class UnsupportedFiletypeException(Exception): 33 | """ 34 | """ 35 | 36 | 37 | class NoObjCMetadataException(Exception): 38 | """ 39 | """ 40 | -------------------------------------------------------------------------------- /src/ktool/generator.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # generator.py 4 | # 5 | # Holds some miscellaneous generators for certain filetypes 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | import os 16 | from collections import namedtuple 17 | 18 | from ktool_macho.structs import * 19 | from lib0cyn.log import log 20 | from ktool.loader import MachOImageLoader, SymbolType, Image 21 | from ktool.macho import Slice 22 | from ktool.objc import ObjCImage 23 | 24 | 25 | class TBDGenerator: 26 | def __init__(self, image: Image, general=True, objc_lib: ObjCImage = None): 27 | """ 28 | The TBD Generator is a generator that creates TAPI formatted text based stubs for libraries. 29 | 30 | It is currently fairly incomplete, although its output should still be perfectly functional in an SDK. 31 | 32 | After processing, its .dict attribute can be dumped by a TAPI YAML serializer (located in ktool.util) to 33 | produce a functional .tbd 34 | 35 | :param image: image being processed 36 | :type image: image 37 | :param general: Should the generator create a .tbd for usage in SDKs? 38 | :type general: bool 39 | :param objc_lib: Pass an objc image to the generator. If none is passed it will generate its own 40 | """ 41 | self.image = image 42 | self.objc_lib = objc_lib 43 | self.general = general 44 | self.dict = self._generate_dict() 45 | 46 | def _generate_dict(self): 47 | """ 48 | This function simply parses through the image and creates the tbd dict 49 | 50 | :return: The text-based-stub dictionary representation 51 | """ 52 | tbd = {} 53 | if self.general: 54 | tbd['archs'] = ['armv7', 'armv7s', 'arm64', 'arm64e'] 55 | tbd['platform'] = '(null)' 56 | tbd['install-name'] = self.image.dylib.install_name 57 | tbd['current-version'] = 1 58 | tbd['compatibility-version'] = 1 59 | 60 | export_dict = {'archs': ['armv7', 'armv7s', 'arm64', 'arm64e']} 61 | 62 | if len(self.image.allowed_clients) > 0: 63 | export_dict['allowed-clients'] = self.image.allowed_clients 64 | 65 | symbols = [] 66 | classes = [] 67 | ivars = [] 68 | 69 | for sym in self.image.exports: 70 | if sym.dec_type == SymbolType.FUNC: 71 | symbols.append(sym.name) 72 | elif sym.dec_type == SymbolType.CLASS: 73 | classes.append(sym.name) 74 | elif sym.dec_type == SymbolType.IVAR: 75 | ivars.append(sym.name) 76 | 77 | export_dict['symbols'] = symbols 78 | export_dict['objc-classes'] = classes 79 | export_dict['objc-ivars'] = ivars 80 | 81 | tbd['exports'] = [export_dict] 82 | return tbd 83 | 84 | 85 | fat_arch_for_slice = namedtuple("fat_arch_for_slice", ["slice", "cpu_type", "cpu_subtype", "offset", "size", "align"]) 86 | 87 | 88 | class FatMachOGenerator: 89 | """ 90 | 91 | """ 92 | 93 | def __init__(self, slices): 94 | self.slices = slices 95 | self.fat_archs = [] 96 | pfa = None 97 | for fat_slice in slices: 98 | fat_arch_item = self._fat_arch_for_slice(fat_slice, pfa) 99 | pfa = fat_arch_item 100 | self.fat_archs.append(fat_arch_item) 101 | 102 | fat_head = bytearray() 103 | 104 | fh = Struct.create_with_values(fat_header, [b'\xCA\xFE\xBA\xBE', len(self.fat_archs)], "big") 105 | 106 | fat_head += fh.raw 107 | 108 | for fat_arch_item in self.fat_archs: 109 | fa = Struct.create_with_values(fat_arch, 110 | [fat_arch_item.cpu_type, fat_arch_item.cpu_subtype, fat_arch_item.offset, 111 | fat_arch_item.size, fat_arch_item.align], "big") 112 | fat_head += fa.raw 113 | 114 | self.fat_head = fat_head 115 | 116 | @staticmethod 117 | def _fat_arch_for_slice(fat_slice: Slice, previous_fat_arch: fat_arch_for_slice) -> fat_arch_for_slice: 118 | """ 119 | :param fat_slice: Fat slice 120 | :type fat_slice: Slice 121 | :param previous_fat_arch: Previous item returned by this func, or None if first. 122 | :type previous_fat_arch: fat_arch_for_slice 123 | :return: fat_arch_for_slice item. 124 | :rtype: fat_arch_for_slice 125 | """ 126 | lib = MachOImageLoader.load(fat_slice) 127 | cpu_type = lib.macho_header.dyld_header.cpu_type 128 | cpu_subtype = lib.macho_header.dyld_header.cpu_subtype 129 | 130 | if len(fat_slice.macho_file.slices) > 1: 131 | size = fat_slice.arch_struct.size 132 | align = pow(2, fat_slice.arch_struct.align) 133 | align_directive = fat_slice.arch_struct.align 134 | else: 135 | f = fat_slice.macho_file.file_object 136 | old_file_position = f.tell() 137 | f.seek(0, os.SEEK_END) 138 | size = f.tell() 139 | f.seek(old_file_position, os.SEEK_SET) 140 | 141 | if cpu_type == 16777228: 142 | align = pow(2, 0xe) 143 | align_directive = 0xe 144 | elif cpu_type == 16777223: 145 | align = pow(2, 0xc) 146 | align_directive = 0xc 147 | else: 148 | # TODO: Implement Proper Alignment directive calculation based on page size of the target CPUType/Subtype 149 | print(cpu_type) 150 | raise AssertionError("not yet implemented") 151 | if previous_fat_arch is None: 152 | offset = align 153 | else: 154 | offset = 0 155 | while True: 156 | offset += align 157 | if offset > previous_fat_arch.offset + previous_fat_arch.size: 158 | break 159 | 160 | log.debug(f'Create arch with offset {hex(offset)} and size {hex(size)}') 161 | 162 | return fat_arch_for_slice(fat_slice, cpu_type, cpu_subtype, offset, size, align_directive) 163 | -------------------------------------------------------------------------------- /src/ktool/image.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from enum import Enum 3 | from typing import List, Dict, Union 4 | 5 | from ktool_macho import LOAD_COMMAND, dylib_command, dyld_info_command, Struct, CPUSubTypeARM64, CPUType, segment_command_64 6 | from ktool_macho.base import Constructable 7 | from ktool.codesign import CodesignInfo 8 | from ktool.exceptions import VMAddressingError, MachOAlignmentError 9 | from ktool.util import bytes_to_hex, uint_to_int, Table, get_terminal_size 10 | from lib0cyn.log import log 11 | from ktool.macho import Slice, SlicedBackingFile, MachOImageHeader, Segment, PlatformType, ToolType 12 | 13 | os_version = namedtuple("os_version", ["x", "y", "z"]) 14 | _fakeseg = namedtuple("_fakeseg", ["vm_address", "file_address", "size"]) 15 | 16 | 17 | class VM: 18 | """ 19 | New Virtual Address translation based on actual VM -> physical pages 20 | 21 | """ 22 | 23 | def __init__(self, page_size): 24 | self.page_size = page_size 25 | self.page_size_bits = (self.page_size - 1).bit_length() 26 | self.page_table = {} 27 | self.tlb = {} 28 | self.segs = {} 29 | self.vm_base_addr = None 30 | self.dirty = False 31 | 32 | self.fallback: MisalignedVM = MisalignedVM() 33 | 34 | self.detag_kern_64 = False 35 | self.detag_64 = False 36 | 37 | def __str__(self): 38 | table = Table(dividers=True, avoid_wrapping_titles=True) 39 | table.titles = ['VM Start', 'VM End', 'File Start', 'File End', 'Size'] 40 | table.size_pinned_columns = [0, 1] 41 | for vm_addr in self.segs.keys(): 42 | table.rows.append([hex(vm_addr), hex(vm_addr + self.segs[vm_addr][1]), hex(self.segs[vm_addr][0]), 43 | hex(self.segs[vm_addr][0] + self.segs[vm_addr][1]), hex(self.segs[vm_addr][1])]) 44 | return table.fetch_all(get_terminal_size().columns - 5) 45 | 46 | def vm_check(self, address): 47 | try: 48 | self.translate(address) 49 | return True 50 | except ValueError: 51 | return False 52 | 53 | def add_segment(self, segment: Segment): 54 | if segment.name == '__PAGEZERO': 55 | return 56 | 57 | if self.vm_base_addr is None: 58 | self.vm_base_addr = segment.vm_address 59 | 60 | self.map_pages(segment.file_address, segment.vm_address, segment.size) 61 | 62 | def translate(self, address) -> int: 63 | 64 | l_addr = address 65 | 66 | if self.detag_kern_64: 67 | address = address | (0xFFFF << 12 * 4) 68 | 69 | if self.detag_64: 70 | address = address & 0xFFFFFFFFF 71 | 72 | try: 73 | return self.tlb[address] 74 | except KeyError: 75 | pass 76 | 77 | page_offset = address & self.page_size - 1 78 | page_location = address >> self.page_size_bits 79 | 80 | try: 81 | phys_page = self.page_table[page_location] 82 | physical_location = phys_page + page_offset 83 | self.tlb[address] = physical_location 84 | return physical_location 85 | except KeyError: 86 | log.info(f'Address {hex(address)} not mapped, attempting fallback') 87 | 88 | try: 89 | return self.fallback.translate(address) 90 | except VMAddressingError: 91 | raise VMAddressingError( 92 | f'Address {hex(address)} ({hex(l_addr)}) not in VA Table or fallback map. (page: {hex(page_location)})') 93 | 94 | def de_translate(self, file_address): 95 | """ 96 | This method is slow, and should only be used for introspection, and not things that need to be fast. 97 | 98 | :param file_address: 99 | :return: 100 | """ 101 | return self.fallback.de_translate(file_address) 102 | 103 | def map_pages(self, physical_addr, virtual_addr, size): 104 | if physical_addr % self.page_size != 0 or virtual_addr % self.page_size != 0 or size % self.page_size != 0: 105 | raise MachOAlignmentError(f'Tried to map {hex(virtual_addr)}+{hex(size)} to {hex(physical_addr)}') 106 | for i in range(size // self.page_size): 107 | self.page_table[virtual_addr + (i * self.page_size) >> self.page_size_bits] = physical_addr + ( 108 | i * self.page_size) 109 | 110 | seg = _fakeseg(vm_address=virtual_addr, file_address=physical_addr, size=size) 111 | self.segs[virtual_addr] = [physical_addr, size] 112 | self.fallback.add_segment(seg) 113 | 114 | 115 | vm_obj = namedtuple("vm_obj", ["vmaddr", "vmend", "size", "fileaddr"]) 116 | 117 | 118 | class MisalignedVM: 119 | """ 120 | This is the manual backup if the image can't be mapped to 16/4k segments 121 | """ 122 | 123 | def __init__(self): 124 | self.detag_kern_64 = False 125 | self.detag_64 = False 126 | 127 | self.fallback = None 128 | 129 | self.segs = {} 130 | self.map = {} 131 | self.stats = {} 132 | self.vm_base_addr = 0 133 | self.sorted_map = {} 134 | self.cache = {} 135 | 136 | def __str__(self): 137 | table = Table(dividers=True, avoid_wrapping_titles=True) 138 | table.titles = ['VM Start', 'VM End', 'File Start', 'File End', 'Size'] 139 | table.size_pinned_columns = [0, 1] 140 | for vm_addr in self.segs.keys(): 141 | table.rows.append([hex(vm_addr), hex(vm_addr + self.segs[vm_addr][1]), hex(self.segs[vm_addr][0]), 142 | hex(self.segs[vm_addr][0] + self.segs[vm_addr][1]), hex(self.segs[vm_addr][1])]) 143 | return table.fetch_all(get_terminal_size().columns - 5) 144 | 145 | def vm_check(self, vm_address): 146 | try: 147 | self.translate(vm_address) 148 | return True 149 | except ValueError: 150 | return False 151 | 152 | def translate(self, vm_address: int) -> int: 153 | 154 | if self.detag_kern_64: 155 | vm_address = vm_address | (0xFFFF << 12 * 4) 156 | 157 | if self.detag_64: 158 | vm_address = vm_address & 0xFFFFFFFFF 159 | 160 | if vm_address in self.cache: 161 | return self.cache[vm_address] 162 | 163 | for o in self.map.values(): 164 | # noinspection PyChainedComparisons 165 | if vm_address >= o.vmaddr and o.vmend >= vm_address: 166 | file_addr = o.fileaddr + vm_address - o.vmaddr 167 | self.cache[vm_address] = file_addr 168 | return file_addr 169 | 170 | if self.fallback: 171 | return self.fallback.translate(vm_address) 172 | 173 | raise VMAddressingError(f'Address {hex(vm_address)} couldn\'t be found in vm address set') 174 | 175 | def de_translate(self, file_address): 176 | """ 177 | This method is slow, and should only be used for introspection, and not things that need to be fast. 178 | 179 | :param file_address: 180 | :return: 181 | """ 182 | for o in self.map.values(): 183 | file_start = o.fileaddr 184 | file_end = o.fileaddr + o.size 185 | if file_start <= file_address <= file_end: 186 | return o.vmaddr + (file_address - file_start) 187 | log.debug(f'\n\n{str(self)}\n\n') 188 | raise VMAddressingError(f"Could not de_translate address {file_address}") 189 | 190 | def add_segment(self, segment: Union[Segment, _fakeseg]): 191 | if segment.file_address == 0 and segment.size != 0: 192 | self.vm_base_addr = segment.vm_address 193 | 194 | seg_obj = vm_obj(segment.vm_address, segment.vm_address + segment.size, segment.size, segment.file_address) 195 | log.info(str(seg_obj)) 196 | self.map[segment.vm_address] = seg_obj 197 | self.segs[segment.vm_address] = [segment.file_address, segment.size] 198 | 199 | 200 | class LinkedImage: 201 | def __init__(self, source_image: 'Image', cmd): 202 | self.cmd = cmd 203 | self.source_image = source_image 204 | 205 | self.install_name = self._get_name(cmd) 206 | self.weak = cmd.cmd == LOAD_COMMAND.LOAD_WEAK_DYLIB.value 207 | self.local = cmd.cmd == LOAD_COMMAND.ID_DYLIB.value 208 | 209 | def serialize(self): 210 | return {'install_name': self.install_name, 'load_command': LOAD_COMMAND(self.cmd.cmd).name} 211 | 212 | def _get_name(self, cmd) -> str: 213 | read_address = cmd.off + dylib_command.size() 214 | return self.source_image.read_cstr(read_address) 215 | 216 | 217 | class Image: 218 | """ 219 | This class represents the Mach-O Binary as a whole. 220 | 221 | It's the root object in the massive tree of information we're going to build up about the binary. 222 | 223 | This class on its own does not handle populating its fields. 224 | The Dyld class set is fittingly responsible for loading in and processing the raw values to it. 225 | 226 | :ivar Slice slice: Mach-O underlying Slice. This sits inbetween the Image and the underlying file. 227 | :ivar Union[VM, MisalignedVM] vm: VM mapping information describing how this file is loaded into memory. 228 | If this can't be aligned to 16kb or 4kb segments, it will contain a 'MisalignedVM', which uses 229 | slightly slower map lookups. 230 | :ivar MachOImageHeader macho_header: Mach-O Header representation for this image. 231 | :ivar str install_name: "Install name" of the image. Only shared libraries will have this. 232 | :ivar str base_name: Basename of the image, if it has one. This'll be the "filename" part of the installname 233 | (e.g. /usr/lib/libSystem.dylib -> libSystem.dylib) 234 | :ivar List[LinkedImage] linked_images: List of linked images 235 | :ivar Dict[str, Segment] segments: map of segment names to their respective segments 236 | :ivar Union[dyld_info_command, None] info: Raw content of the dyld_info_command if this image contains one 237 | :ivar Union[LinkedImage, None] dylib: "Identity" info of this image. This contains the info that would be used to link it within another image. 238 | :ivar bytearray uuid: UUID of this image. 239 | :ivar Union[CodesignInfo, None] codesign_info: Codesigning information for this binary. 240 | :ivar PlatformType platform: Platform type for the image. 241 | :ivar List[str] allowed_clients: List of the allowed clients for this image. 242 | Allowed clients are a list of executables dyld will allow to load this image. 243 | :ivar str rpath: rpath of the library. 244 | :ivar os_version minos: x.y.z Minimum OS for the library 245 | :ivar os_version sdk_version: x.y.z SDK version this library was linked against 246 | :ivar List[Symbol] imports: List of imported symbols 247 | :ivar List[Symbol] exports: List of exported symbols 248 | :ivar Dict[int, 'Symbol'] symbols: Table mapping locations to symbols in the library 249 | :ivar Dict[int, 'Symbol'] import_table: Table mapping locations to linked symbols in the library 250 | :ivar Dict[int, 'Symbol'] export_table: Table mapping locations to exported symbols in the library 251 | :ivar int entry_point: Extrapolated entry point for the image, pulled from either thread starts or an entry point cmd 252 | :ivar List[int] function_starts: List of function starts for this image 253 | :ivar List[int] thread_state: Initial values for registers when launching this binary. 254 | """ 255 | 256 | def __init__(self, macho_slice: Slice, force_misaligned_vm=False): 257 | """ 258 | Create a MachO image 259 | 260 | :param macho_slice: MachO Slice being processed 261 | :type macho_slice: MachO Slice 262 | """ 263 | self.slice: Slice = macho_slice 264 | 265 | self.vm = None 266 | 267 | if self.slice: 268 | self.macho_header: MachOImageHeader = MachOImageHeader.from_image(macho_slice=macho_slice) 269 | 270 | if force_misaligned_vm: 271 | self.vm = MisalignedVM() 272 | else: 273 | self.vm_realign() 274 | self.ptr_size = self.slice.ptr_size 275 | 276 | self.base_name = "" # copy of self.name 277 | self.install_name = "" 278 | 279 | self.linked_images: List[LinkedImage] = [] 280 | 281 | self.segments: Dict[str, Segment] = {} 282 | 283 | self.info: Union[dyld_info_command, None] = None 284 | self.dylib: Union[LinkedImage, None] = None 285 | self.uuid = None 286 | self.codesign_info: Union[CodesignInfo, None] = None 287 | 288 | self._codesign_cmd = None 289 | 290 | self.platform: PlatformType = PlatformType.UNK 291 | 292 | self.allowed_clients: List[str] = [] 293 | 294 | self.rpath: Union[str, None] = None 295 | 296 | self.minos = os_version(0, 0, 0) 297 | self.sdk_version = os_version(0, 0, 0) 298 | 299 | self.imports: List['Symbol'] = [] 300 | self.exports: List['Symbol'] = [] 301 | 302 | self.symbols: Dict[int, 'Symbol'] = {} 303 | self.import_table: Dict[int, 'Symbol'] = {} 304 | self.export_table: Dict[int, 'Symbol'] = {} 305 | 306 | self.entry_point = 0 307 | 308 | self.function_starts: List[int] = [] 309 | 310 | self.thread_state: List[int] = [] 311 | self._entry_off = 0 312 | 313 | self.binding_table = None 314 | self.weak_binding_table = None 315 | self.lazy_binding_table = None 316 | self.export_trie = None 317 | 318 | self.chained_fixups = None 319 | 320 | self.symbol_table = None 321 | 322 | self.struct_cache: Dict[int, Struct] = {} 323 | 324 | def serialize(self): 325 | image_dict = {'macho_header': self.macho_header.serialize()} 326 | 327 | if self.install_name != "": 328 | image_dict['install_name'] = self.install_name 329 | 330 | linked = [] 331 | for ext_dylib in self.linked_images: 332 | linked.append(ext_dylib.serialize()) 333 | 334 | image_dict['linked'] = linked 335 | 336 | segments = {} 337 | 338 | for seg_name, seg in self.segments.items(): 339 | segments[seg_name] = seg.serialize() 340 | 341 | image_dict['segments'] = segments 342 | if self.uuid: 343 | image_dict['uuid'] = bytes_to_hex(self.uuid) 344 | 345 | image_dict['platform'] = self.platform.name 346 | 347 | image_dict['allowed-clients'] = self.allowed_clients 348 | 349 | if self.rpath: 350 | image_dict['rpath'] = self.rpath 351 | 352 | image_dict['imports'] = [sym.serialize() for sym in self.imports] 353 | image_dict['exports'] = [sym.serialize() for sym in self.exports] 354 | image_dict['symbols'] = [sym.serialize() for sym in self.symbols.values()] 355 | 356 | image_dict['entry_point'] = self.entry_point 357 | 358 | image_dict['function_starts'] = self.function_starts 359 | 360 | image_dict['thread_state'] = self.thread_state 361 | 362 | image_dict['minos'] = f'{self.minos.x}.{self.minos.y}{self.minos.z}' 363 | image_dict['sdk_version'] = f'{self.sdk_version.x}.{self.sdk_version.y}.{self.sdk_version.z}' 364 | 365 | return image_dict 366 | 367 | def vm_realign(self, yell_about_misalignment=True): 368 | 369 | align_by = 0x4000 370 | aligned = False 371 | 372 | detag_64 = False 373 | 374 | segs = [] 375 | for cmd in self.macho_header.load_commands: 376 | if cmd.cmd in [LOAD_COMMAND.SEGMENT.value, LOAD_COMMAND.SEGMENT_64.value]: 377 | segs.append(cmd) 378 | if cmd.cmd == LOAD_COMMAND.LC_DYLD_CHAINED_FIXUPS: 379 | detag_64 = True 380 | 381 | if self.slice.type == CPUType.ARM64 and self.slice.subtype == CPUSubTypeARM64.ARM64E: 382 | detag_64 = True 383 | 384 | while not aligned: 385 | aligned = True 386 | for cmd in segs: 387 | cmd: segment_command_64 = cmd 388 | if cmd.vmaddr % align_by != 0: 389 | if align_by == 0x4000: 390 | align_by = 0x1000 391 | aligned = False 392 | break 393 | else: 394 | align_by = 0 395 | aligned = True 396 | break 397 | 398 | if align_by != 0: 399 | log.info(f'Aligned to {hex(align_by)} pages') 400 | self.vm: VM = VM(page_size=align_by) 401 | self.vm.detag_64 = detag_64 402 | else: 403 | if yell_about_misalignment: 404 | log.info("MachO cannot be aligned to 16k or 4k pages. Swapping to fallback mapping.") 405 | self.vm: MisalignedVM = MisalignedVM() 406 | self.vm.detag_64 = detag_64 407 | 408 | def vm_check(self, address): 409 | return self.vm.vm_check(address) 410 | 411 | def read_uint(self, offset: int, length: int, vm=False): 412 | """ 413 | Get a sequence of bytes (as an int) from a location 414 | 415 | :param offset: Offset within the image 416 | :param length: Amount of bytes to get 417 | :param vm: Is `offset` a VM address 418 | :param section_name: Section Name if vm==True (improves translation time slightly) 419 | :return: `length` Bytes at `offset` 420 | """ 421 | if vm: 422 | offset = self.vm.translate(offset) 423 | return self.slice.read_uint(offset, length) 424 | 425 | def read_ptr(self, offset: int, vm=False): 426 | """ Read a ptr (uint of size self.ptr_size) 427 | 428 | :param offset: 429 | :param vm: 430 | """ 431 | return self.read_uint(offset, self.ptr_size, vm=vm) 432 | 433 | def read_int(self, offset: int, length: int, vm=False): 434 | return uint_to_int(self.read_uint(offset, length, vm), length * 8) 435 | 436 | def read_bytearray(self, offset: int, length: int, vm=False) -> bytearray: 437 | """ 438 | Get a sequence of bytes from a location 439 | 440 | :param offset: Offset within the image 441 | :param length: Amount of bytes to get 442 | :param vm: Is `offset` a VM address 443 | :param section_name: Section Name if vm==True (improves translation time slightly) 444 | :return: `length` Bytes at `offset` 445 | """ 446 | if vm: 447 | offset = self.vm.translate(offset) 448 | return self.slice.read_bytearray(offset, length) 449 | 450 | def read_struct(self, address: int, struct_type, vm=False, endian="little", force_reload=False): 451 | """ 452 | Load a struct (struct_type_t) from a location and return the processed object 453 | 454 | :param address: Address to load struct from 455 | :param struct_type: type of struct (e.g. dyld_header) 456 | :param vm: Is `address` a VM address? 457 | :param endian: Endianness of bytes to read. 458 | :param force_reload: We cache structs to avoid struct unpacking repeatedly. If you for some reason need to force 459 | a reload, set this to true 460 | :return: Loaded struct 461 | """ 462 | if address not in self.struct_cache or force_reload: 463 | if vm: 464 | address = self.vm.translate(address) 465 | struct = self.slice.read_struct(address, struct_type, endian) 466 | self.struct_cache[address] = struct 467 | return struct 468 | 469 | return self.struct_cache[address] 470 | 471 | def read_fixed_len_str(self, address: int, count: int, vm=False, force=False): 472 | """ 473 | Get string with set length from location (to be used essentially only for loading segment names) 474 | 475 | :param address: Address of string start 476 | :param count: Length of string 477 | :param vm: Is `address` a VM address? 478 | :param force: Force reading non-ascii data into the str. Failed decodes will be rendered as `?`. 479 | This is slightly slower than ascii reads due to python jank. 480 | :return: The loaded string. 481 | """ 482 | if vm: 483 | address = self.vm.translate(address) 484 | return self.slice.read_fixed_len_str(address, count, force=force) 485 | 486 | def read_cstr(self, address: int, limit: int = 0, vm=False): 487 | """ 488 | Load a C style string from a location, stopping once a null byte is encountered. 489 | 490 | :param address: Address to load string from 491 | :param limit: Limit of the length of bytes, 0 = unlimited 492 | :param vm: Is `address` a VM address? 493 | :return: The loaded C string 494 | """ 495 | if vm: 496 | address = self.vm.translate(address) 497 | return self.slice.read_cstr(address, limit) 498 | 499 | def read_uleb128(self, read_head: int): 500 | """ 501 | Decode a uleb128 integer from a location 502 | 503 | :param read_head: Start location 504 | :return: (end location, value) 505 | """ 506 | return self.slice.read_uleb128(read_head) 507 | -------------------------------------------------------------------------------- /src/ktool/kcache.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # kcache.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | from io import BytesIO 15 | 16 | import ktool 17 | from lib0cyn.structs import * 18 | from ktool import MachOFile, Image 19 | from lib0cyn.log import log 20 | from ktool.loader import MachOImageHeader, MachOImageLoader 21 | import lib0cyn.kplistlib as plistlib 22 | from ktool.exceptions import UnsupportedFiletypeException 23 | 24 | 25 | class kmod_info_64(Struct): 26 | """ 27 | """ 28 | _FIELDNAMES = ['next_addr', 'info_version', 'id', 'name', 'version', 'reference_count', 'reference_list_addr', 29 | 'address', 'size', 'hdr_size', 'start_addr', 'stop_addr'] 30 | _SIZES = [uint64_t, int32_t, uint32_t, char_t[64], char_t[64], int32_t, uint64_t, uint64_t, uint64_t, uint64_t, 31 | uint64_t, uint64_t] 32 | SIZE = sum([0xffff & i for i in _SIZES]) 33 | 34 | def __init__(self, byte_order="little"): 35 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 36 | 37 | 38 | class Kext: 39 | def __init__(self): 40 | self.prelink_info = {} 41 | 42 | self.name = "" 43 | self.version = "" 44 | self.start_addr = 0 45 | 46 | self.development_region = "" 47 | self.executable_name = "" 48 | self.id = "" 49 | self.bundle_name = "" 50 | self.package_type = "" 51 | self.info_string = "" 52 | self.version_str = "" 53 | 54 | self.image = None 55 | 56 | 57 | class EmbeddedKext(Kext): 58 | def __init__(self, image, prelink_info): 59 | super().__init__() 60 | self.start_addr = prelink_info['_PrelinkExecutableLoadAddr'] 61 | self.size = prelink_info['_PrelinkExecutableSize'] 62 | self.name = prelink_info['CFBundleIdentifier'] 63 | self.version = prelink_info['CFBundleVersion'] 64 | 65 | self.backing_file = BytesIO() 66 | self.backing_file.write(image.read_bytearray(self.start_addr, self.size, vm=True)) 67 | self.backing_file.seek(0) 68 | self.image = ktool.load_image(self.backing_file) 69 | 70 | 71 | class MergedKext(Kext): 72 | def __init__(self, image: Image, kmod_info, start_addr): 73 | super().__init__() 74 | 75 | self.backing_image = image 76 | self.backing_slice = image.slice 77 | 78 | is64 = image.macho_header.is64 79 | self.name = image.read_cstr(kmod_info.off + (0x10 if is64 else 0x8), vm=False) 80 | self.version = image.read_cstr(kmod_info.off + 64 + (0x10 if is64 else 0x8), vm=False) 81 | self.start_addr = start_addr 82 | self.info = kmod_info 83 | 84 | file_base_addr = image.vm.translate(start_addr) 85 | 86 | # cool. we have a basic set of stuff in place, lets bootstrap up an Image from it. 87 | 88 | self.mach_header = MachOImageHeader.from_image(self.backing_slice, file_base_addr) 89 | self.image = Image(self.backing_slice) 90 | self.image.macho_header = self.mach_header 91 | self.image.vm_realign(yell_about_misalignment=False) 92 | 93 | # noinspection PyProtectedMember 94 | MachOImageLoader._parse_load_commands(self.image) 95 | # noinspection PyProtectedMember 96 | MachOImageLoader._process_image(self.image) 97 | 98 | for segment in image.segments.values(): 99 | segment.vm_address = segment.vm_address | 0xffff000000000000 100 | 101 | 102 | class KernelCache: 103 | 104 | def __init__(self, macho_file: MachOFile): 105 | self.mach_kernel_file = macho_file 106 | self.mach_kernel = ktool.load_image(macho_file) 107 | 108 | if self.mach_kernel.macho_header.is64: 109 | self.mach_kernel.vm.detag_kern_64 = True 110 | 111 | self.kexts = [] 112 | 113 | self.prelink_info = {} 114 | 115 | if '__info' in self.mach_kernel.segments['__PRELINK_INFO'].sections: 116 | self._process_prelink_info() 117 | 118 | self.version = self.prelink_info['com.apple.kpi.mach']['CFBundleVersion'] 119 | 120 | self.version_str = "" 121 | vloc = self.mach_kernel.slice.find('@(#)VERSION:') 122 | self.version_str = self.mach_kernel.read_cstr(vloc) 123 | dat = self.version_str.split('xnu_')[-1].split('/')[-1].lower() 124 | 125 | self.release_type = dat.split('_')[0] 126 | self.arch = dat.split('_')[1] 127 | self.soc = dat.split('_')[2] 128 | 129 | if '__kmod_info' in self.mach_kernel.segments['__PRELINK_INFO'].sections: 130 | self._process_merged_kexts() 131 | 132 | if len(self.kexts) == 0: 133 | if '_PrelinkExecutableLoadAddr' in self.prelink_info['com.apple.kpi.mach']: 134 | self._process_kexts_from_prelink_info() 135 | 136 | self._process_kexts() 137 | 138 | def _process_kexts_from_prelink_info(self): 139 | for kext_name, kext in self.prelink_info.items(): 140 | try: 141 | self.kexts.append(EmbeddedKext(self.mach_kernel, kext)) 142 | except UnsupportedFiletypeException: 143 | log.debug(f'Bad Header(?) at {kext_name}') 144 | except KeyError: 145 | pass 146 | 147 | def _process_kexts(self): 148 | for kext in self.kexts: 149 | if kext.name in self.prelink_info.keys(): 150 | kext.executable_name = self.prelink_info[kext.name]['CFBundleExecutable'] 151 | kext.id = self.prelink_info[kext.name]['CFBundleIdentifier'] 152 | kext.bundle_name = self.prelink_info[kext.name]['CFBundleName'] 153 | kext.package_type = self.prelink_info[kext.name]['CFBundlePackageType'] 154 | kext.info_string = self.prelink_info[kext.name]['CFBundleGetInfoString'] if 'CFBundleGetInfoString' in \ 155 | self.prelink_info[ 156 | kext.name] else '' 157 | kext.version_str = self.prelink_info[kext.name]['CFBundleVersion'] 158 | 159 | kext.prelink_info = self.prelink_info[kext.name] 160 | 161 | def _process_prelink_info(self): 162 | address = self.mach_kernel.segments['__PRELINK_INFO'].sections['__info'].vm_address 163 | prelink_info_str = f'{self.mach_kernel.read_cstr(address, vm=True)}' 164 | prelink_info_dat = prelink_info_str.encode('utf-8') 165 | prelink_info = plistlib.readPlistFromBytes(prelink_info_dat) 166 | items = prelink_info['_PrelinkInfoDictionary'] 167 | for bundle_dict in items: 168 | self.prelink_info[bundle_dict['CFBundleIdentifier']] = bundle_dict 169 | 170 | def _process_merged_kexts(self): 171 | kext_starts = [] 172 | kmod_start_sect = self.mach_kernel.segments['__PRELINK_INFO'].sections['__kmod_start'] 173 | 174 | ptr_size = 8 if self.mach_kernel.macho_header.is64 else 4 175 | 176 | for i in range(kmod_start_sect.file_address, kmod_start_sect.file_address + kmod_start_sect.size, ptr_size): 177 | kext_starts.append(self.mach_kernel.read_uint(i, ptr_size, vm=False)) 178 | 179 | kmod_info_locations = [] 180 | kmod_info_sect = self.mach_kernel.segments['__PRELINK_INFO'].sections['__kmod_info'] 181 | 182 | for i in range(kmod_info_sect.file_address, kmod_info_sect.file_address + kmod_info_sect.size, ptr_size): 183 | kmod_info_locations.append(self.mach_kernel.read_uint(i, ptr_size, vm=False)) 184 | 185 | # start processing kmod info 186 | for i, info_loc in enumerate(kmod_info_locations): 187 | info = self.mach_kernel.read_struct(info_loc, kmod_info_64, vm=True) 188 | 189 | start_addr = kext_starts[i] 190 | kext = MergedKext(self.mach_kernel, info, start_addr) 191 | self.kexts.append(kext) 192 | -------------------------------------------------------------------------------- /src/ktool/ktool.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # ktool.py 4 | # 5 | # Outward facing API 6 | # 7 | # Some of these functions are only one line long, but the point is to standardize an outward facing API that allows 8 | # me to refactor and change things internally without breaking others' scripts. 9 | # 10 | # This file is part of ktool. ktool is free software that 11 | # is made available under the MIT license. Consult the 12 | # file "LICENSE" that is distributed together with this file 13 | # for the exact licensing terms. 14 | # 15 | # Copyright (c) 0cyn 2021. 16 | # 17 | 18 | from typing import Dict, Union, BinaryIO, List 19 | from io import BytesIO 20 | 21 | from ktool.loader import MachOImageLoader, Image 22 | from ktool.generator import TBDGenerator, FatMachOGenerator 23 | 24 | try: 25 | from ktool.headers import HeaderGenerator, Header 26 | except ModuleNotFoundError: 27 | # Maybe pygments wasn't installed and we're running in some weird context 28 | # So let whatever works, work 29 | Header = None 30 | HeaderGenerator = None 31 | pass 32 | from ktool.macho import Slice, MachOFile, SlicedBackingFile 33 | from ktool.objc import ObjCImage, MethodList 34 | from ktool.swift import SwiftImage 35 | from ktool.util import TapiYAMLWriter, ignore 36 | 37 | from lib0cyn.log import log 38 | 39 | 40 | def load_macho_file(fp: Union[SlicedBackingFile, BinaryIO, BytesIO], use_mmaped_io=False) -> MachOFile: 41 | """ 42 | This function takes a bare file and loads it as a MachOFile. 43 | 44 | File should be opened with 'rb' 45 | 46 | :param fp: BinaryIO object 47 | :param use_mmaped_io: Should the MachOFile be loaded with a mmaped-io-backend? Leaving this enabled massively 48 | improves load time and IO performance, only disable if your system doesn't support it 49 | :return: 50 | """ 51 | if isinstance(fp, BytesIO): 52 | use_mmaped_io = False 53 | elif isinstance(fp, SlicedBackingFile): 54 | use_mmaped_io = False 55 | new_fp = BytesIO() 56 | new_fp.write(bytes(fp.file)) 57 | new_fp.seek(0) 58 | fp = new_fp 59 | 60 | return MachOFile(fp, use_mmaped_io=use_mmaped_io) 61 | 62 | 63 | def reload_image(image: Image) -> Image: 64 | """ 65 | Reload an image (properly updates internal representations after patches) 66 | 67 | :param image: 68 | :return: 69 | """ 70 | # This is going to be horribly slow. Dyld class needs refactored to have a better way to do this or ideally just 71 | # not fuck things up and require a reload every time we make a patch. 72 | return load_image(image.slice) 73 | 74 | 75 | def load_image(fp: Union[BinaryIO, MachOFile, Slice, BytesIO, SlicedBackingFile], slice_index=0, load_symtab=True, 76 | load_imports=True, load_exports=True, use_mmaped_io=True, force_misaligned_vm=False) -> Image: 77 | """ 78 | Take a bare file, MachOFile, BytesIO, SlicedBackingFile, or Slice, and load MachO/dyld metadata about that item 79 | 80 | :param fp: a bare file, MachOFile, or Slice to load. 81 | :param slice_index: If a Slice is not being passed, and a file or MachOFile is a Fat MachO, which slice should be loaded? 82 | :param use_mmaped_io: If a bare file is being passed, load it with mmaped IO? 83 | :param load_symtab: Load the symbol table if one exists. This can be disabled for targeted loads, for speed. 84 | :param load_imports: Load imports if they exist. This can be disabled for targeted loads, for speed. 85 | :param load_exports: Load exports if they exist. This can be disabled for targeted loads, for speed. 86 | :return: Returns a loaded Image object 87 | :rtype: Image 88 | """ 89 | if isinstance(fp, MachOFile): 90 | macho_file = fp 91 | macho_slice: Slice = macho_file.slices[slice_index] 92 | elif isinstance(fp, Slice): 93 | macho_slice = fp 94 | elif isinstance(fp, BytesIO): 95 | macho_file = load_macho_file(fp, use_mmaped_io=False) 96 | macho_slice: Slice = macho_file.slices[slice_index] 97 | elif isinstance(fp, SlicedBackingFile): 98 | macho_file = load_macho_file(fp, use_mmaped_io=False) 99 | macho_slice: Slice = macho_file.slices[slice_index] 100 | else: 101 | macho_file = load_macho_file(fp, use_mmaped_io=use_mmaped_io) 102 | macho_slice: Slice = macho_file.slices[slice_index] 103 | 104 | return MachOImageLoader.load(macho_slice, load_symtab=load_symtab, load_imports=load_imports, 105 | load_exports=load_exports, force_misaligned_vm=force_misaligned_vm) 106 | 107 | 108 | def macho_verify(fp: Union[BinaryIO, MachOFile, Slice, Image]) -> None: 109 | """ 110 | This function takes a variety of MachO-based objects, and loads them with malformation exceptions fully enabled. 111 | 112 | This can be used to verify patch code did not damage or improperly modify a MachO. 113 | 114 | :param fp: One of: BinaryIO, MachOFile, Slice, or Image, to load and verify 115 | :return: 116 | :raises: MalformedMachOException 117 | """ 118 | should_ignore = ignore.MALFORMED 119 | 120 | log.info("Verifying MachO Integrity") 121 | ignore.MALFORMED = False 122 | 123 | if isinstance(fp, Image): 124 | load_image(fp.slice) 125 | 126 | elif isinstance(fp, MachOFile) or isinstance(fp, BinaryIO): 127 | if isinstance(fp, MachOFile): 128 | slices = fp.slices 129 | else: 130 | slices = load_macho_file(fp) 131 | 132 | for macho_slice in slices: 133 | load_image(macho_slice) 134 | 135 | else: 136 | load_image(fp) 137 | 138 | ignore.MALFORMED = should_ignore 139 | 140 | 141 | def load_objc_metadata(image: Image) -> ObjCImage: 142 | if image.chained_fixups is not None: 143 | data_io = BytesIO() 144 | data_io.write(image.slice.file.read_bytes(0, image.slice.file.size)) 145 | data_io.seek(0) 146 | for rebase in image.chained_fixups.rebases.items(): 147 | data_io.seek(image.vm.translate(rebase[0])) 148 | data_io.write(rebase[1].to_bytes(8, 'little')) 149 | data_io.seek(0) 150 | image = load_image(data_io) 151 | return ObjCImage.from_image(image) 152 | 153 | 154 | def load_swift_metadata(objc_image: ObjCImage) -> SwiftImage: 155 | return SwiftImage.from_image(objc_image) 156 | 157 | 158 | def generate_headers(objc_image: 'ObjCImage', sort_items=False, forward_declare_private_imports=False) -> Dict[ 159 | str, Header]: 160 | out = {} 161 | 162 | if sort_items: 163 | for objc_class in objc_image.classlist: 164 | objc_class.methods.sort(key=lambda h: h.signature) 165 | objc_class.properties.sort(key=lambda h: h.name) 166 | 167 | for objc_proto in objc_image.protolist: 168 | objc_proto.methods.sort(key=lambda h: h.signature) 169 | objc_proto.opt_methods.sort(key=lambda h: h.signature) 170 | 171 | for header_name, header in HeaderGenerator(objc_image, 172 | forward_declare_private_includes=forward_declare_private_imports).headers.items(): 173 | out[header_name] = header 174 | 175 | return out 176 | 177 | 178 | def generate_text_based_stub(image: Image, compatibility=True) -> str: 179 | generator = TBDGenerator(image, compatibility) 180 | return TapiYAMLWriter.write_out(generator.dict) 181 | 182 | 183 | def macho_combine(slices: List[Slice]) -> BytesIO: 184 | fat_generator = FatMachOGenerator(slices) 185 | 186 | fat_file = BytesIO() 187 | fat_file.write(fat_generator.fat_head) 188 | 189 | for arch in fat_generator.fat_archs: 190 | fat_file.seek(arch.offset) 191 | fat_file.write(arch.slice.full_bytes_for_slice()) 192 | 193 | fat_file.seek(0) 194 | return fat_file 195 | -------------------------------------------------------------------------------- /src/ktool/structs.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # structs.py 4 | # 5 | # This file contains objc2 structs conforming to the ktool_macho struct system. 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | from lib0cyn.structs import * 16 | 17 | 18 | class objc2_class(Struct): 19 | FIELDS = { 20 | 'isa': uintptr_t, 21 | 'superclass': uintptr_t, 22 | 'cache': uintptr_t, 23 | 'vtable': uintptr_t, 24 | 'info': uintptr_t 25 | } 26 | 27 | def __init__(self, byte_order="little"): 28 | super().__init__(byte_order=byte_order) 29 | self.isa = 0 30 | self.superclass = 0 31 | self.cache = 0 32 | self.vtable = 0 33 | self.info = 0 34 | 35 | 36 | class objc2_class_ro(Struct): 37 | FIELDS = { 38 | 'flags': uint32_t, 39 | 'ivar_base_start': uint32_t, 40 | 'ivar_base_size': uint32_t, 41 | 'reserved': pad_for_64_bit_only(4), 42 | 'ivar_lyt': uintptr_t, 43 | 'name': uintptr_t, 44 | 'base_meths': uintptr_t, 45 | 'base_prots': uintptr_t, 46 | 'ivars': uintptr_t, 47 | 'weak_ivar_lyt': uintptr_t, 48 | 'base_props': uintptr_t 49 | } 50 | 51 | def __init__(self, byte_order="little"): 52 | super().__init__(byte_order=byte_order) 53 | self.flags = 0 54 | self.ivar_base_start = 0 55 | self.ivar_base_size = 0 56 | self.reserved = 0 57 | self.ivar_lyt = 0 58 | self.name = 0 59 | self.base_meths = 0 60 | self.base_prots = 0 61 | self.ivars = 0 62 | self.weak_ivar_lyt = 0 63 | self.base_props = 0 64 | 65 | 66 | class objc2_meth(Struct): 67 | FIELDS = { 68 | 'selector': uintptr_t, 69 | 'types': uintptr_t, 70 | 'imp': uintptr_t 71 | } 72 | 73 | def __init__(self, byte_order="little"): 74 | super().__init__(byte_order=byte_order) 75 | self.selector = 0 76 | self.types = 0 77 | self.imp = 0 78 | 79 | 80 | class objc2_meth_list_entry(Struct): 81 | FIELDS = { 82 | 'selector': uint32_t, 83 | 'types': uint32_t, 84 | 'imp': uint32_t 85 | } 86 | 87 | def __init__(self, byte_order="little"): 88 | super().__init__(byte_order=byte_order) 89 | self.selector = 0 90 | self.types = 0 91 | self.imp = 0 92 | 93 | 94 | class objc2_meth_list(Struct): 95 | FIELDS = { 96 | 'entrysize': uint32_t, 97 | 'count': uint32_t 98 | } 99 | 100 | def __init__(self, byte_order="little"): 101 | super().__init__(byte_order=byte_order) 102 | self.entrysize = 0 103 | self.count = 0 104 | 105 | 106 | class objc2_prop_list(Struct): 107 | FIELDS = { 108 | 'entrysize': uint32_t, 109 | 'count': uint32_t 110 | } 111 | 112 | def __init__(self, byte_order="little"): 113 | super().__init__(byte_order=byte_order) 114 | self.entrysize = 0 115 | self.count = 0 116 | 117 | 118 | class objc2_prop(Struct): 119 | FIELDS = { 120 | 'name': uintptr_t, 121 | 'attr': uintptr_t 122 | } 123 | 124 | def __init__(self, byte_order="little"): 125 | super().__init__(byte_order=byte_order) 126 | 127 | self.name = 0 128 | self.attr = 0 129 | 130 | 131 | class objc2_prot_list(Struct): 132 | FIELDS = { 133 | 'cnt': uint64_t 134 | } 135 | 136 | def __init__(self, byte_order="little"): 137 | super().__init__(byte_order=byte_order) 138 | self.cnt = 0 139 | 140 | 141 | class objc2_prot(Struct): 142 | FIELDS = { 143 | 'isa': uintptr_t, 144 | 'name': uintptr_t, 145 | 'prots': uintptr_t, 146 | 'inst_meths': uintptr_t, 147 | 'class_meths': uintptr_t, 148 | 'opt_inst_meths': uintptr_t, 149 | 'opt_class_meths': uintptr_t, 150 | 'inst_props': uintptr_t, 151 | 'cb': uint32_t, 152 | 'flags': uint32_t 153 | } 154 | 155 | def __init__(self, byte_order="little"): 156 | super().__init__(byte_order=byte_order) 157 | self.isa = 0 158 | self.name = 0 159 | self.prots = 0 160 | self.inst_meths = 0 161 | self.class_meths = 0 162 | self.opt_inst_meths = 0 163 | self.opt_class_meths = 0 164 | self.inst_props = 0 165 | self.cb = 0 166 | self.flags = 0 167 | 168 | 169 | class objc2_ivar_list(Struct): 170 | FIELDS = { 171 | 'entrysize': uint32_t, 172 | 'cnt': uint32_t 173 | } 174 | 175 | def __init__(self, byte_order="little"): 176 | super().__init__(byte_order=byte_order) 177 | self.entrysize = 0 178 | self.cnt = 0 179 | 180 | 181 | class objc2_ivar(Struct): 182 | FIELDS = { 183 | 'offs': uintptr_t, 184 | 'name': uintptr_t, 185 | 'type': uintptr_t, 186 | 'align': uint32_t, 187 | 'size': uint32_t 188 | } 189 | 190 | def __init__(self, byte_order="little"): 191 | super().__init__(byte_order=byte_order) 192 | self.offs = 0 193 | self.name = 0 194 | 195 | 196 | class objc2_category(Struct): 197 | FIELDS = { 198 | 'name': uintptr_t, 199 | 's_class': uintptr_t, 200 | 'inst_meths': uintptr_t, 201 | 'class_meths': uintptr_t, 202 | 'prots': uintptr_t, 203 | 'props': uintptr_t 204 | } 205 | 206 | def __init__(self, byte_order="little"): 207 | super().__init__(byte_order=byte_order) 208 | self.name = 0 209 | self.s_class = 0 210 | self.inst_meths = 0 211 | self.class_meths = 0 212 | self.prots = 0 213 | self.props = 0 214 | -------------------------------------------------------------------------------- /src/ktool/swift.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # swift.py 4 | # 5 | # Swift type processing 6 | # 7 | # Comments here are currently from me reverse engineering type ser 8 | # 9 | # I have a habit of REing things that are technically publicly available, 10 | # because this is the way I like to write these parsers, it's far less boring, 11 | # and gives me a better initial understanding/foothold. 12 | # 13 | # So please note that comments, etc may be inaccurate until I eventually get around to 14 | # diving into the swift compiler. 15 | # 16 | # https://belkadan.com/blog/2020/09/Swift-Runtime-Type-Metadata/ 17 | # https://knight.sc/reverse%20engineering/2019/07/17/swift-metadata.html 18 | # 19 | # This file is part of ktool. ktool is free software that 20 | # is made available under the MIT license. Consult the 21 | # file "LICENSE" that is distributed together with this file 22 | # for the exact licensing terms. 23 | # 24 | # Copyright (c) 0cyn 2021. 25 | # 26 | 27 | from ktool_macho.base import Constructable 28 | from ktool_swift.structs import * 29 | from ktool_swift.demangle import demangle 30 | 31 | from ktool.loader import Image 32 | from ktool.macho import Section 33 | from ktool.objc import Class 34 | from lib0cyn.log import log 35 | from ktool.util import uint_to_int 36 | 37 | 38 | class Field: 39 | def __init__(self, flags, type_name, name): 40 | self.flags = flags 41 | self.type_name = type_name 42 | self.name = name 43 | 44 | def __str__(self): 45 | return f'{self.name} : {self.type_name} ({hex(self.flags)})' 46 | 47 | 48 | class _FieldDescriptor(Constructable): 49 | 50 | @classmethod 51 | def from_image(cls, objc_image, location): 52 | image = objc_image.image 53 | fields = [] 54 | 55 | fd = image.read_struct(location, FieldDescriptor, vm=True) 56 | 57 | for i in range(fd.NumFields): 58 | ea = location + FieldDescriptor.size() + (i * 0xc) 59 | record = image.read_struct(ea, FieldRecord, vm=True, force_reload=True) 60 | 61 | flags = record.Flags 62 | type_name_loc = ea + 4 + record.MangledTypeName 63 | name_loc = ea + 8 + record.FieldName 64 | try: 65 | name = image.read_cstr(name_loc, vm=True) 66 | except ValueError: 67 | name = "" 68 | except IndexError: 69 | name = "" 70 | try: 71 | type_name = image.read_cstr(type_name_loc, vm=True) 72 | except ValueError: 73 | type_name = "" 74 | except IndexError: 75 | type_name = "" 76 | 77 | fields.append(Field(flags, type_name, name)) 78 | 79 | return cls(fields, fd) 80 | 81 | @classmethod 82 | def from_values(cls, *args, **kwargs): 83 | pass 84 | 85 | def raw_bytes(self): 86 | pass 87 | 88 | def __init__(self, fields, desc): 89 | self.fields = fields 90 | self.desc = desc 91 | 92 | 93 | class SwiftStruct(Constructable): 94 | 95 | @classmethod 96 | def from_image(cls, objc_image: 'ObjCImage', type_location): 97 | image = objc_image.image 98 | struct_desc = image.read_struct(type_location, StructDescriptor, vm=True) 99 | name = image.read_cstr(type_location + 8 + struct_desc.Name, vm=True) 100 | 101 | # 102 | field_desc = _FieldDescriptor.from_image(objc_image, type_location + (4*4) + struct_desc.FieldDescriptor) 103 | 104 | return cls(name, field_desc) 105 | 106 | @classmethod 107 | def from_values(cls, *args, **kwargs): 108 | pass 109 | 110 | def raw_bytes(self): 111 | pass 112 | 113 | def __init__(self, name, field_desc: _FieldDescriptor): 114 | self.name = name 115 | self.field_desc = field_desc 116 | self.fields = field_desc.fields 117 | 118 | 119 | class SwiftClass(Constructable): 120 | 121 | @classmethod 122 | def from_image(cls, image: Image, objc_image: 'ObjCImage', type_location): 123 | class_descriptor = image.read_struct(type_location, ClassDescriptor, vm=True) 124 | name = image.read_cstr(type_location + 8 + class_descriptor.Name, vm=True) 125 | fd_loc = class_descriptor.FieldDescriptor + type_location + 16 126 | field_descriptor = _FieldDescriptor.from_image(objc_image, fd_loc) 127 | ivars = [] 128 | 129 | for objc_class in objc_image.classlist: 130 | mangled_name = objc_class.name 131 | project, classname = demangle(mangled_name) 132 | if classname == name: 133 | objc_backing_class: Class = objc_class 134 | ivars = objc_backing_class.ivars 135 | name = f'{project}.{name}' 136 | 137 | return cls(name, field_descriptor.fields, class_descriptor, field_descriptor, ivars) 138 | 139 | @classmethod 140 | def from_values(cls, *args, **kwargs): 141 | pass 142 | 143 | def raw_bytes(self): 144 | pass 145 | 146 | def __init__(self, name, fields, class_descriptor=None, field_descriptor=None, ivars=None): 147 | self.name = name 148 | self.fields = fields 149 | self.class_desc = class_descriptor 150 | self.field_desc = field_descriptor 151 | self.ivars = ivars 152 | 153 | 154 | class SwiftEnum(Constructable): 155 | 156 | @classmethod 157 | def from_image(cls, objc_image, type_location): 158 | image = objc_image.image 159 | enum_descriptor = image.read_struct(type_location, EnumDescriptor, vm=True) 160 | name = image.read_cstr(type_location + 8 + enum_descriptor.Name, vm=True) 161 | field_desc = None 162 | if enum_descriptor.FieldDescriptor != 0: 163 | field_desc = _FieldDescriptor.from_image(objc_image, type_location + (4*4) + enum_descriptor.FieldDescriptor) 164 | 165 | return cls(name, field_desc) 166 | 167 | @classmethod 168 | def from_values(cls, *args, **kwargs): 169 | pass 170 | 171 | def raw_bytes(self): 172 | pass 173 | 174 | def __init__(self, name, field_desc: _FieldDescriptor): 175 | self.name = name 176 | self.field_desc = field_desc 177 | self.fields = field_desc.fields if self.field_desc is not None else [] 178 | 179 | 180 | class SwiftType(Constructable): 181 | 182 | @classmethod 183 | def from_image(cls, image: Image, objc_image, type_location): 184 | kind = ContextDescriptorKind(image.read_uint(type_location, 1, vm=True) & 0x1f) 185 | 186 | if kind == ContextDescriptorKind.Class: 187 | return SwiftClass.from_image(image, objc_image, type_location) 188 | elif kind == ContextDescriptorKind.Struct: 189 | return SwiftStruct.from_image(objc_image, type_location) 190 | elif kind == ContextDescriptorKind.Enum: 191 | return SwiftEnum.from_image(objc_image, type_location) 192 | else: 193 | return None 194 | 195 | @classmethod 196 | def from_values(cls, *args, **kwargs): 197 | pass 198 | 199 | def raw_bytes(self): 200 | pass 201 | 202 | def __init__(self, name, kind, typedesc=None, field_desc=None): 203 | self.name = name 204 | self.kind = kind 205 | self.typedesc = typedesc 206 | self.field_desc = field_desc 207 | 208 | 209 | class SwiftImage(Constructable): 210 | def raw_bytes(self): 211 | pass 212 | 213 | @classmethod 214 | def from_image(cls, objc_image: 'ObjCImage'): 215 | 216 | types: List[SwiftType] = [] 217 | 218 | image = objc_image.image 219 | swift_type_seg_start_sect: Section = image.segments['__TEXT'].sections['__swift5_types'] 220 | for addr in Section.SectionIterator(swift_type_seg_start_sect, vm=True, ptr_size=4): 221 | type_rel = image.read_int(addr, 4) 222 | type_off = addr + type_rel 223 | types.append(SwiftType.from_image(image, objc_image, type_off)) 224 | 225 | return cls(types) 226 | 227 | @classmethod 228 | def from_values(cls): 229 | pass 230 | 231 | def __init__(self, types): 232 | self.types = types 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/ktool/util.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # util.py 4 | # 5 | # This file contains miscellaneous utilities used around ktool 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | import concurrent.futures 15 | import inspect 16 | import os 17 | import sys 18 | import time 19 | from enum import Enum 20 | from typing import List, Union 21 | import re 22 | import shutil 23 | 24 | from ktool_macho import Struct, FAT_CIGAM, FAT_MAGIC, MH_CIGAM, MH_CIGAM_64, MH_MAGIC, MH_MAGIC_64 25 | from ktool.exceptions import * 26 | 27 | import pkg_resources 28 | 29 | import lib0cyn.log as log 30 | 31 | from pygments import highlight 32 | from pygments.formatters.terminal import TerminalFormatter 33 | try: 34 | from pygments.lexers.data import YamlLexer, JsonLexer 35 | from pygments.lexers.html import XmlLexer 36 | except: 37 | YamlLexer = None 38 | XmlLexer = None 39 | JsonLexer = None 40 | 41 | try: 42 | KTOOL_VERSION = pkg_resources.get_distribution('k2l').version 43 | except pkg_resources.DistributionNotFound: 44 | KTOOL_VERSION = '1.0.0' 45 | THREAD_COUNT = os.cpu_count() - 1 46 | 47 | OUT_IS_TTY = sys.stdout.isatty() 48 | 49 | MY_DIR = __file__ 50 | 51 | 52 | def get_terminal_size(): 53 | # We use this instead of shutil.get_terminal_size, because when output is being piped, it returns column width 80 54 | # We want to make sure if output is being piped (for example, to grep), that no wrapping occurs, so greps will 55 | # always display all relevant info on a single line. This also helps if it's being piped into a file, 56 | # for processing purposes among everything else. 57 | try: 58 | return os.get_terminal_size() 59 | except OSError: 60 | return shutil.get_terminal_size() 61 | 62 | 63 | def version_output(): 64 | if OUT_IS_TTY: 65 | pass 66 | 67 | print(f'ktool v{KTOOL_VERSION}. by cynder. gh/0cyn') 68 | 69 | 70 | class ignore: 71 | MALFORMED = False 72 | OBJC_ERRORS = True 73 | 74 | 75 | class opts: 76 | DISABLE_COLOR = False 77 | USE_SYMTAB_INSTEAD_OF_SELECTORS = False 78 | OBJC_LOAD_ERRORS_SEND_TO_DEBUG = False 79 | 80 | 81 | class QueueItem: 82 | def __init__(self): 83 | self.args = [] 84 | self.func = None 85 | 86 | 87 | class Queue: 88 | def __init__(self): 89 | self.items: List[QueueItem] = [] 90 | self.returns: List = [] 91 | self.multithread = False 92 | 93 | def process_item(self, item: QueueItem): 94 | try: 95 | return item.func(*item.args) 96 | except Exception as ex: 97 | if not ignore.OBJC_ERRORS: 98 | raise ex 99 | log.log.error("Queueitem failed to process for some unhandled reason.") 100 | return None 101 | 102 | def go(self): 103 | if self.multithread: 104 | futures = [] 105 | with concurrent.futures.ThreadPoolExecutor(max_workers=THREAD_COUNT) as executor: 106 | for item in self.items: 107 | futures.append(executor.submit(item.func, *item.args)) 108 | self.returns = [f.result() for f in futures] 109 | else: 110 | self.returns = [self.process_item(item) for item in self.items] 111 | 112 | 113 | def highlight_xml(input): 114 | if XmlLexer: 115 | formatter = TerminalFormatter() 116 | return highlight(input, XmlLexer(), formatter) 117 | else: 118 | return input 119 | 120 | 121 | def highlight_json(input): 122 | if JsonLexer: 123 | formatter = TerminalFormatter() 124 | return highlight(input, JsonLexer(), formatter) 125 | else: 126 | return input 127 | 128 | 129 | def macho_is_malformed(): 130 | """Raise MalformedMachOException *if* we dont want to ignore bad mach-os 131 | 132 | :return: 133 | """ 134 | if not ignore.MALFORMED: 135 | raise MalformedMachOException 136 | 137 | 138 | def uint_to_int(val, bits): 139 | """ 140 | Assume an int was read from binary as an unsigned int, 141 | 142 | decode it as a two's compliment signed integer 143 | 144 | :param uint: 145 | :param bits: 146 | :return: 147 | """ 148 | if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 149 | val = val - (1 << bits) # compute negative value 150 | return val 151 | 152 | def usi32_to_si32(val): 153 | """ 154 | Quick hack to read the signed val of an unsigned int (Image loads all ints from bytes as unsigned ints) 155 | 156 | :param val: 157 | :return: 158 | """ 159 | bits = 32 160 | if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 161 | val = val - (1 << bits) # compute negative value 162 | return val # return positive value as is 163 | 164 | 165 | class FileType(Enum): 166 | MachOFileType = 0 167 | FatMachOFileType = 1 168 | KCacheFileType = 2 169 | IMG4FileType = 64 170 | SharedCacheFileType = 128 171 | UnknownFileType = 512 172 | 173 | 174 | def detect_filetype(fp) -> FileType: 175 | magic = fp.read(4) 176 | if magic in [FAT_MAGIC, FAT_CIGAM]: 177 | return FileType.FatMachOFileType 178 | elif magic in [MH_MAGIC, MH_CIGAM, MH_MAGIC_64, MH_CIGAM_64]: 179 | first1k = fp.read(0x1000) 180 | fp.seek(0) 181 | if b'__BOOTDATA\x00\x00\x00\x00\x00\x00' in first1k: 182 | return FileType.KCacheFileType 183 | else: 184 | return FileType.MachOFileType 185 | 186 | elif magic == b'dyld': 187 | return FileType.SharedCacheFileType 188 | 189 | 190 | class TapiYAMLWriter: 191 | 192 | @staticmethod 193 | def write_out(tapi_dict: dict): 194 | text = ["---", "archs:".ljust(23) + TapiYAMLWriter.serialize_list(tapi_dict['archs']), 195 | "platform:".ljust(23) + tapi_dict['platform'], "install-name:".ljust(23) + tapi_dict['install-name'], 196 | "current-version:".ljust(23) + str(tapi_dict['current-version']), 197 | "compatibility-version: " + str(tapi_dict['compatibility-version']), "exports:"] 198 | for arch in tapi_dict['exports']: 199 | text.append(TapiYAMLWriter.serialize_export_arch(arch)) 200 | text.append('...') 201 | formatter = TerminalFormatter() 202 | highlighted_text = highlight('\n'.join(text), YamlLexer(), formatter) 203 | return highlighted_text 204 | 205 | @staticmethod 206 | def serialize_export_arch(export_dict): 207 | text = [' - ' + 'archs:'.ljust(22) + TapiYAMLWriter.serialize_list(export_dict['archs'])] 208 | if 'allowed-clients' in export_dict: 209 | text.append( 210 | ' ' + 'allowed-clients:'.ljust(22) + TapiYAMLWriter.serialize_list(export_dict['allowed-clients'])) 211 | if 'symbols' in export_dict: 212 | text.append(' ' + 'symbols:'.ljust(22) + TapiYAMLWriter.serialize_list(export_dict['symbols'])) 213 | if 'objc-classes' in export_dict: 214 | text.append(' ' + 'objc-classes:'.ljust(22) + TapiYAMLWriter.serialize_list(export_dict['objc-classes'])) 215 | if 'objc-ivars' in export_dict: 216 | text.append(' ' + 'objc-ivars:'.ljust(22) + TapiYAMLWriter.serialize_list(export_dict['objc-ivars'])) 217 | return '\n'.join(text) 218 | 219 | @staticmethod 220 | def serialize_list(slist): 221 | text = "[ " 222 | wraplen = 55 223 | lpad = 28 224 | stack = [] 225 | for item in slist: 226 | if len(', '.join(stack)) + len(item) > wraplen and len(stack) > 0: 227 | text += ', '.join(stack) + ',\n' + ''.ljust(lpad) 228 | stack = [] 229 | stack.append(item) 230 | text += ', '.join(stack) + " ]" 231 | return text 232 | 233 | 234 | class Table: 235 | """ 236 | ASCII Table Renderer 237 | .titles = a list of titles for each column 238 | .rows is a list of lists, each "sublist" representing each column, .e.g self.rows.append(['col1thing', 'col2thing']) 239 | 240 | .column_pad (default is 2 (without dividers)) 241 | 242 | This can be used with and without curses; 243 | you just need to set the max width it can be rendered at on the render call. 244 | (shutil.get_terminal_size) 245 | """ 246 | 247 | def __init__(self, dividers=False, avoid_wrapping_titles=False): 248 | self.titles = [] 249 | self.rows = [] 250 | self.size_pinned_columns = [] 251 | 252 | self.dividers = dividers 253 | self.avoid_wrapping_titles = avoid_wrapping_titles 254 | self.ansi_borders = True 255 | 256 | self.column_pad = 3 if dividers else 2 257 | 258 | # Holds the maximum length of the fields within the seperate columns 259 | self.column_maxes = [] 260 | # Most recently calculated maxes (not thread safe) 261 | self.most_recent_adjusted_maxes = [] 262 | 263 | # width-based caches for loaded and rendered columns 264 | self.rendered_row_cache = {} 265 | self.header_cache = {} 266 | 267 | def preheat(self): 268 | """ 269 | Call this whenever there's a second to do so, to pre-run a few width-independent calculations 270 | 271 | :return: 272 | """ 273 | self.column_maxes = [0 for _ in self.titles] 274 | self.most_recent_adjusted_maxes = [*self.column_maxes] 275 | 276 | # Iterate through each row, 277 | for row in self.rows: 278 | # And in each row, iterate through each column 279 | for index, col in enumerate(row): 280 | # Check the length of this column; if it's larger than the length in the array, 281 | # set the max to the new one 282 | col_size = max([len(i) + self.column_pad for i in col.split('\n')]) 283 | self.column_maxes[index] = max(col_size, self.column_maxes[index]) 284 | 285 | # If the titles are longer than any of the items in that column, account for those too 286 | for i, title in enumerate(self.titles): 287 | self.column_maxes[i] = max(self.column_maxes[i], len(title) + 1 + len(self.titles)) 288 | 289 | def fetch_all(self, screen_width): 290 | """ 291 | Render the entirety of the table for a screen width 292 | 293 | (avoid calling this in GUI, only use it in CLI) 294 | 295 | :param screen_width: 296 | :return: 297 | """ 298 | # This function effectively replaces the previous usage of .render() and does it all in one go. 299 | return self.fetch(0, len(self.rows), screen_width) 300 | 301 | def fetch(self, row_start, row_count, screen_width): 302 | """ 303 | Cache-based batch processing and rendering 304 | 305 | Will spit out a generated table for screen_width containing row_count rows. 306 | 307 | :param row_start: Start index to load 308 | :param row_count: Amount from index to load 309 | :param screen_width: Screen width 310 | :return: 311 | """ 312 | 313 | cgrey = '\33[0m\33[38;5;242m' 314 | reset = '\33[0m' 315 | cwhitebold = '\33[0m\33[1m' 316 | cend = '\33[0m\33[39m' 317 | if opts.DISABLE_COLOR: 318 | cgrey = reset 319 | cend = reset 320 | 321 | if row_count == 0: 322 | return "" 323 | rows = [] 324 | if screen_width in self.rendered_row_cache: 325 | for i in range(row_start, row_start + row_count): 326 | if str(i) in self.rendered_row_cache[screen_width]: 327 | rows.append(self.rendered_row_cache[screen_width][str(i)]) 328 | else: 329 | break 330 | else: 331 | self.rendered_row_cache[screen_width] = {} 332 | r_row_count = row_count - len(rows) 333 | r_start = row_start + len(rows) 334 | rows_text = ''.join([i + '\n' for i in rows]) 335 | sep_line = "" 336 | 337 | rows_text += self.render(self.rows[r_start:r_start + r_row_count], screen_width, r_start) 338 | 339 | if self.dividers: 340 | rows_text = rows_text[:-1] # cut off the "\n" 341 | sep_line = '┣━' 342 | for size in self.most_recent_adjusted_maxes: 343 | sep_line += ''.ljust(size - 2, '━') + '╋━' 344 | sep_line = cgrey + sep_line[:-self.column_pad].ljust(screen_width - 1, '━')[ 345 | :-self.column_pad] + '━━━┫' + cend 346 | 347 | rows_text = rows_text[:-(len(sep_line))] # Use our calculated sep_line length to cut off the last one 348 | rows_text += sep_line.replace('┣', '┗').replace('╋', '┻').replace('┫', '┛') 349 | 350 | if screen_width in self.header_cache: 351 | rows_text = self.header_cache[screen_width] + rows_text 352 | else: 353 | title_row = '' 354 | for i, title in enumerate(self.titles): 355 | if self.dividers: 356 | try: 357 | title_row += cgrey + '┃ ' + cwhitebold + title.ljust(self.most_recent_adjusted_maxes[i], ' ')[ 358 | :-(self.column_pad - 1)] 359 | except IndexError: 360 | # I have no idea what causes this 361 | title_row = "" 362 | else: 363 | try: 364 | title_row += ' ' + title.ljust(self.most_recent_adjusted_maxes[i], ' ')[:-(self.column_pad - 1)] 365 | except IndexError: 366 | title_row = "" 367 | header_text = "" 368 | if self.dividers: 369 | header_text += cgrey + sep_line.replace('┣', '┏').replace('╋', '┳').replace('┫', '┓') + cwhitebold + '\n' 370 | header_text += title_row.ljust(screen_width - 1)[ 371 | :-1] + cgrey + ' ┃\n' + cwhitebold if self.dividers else cwhitebold + title_row + reset + '\n' 372 | if self.dividers: 373 | header_text += sep_line + '\n' 374 | self.header_cache[screen_width] = header_text 375 | rows_text = header_text + rows_text 376 | 377 | rows_text = rows_text.replace('┣', cgrey + '┣').replace('┫', '┫' + cend) 378 | return rows_text 379 | 380 | # noinspection PyUnreachableCode 381 | def render(self, _rows, width, row_start): 382 | """ 383 | Render a list of rows for screen_width 384 | 385 | :param _rows: list of rows to be rendered 386 | :param width: Screen width 387 | :param row_start: Starting index of rows (for the sake of cacheing) 388 | :return: 389 | """ 390 | 391 | width -= 1 392 | 393 | if len(_rows) == 0: 394 | return "" 395 | 396 | if not len(self.column_maxes) > 0: 397 | self.preheat() 398 | 399 | column_maxes = [*self.column_maxes] 400 | 401 | # if column widths aren't large enough for the width, fill out the last col. 402 | if sum(column_maxes) < width: 403 | column_maxes[-1] += width - sum(column_maxes) + 1 404 | 405 | # Minimum Column Size 406 | col_min = min(column_maxes) 407 | 408 | # Iterate through column maxes, subtracting one from each until they fit within the passed width arg 409 | last_sum = 0 410 | while sum(column_maxes) >= width: 411 | for index, i, in enumerate(column_maxes): 412 | if index in self.size_pinned_columns: 413 | continue 414 | if self.avoid_wrapping_titles: 415 | column_maxes[index] = max(col_min, column_maxes[index] - 1, len(self.titles[index]) + 3) 416 | else: 417 | column_maxes[index] = max(col_min, column_maxes[index] - 1) 418 | if sum(column_maxes) == last_sum: 419 | return 'Width too small to render table' 420 | last_sum = sum(column_maxes) 421 | 422 | self.most_recent_adjusted_maxes = [*column_maxes] 423 | 424 | def split_handling_ansi(input_string, split_length): 425 | """ 426 | Splits the input_string into chunks of split_length, taking into account 427 | ANSI escape sequences and trying to wrap whole words. 428 | """ 429 | parts = [] 430 | current_part = '' 431 | current_length = 0 432 | current_color = '\x1b[0m' 433 | i = 0 434 | while i < len(input_string): 435 | match = ansi_escape.match(input_string, i) 436 | if match: 437 | # Include the ANSI sequence without adding to the length 438 | current_color = match.group() 439 | current_part += match.group() 440 | i += len(match.group()) 441 | else: 442 | space_pos = input_string.find(' ', i) 443 | newline_pos = input_string.find('\n', i) 444 | next_break = min(space_pos if space_pos != -1 else len(input_string), 445 | newline_pos if newline_pos != -1 else len(input_string)) 446 | word_end = next_break if next_break != -1 else len(input_string) 447 | word_length = strip_ansi(input_string[i:word_end]).__len__() 448 | 449 | if current_length + word_length + 6 <= split_length or current_length == 0: 450 | # Add the word to the current line 451 | current_part += input_string[i:word_end] 452 | current_length += word_length 453 | i = word_end 454 | else: 455 | # Finish the current line and start a new one 456 | parts.append(current_part) 457 | current_part = current_color # Reset with the current ANSI color 458 | current_length = 0 459 | 460 | if input_string[i:i + 1] == ' ': 461 | # Include the space in the current part if it's not at the end 462 | current_part += ' ' 463 | i += 1 464 | elif input_string[i:i + 1] == '\n': 465 | # Handle newline: finish the current part and reset 466 | parts.append(current_part) 467 | current_part = current_color # Reset with the current ANSI color 468 | current_length = 0 469 | i += 1 470 | 471 | if strip_ansi(current_part): 472 | parts.append(current_part) 473 | return parts 474 | 475 | rows = [] 476 | for row_i, row in enumerate(_rows): 477 | cols = [] 478 | max_line_count_in_row = 0 479 | for col_i, col in enumerate(row): 480 | lines = [] 481 | column_width = column_maxes[col_i] - self.column_pad 482 | wrapped_lines = split_handling_ansi(col, column_width) 483 | for line in wrapped_lines: 484 | # Splitting further if there are newline characters in the wrapped line 485 | lines.extend(line.split('\n')) 486 | max_line_count_in_row = max(len(lines), max_line_count_in_row) 487 | cols.append(lines) 488 | for col in cols: 489 | while len(col) < max_line_count_in_row: 490 | col.append('') 491 | rows.append(cols) 492 | 493 | lines = "" 494 | sep_line = "" 495 | 496 | cgrey = '\33[0m\33[38;5;242m' 497 | reset = '\33[0m' 498 | cend = '\33[0m\33[39m' 499 | if opts.DISABLE_COLOR: 500 | cgrey = reset 501 | cend = reset 502 | 503 | if self.dividers: 504 | sep_line = '┣━' 505 | for size in column_maxes: 506 | sep_line += ''.ljust(size - 2, '━') + '╋━' 507 | sep_line = sep_line[:-self.column_pad].ljust(width, '━')[:-self.column_pad] + '━━━┫' 508 | 509 | if self.dividers: 510 | lines += sep_line + '\n' 511 | 512 | for row_index, row in enumerate(rows): 513 | row_lines = [] 514 | column_count = len(row[0]) 515 | for i in range(0, column_count): 516 | line = "" 517 | for j, col in enumerate(row): 518 | diff = column_maxes[j] - len(strip_ansi(col[i])) 519 | line += col[i] + (' ' * diff) 520 | if self.dividers: 521 | line = line[:-self.column_pad] + f' ┃ ' 522 | if self.dividers: 523 | diff = width - len(strip_ansi(line)) 524 | line = cgrey + '┃ ' + reset + (line + (' ' * diff))[:-self.column_pad] + cgrey + ' ┃ ' + cend 525 | line = line.replace('┃', cgrey + '┃' + reset) 526 | else: 527 | line = ' ' + line[:-self.column_pad].ljust(width, ' ')[:-self.column_pad] + (' ' * self.column_pad) 528 | row_lines.append(line) 529 | 530 | if self.dividers: 531 | row_lines.append(cgrey + sep_line + cend) 532 | 533 | self.rendered_row_cache[width + 1][str(row_index + row_start)] = '\n'.join(row_lines) 534 | lines += '\n'.join(row_lines) 535 | lines += '\n' 536 | return lines 537 | 538 | 539 | ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') 540 | 541 | 542 | def strip_ansi(msg): 543 | return ansi_escape.sub('', msg) 544 | 545 | 546 | def bytes_to_hex(data: Union[bytes, bytearray]) -> str: 547 | return data.hex() 548 | 549 | 550 | def ktool_print(msg, file=sys.stdout): 551 | if file.isatty(): 552 | print(msg, file=file) 553 | else: 554 | print(strip_ansi(msg), file=file) 555 | 556 | 557 | def print_err(msg): 558 | print(msg, file=sys.stderr) 559 | -------------------------------------------------------------------------------- /src/ktool_macho/__init__.py: -------------------------------------------------------------------------------- 1 | from .mach_header import * 2 | from .binding import * 3 | -------------------------------------------------------------------------------- /src/ktool_macho/base.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # base.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | from abc import ABC, abstractmethod 16 | 17 | 18 | class Constructable(ABC): 19 | """ 20 | This is an attempt to define a standardized API for objects we load and may want to create. 21 | 22 | The idea is that all objects should be loadable and serializable in both directions, to allow patching, creation, 23 | and standard loading, with hopefully not too much overhead being shared between the three. 24 | 25 | """ 26 | 27 | @classmethod 28 | @abstractmethod 29 | def from_image(cls, *args, **kwargs): 30 | """ 31 | Base method for serializing an instance of the subclass based on raw bytes 32 | 33 | Implementation/Args left up to implementations, but should usually follow `from_bytes(raw: bytes)` 34 | 35 | :return: 36 | """ 37 | 38 | @classmethod 39 | @abstractmethod 40 | def from_values(cls, *args, **kwargs): 41 | """ 42 | Base method for serializing an instance of the subclass based on the required set of values to create it. 43 | 44 | Implementation and argument structure of this is definitely left up to subclasses. 45 | 46 | :return: 47 | """ 48 | 49 | @abstractmethod 50 | def raw_bytes(self): 51 | """ 52 | Built or stored raw byte representation of this item 53 | 54 | :return: 55 | """ 56 | -------------------------------------------------------------------------------- /src/ktool_macho/binding.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # macho.py 4 | # 5 | # This file contains pythonized representations of certain #defines and enums from dyld source 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | 16 | from enum import IntEnum 17 | 18 | 19 | class REBASE_OPCODE(IntEnum): 20 | DONE = 0x0 21 | SET_TYPE_IMM = 0x10 22 | SET_SEGMENT_AND_OFFSET_ULEB = 0x20 23 | ADD_ADDR_ULEB = 0x30 24 | ADD_ADDR_IMM_SCALED = 0x40 25 | DO_REBASE_IMM_TIMES = 0x50 26 | DO_REBASE_ULEB_TIMES = 0x60 27 | DO_REBASE_ADD_ADDR_ULEB = 0x70 28 | DO_REBASE_ULEB_TIMES_SKIPPING_ULEB = 0x80 29 | 30 | 31 | class BINDING_OPCODE(IntEnum): 32 | DONE = 0x0 33 | SET_DYLIB_ORDINAL_IMM = 0x10 34 | SET_DYLIB_ORDINAL_ULEB = 0x20 35 | SET_DYLIB_SPECIAL_IMM = 0x30 36 | SET_SYMBOL_TRAILING_FLAGS_IMM = 0x40 37 | SET_TYPE_IMM = 0x50 38 | SET_ADDEND_SLEB = 0x60 39 | SET_SEGMENT_AND_OFFSET_ULEB = 0x70 40 | ADD_ADDR_ULEB = 0x80 41 | DO_BIND = 0x90 42 | DO_BIND_ADD_ADDR_ULEB = 0xa0 43 | DO_BIND_ADD_ADDR_IMM_SCALED = 0xb0 44 | DO_BIND_ULEB_TIMES_SKIPPING_ULEB = 0xc0 45 | THREADED = 0xd0 46 | 47 | 48 | BIND_SUBOPCODE_THREADED_SET_BIND_ORDINAL_TABLE_SIZE_ULEB = 0x00 49 | BIND_SUBOPCODE_THREADED_APPLY = 0x01 50 | -------------------------------------------------------------------------------- /src/ktool_macho/codesign.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # codesign.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | 15 | from ktool_macho.structs import Struct, uint32_t 16 | 17 | CSMAGIC_REQUIREMENT = 0xfade0c00 18 | CSMAGIC_REQUIREMENTS = 0xfade0c01 19 | CSMAGIC_CODEDIRECTORY = 0xfade0c02 20 | CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0 21 | CSMAGIC_EMBEDDED_SIGNATURE_OLD = 0xfade0b02 22 | CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171 23 | CSMAGIC_EMBEDDED_DERFORMAT = 0xfade7172 24 | CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1 25 | CSMAGIC_BLOBWRAPPER = 0xfade0b01 26 | 27 | CSSLOT_CODEDIRECTORY = 0x00000 28 | CSSLOT_INFOSLOT = 0x00001 29 | CSSLOT_REQUIREMENTS = 0x00002 30 | CSSLOT_RESOURCEDIR = 0x00003 31 | CSSLOT_APPLICATION = 0x00004 32 | CSSLOT_ENTITLEMENTS = 0x00005 33 | CSSLOT_REPSPECIFIC = 0x00006 34 | CSSLOT_DERFORMAT = 0x00007 35 | CSSLOT_ALTERNATE = 0x01000 36 | 37 | 38 | class BlobIndex(Struct): 39 | _FIELDNAMES = ["type", "offset"] 40 | _SIZES = [uint32_t, uint32_t] 41 | SIZE = 8 42 | 43 | def __init__(self, byte_order="little"): 44 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 45 | self.type = 0 46 | self.offset = 0 47 | 48 | 49 | class Blob(Struct): 50 | _FIELDNAMES = ["magic", "length"] 51 | _SIZES = [uint32_t, uint32_t] 52 | SIZE = 8 53 | 54 | def __init__(self, byte_order="little"): 55 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 56 | self.magic = 0 57 | self.length = 0 58 | 59 | 60 | class SuperBlob(Struct): 61 | _FIELDNAMES = ["blob", "count"] 62 | _SIZES = [Blob, uint32_t] 63 | SIZE = 12 64 | 65 | def __init__(self, byte_order="little"): 66 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 67 | self.blob = 0 68 | self.count = 0 69 | -------------------------------------------------------------------------------- /src/ktool_macho/fixups.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # fixups.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | from enum import Enum 16 | 17 | from ktool_macho.structs import * 18 | 19 | 20 | class dyld_chained_fixups_header(Struct): 21 | _FIELDNAMES = ['fixups_version', 'starts_offset', 'imports_offset', 'symbols_offset', 'imports_count', 22 | 'imports_format', 'symbols_format'] 23 | _SIZES = [4, 4, 4, 4, 4, 4, 4] 24 | SIZE = sum(_SIZES) 25 | 26 | def __init__(self, byte_order="little"): 27 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 28 | 29 | 30 | class dyld_chained_starts_in_image(Struct): 31 | _FIELDNAMES = ['seg_count', 'seg_info_offset'] 32 | _SIZES = [4, 4] 33 | SIZE = sum(_SIZES) 34 | 35 | def __init__(self, byte_order="little"): 36 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 37 | 38 | 39 | class dyld_chained_starts_in_segment(Struct): 40 | _FIELDNAMES = ['size', 'page_size', 'pointer_format', 'segment_offset', 'max_valid_pointer', 'page_count', 41 | 'page_starts'] 42 | _SIZES = [4, 2, 2, 8, 4, 2, 2] 43 | SIZE = sum(_SIZES) 44 | 45 | def __init__(self, byte_order="little"): 46 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 47 | 48 | 49 | DYLD_CHAINED_PTR_START_NONE = 0xFFFF 50 | DYLD_CHAINED_PTR_START_MULTI = 0x8000 51 | DYLD_CHAINED_PTR_START_LAST = 0x8000 52 | 53 | 54 | class dyld_chained_start_offsets(Struct): 55 | _FIELDNAMES = ['pointer_format', 'starts_count', 'chain_starts'] 56 | _SIZES = [2, 2, 2] 57 | SIZE = sum(_SIZES) 58 | 59 | def __init__(self, byte_order="little"): 60 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 61 | 62 | 63 | """ 64 | enum { 65 | DYLD_CHAINED_PTR_ARM64E = 1, // stride 8, unauth target is vmaddr 66 | DYLD_CHAINED_PTR_64 = 2, // target is vmaddr 67 | DYLD_CHAINED_PTR_32 = 3, 68 | DYLD_CHAINED_PTR_32_CACHE = 4, 69 | DYLD_CHAINED_PTR_32_FIRMWARE = 5, 70 | DYLD_CHAINED_PTR_64_OFFSET = 6, // target is vm offset 71 | DYLD_CHAINED_PTR_ARM64E_OFFSET = 7, // old name 72 | DYLD_CHAINED_PTR_ARM64E_KERNEL = 7, // stride 4, unauth target is vm offset 73 | DYLD_CHAINED_PTR_64_KERNEL_CACHE = 8, 74 | DYLD_CHAINED_PTR_ARM64E_USERLAND = 9, // stride 8, unauth target is vm offset 75 | DYLD_CHAINED_PTR_ARM64E_FIRMWARE = 10, // stride 4, unauth target is vmaddr 76 | DYLD_CHAINED_PTR_X86_64_KERNEL_CACHE = 11, // stride 1, x86_64 kernel caches 77 | there is apparently a 12. it is not in xnu source. i found it in an arm64e userland (Console.app M1) bin, so we're assuming its 78 | like, 1 I guess. 79 | };""" 80 | 81 | 82 | class dyld_chained_ptr_format(Enum): 83 | DYLD_CHAINED_PTR_ARM64E = 1 84 | DYLD_CHAINED_PTR_64 = 2 85 | DYLD_CHAINED_PTR_32 = 3 86 | DYLD_CHAINED_PTR_32_CACHE = 4 87 | DYLD_CHAINED_PTR_32_FIRMWARE = 5 88 | DYLD_CHAINED_PTR_64_OFFSET = 6 89 | DYLD_CHAINED_PTR_ARM64E_OFFSET = 7 90 | DYLD_CHAINED_PTR_ARM64E_KERNEL = 7 91 | DYLD_CHAINED_PTR_64_KERNEL_CACHE = 8 92 | DYLD_CHAINED_PTR_ARM64E_USERLAND = 9 93 | DYLD_CHAINED_PTR_ARM64E_FIRMWARE = 10 94 | DYLD_CHAINED_PTR_x86_64_KERNEL_CACHE = 11 95 | DYLD_CHAINED_PTR_ARM64E_USERLAND24 = 12 96 | 97 | 98 | class dyld_chained_import_format(Enum): 99 | DYLD_CHAINED_IMPORT = 1 100 | DYLD_CHAINED_IMPORT_ADDEND = 2 101 | DYLD_CHAINED_IMPORT_ADDEND64 = 3 102 | 103 | 104 | class ChainedFixupPointerGeneric(Enum): 105 | GenericArm64eFixupFormat = 0 106 | Generic64FixupFormat = 1 107 | Generic32FixupFormat = 2 108 | Firmware32FixupFormat = 3 109 | Error = 4 110 | 111 | 112 | class dyld_chained_import(Struct): 113 | _FIELDS = {"value": Bitfield( 114 | {'lib_ordinal': 8, 115 | 'weak_import': 1, 116 | 'name_offset': 23} 117 | )} 118 | SIZE = uint32_t 119 | 120 | def __init__(self, byte_order="little"): 121 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 122 | 123 | 124 | class dyld_chained_import_addend(Struct): 125 | _FIELDS = {"value": Bitfield( 126 | {'lib_ordinal': 8, 127 | 'weak_import': 1, 128 | 'name_offset': 23} 129 | ), 130 | "addend": int32_t} 131 | SIZE = 8 132 | 133 | def __init__(self, byte_order="little"): 134 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 135 | 136 | 137 | class dyld_chained_import_addend64(Struct): 138 | _FIELDS = {"value": Bitfield( 139 | {'lib_ordinal': 16, 140 | 'weak_import': 1, 141 | 'reserved': 15, 142 | 'name_offset': 32} 143 | ), 144 | "addend": uint64_t} 145 | SIZE = 8 146 | 147 | def __init__(self, byte_order="little"): 148 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 149 | 150 | 151 | class dyld_chained_ptr(Struct): 152 | _FIELDNAMES = ['ptr'] 153 | _SIZES = [8] 154 | SIZE = 8 155 | 156 | def __init__(self, byte_order="little"): 157 | super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order) 158 | 159 | 160 | class dyld_chained_ptr_arm64e(Struct): 161 | _FIELDS = {"reserved": Bitfield({'reserved': 62, 'bind': 1, 'auth': 1})} 162 | SIZE = 8 163 | 164 | def __init__(self, byte_order="little"): 165 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 166 | self.bind = 0 167 | self.auth = 0 168 | 169 | 170 | class dyld_chained_ptr_arm64e_rebase(Struct): 171 | _FIELDS = {'target': Bitfield({'target': 43, 'high8': 8, 'next': 11, 'bind': 1, 'auth': 1})} 172 | SIZE = 8 173 | 174 | def __init__(self, byte_order="little"): 175 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 176 | 177 | 178 | class dyld_chained_ptr_arm64e_bind(Struct): 179 | _FIELDS = {'ordinal': Bitfield({'ordinal': 16, 'zero': 16, 'addend': 19, 'next': 11, 'bind': 1, 'auth': 1})} 180 | SIZE = 8 181 | 182 | def __init__(self, byte_order="little"): 183 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 184 | 185 | 186 | class dyld_chained_ptr_arm64e_auth_rebase(Struct): 187 | _FIELDS = { 188 | 'target': Bitfield({'target': 32, 'diversity': 16, 'addrDiv': 1, 'key': 2, 'next': 11, 'bind': 1, 'auth': 1})} 189 | SIZE = 8 190 | 191 | def __init__(self, byte_order="little"): 192 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 193 | 194 | 195 | class dyld_chained_ptr_arm64e_auth_bind(Struct): 196 | _FIELDS = {'value': Bitfield( 197 | {'ordinal': 16, 'zero': 16, 'diversity': 16, 'addrDiv': 1, 'key': 2, 'next': 11, 'bind': 1, 'auth': 1})} 198 | SIZE = 8 199 | 200 | def __init__(self, byte_order="little"): 201 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 202 | 203 | 204 | class dyld_chained_ptr_64(Struct): 205 | _FIELDS = {'value': Bitfield({'reserved': 63, 'bind': 1})} 206 | SIZE = 8 207 | 208 | def __init__(self, byte_order="little"): 209 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 210 | 211 | 212 | class dyld_chained_ptr_64_rebase(Struct): 213 | _FIELDS = {'value': Bitfield({'target': 36, 'high8': 8, 'reserved': 7, 'next': 12, 'bind': 1})} 214 | SIZE = 8 215 | 216 | def __init__(self, byte_order="little"): 217 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 218 | 219 | 220 | class dyld_chained_ptr_64_bind(Struct): 221 | _FIELDS = {'value': Bitfield({'ordinal': 24, 'addend': 8, 'reserved': 19, 'next': 12, 'bind': 1})} 222 | SIZE = 8 223 | 224 | def __init__(self, byte_order="little"): 225 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 226 | 227 | 228 | class dyld_chained_ptr_arm64e_bind24(Struct): 229 | _FIELDS = {'value': Bitfield({'ordinal': 24, 'zero': 8, 'addend': 19, 'next': 11, 'bind': 1, 'auth': 1, })} 230 | SIZE = 8 231 | 232 | def __init__(self, byte_order="little"): 233 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 234 | 235 | 236 | class dyld_chained_ptr_arm64e_auth_bind24(Struct): 237 | _FIELDS = {'value': Bitfield( 238 | {'ordinal': 24, 'zero': 8, 'diversity': 16, 'addrDiv': 1, 'key': 2, 'next': 11, 'bind': 1, 'auth': 1, })} 239 | SIZE = 8 240 | 241 | def __init__(self, byte_order="little"): 242 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 243 | 244 | 245 | class dyld_chained_ptr_32_rebase(Struct): 246 | _FIELDS = {'value': Bitfield({'target': 26, 'next': 5, 'bind': 1})} 247 | SIZE = 4 248 | 249 | def __init__(self, byte_order="little"): 250 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 251 | 252 | 253 | class dyld_chained_ptr_32_bind(Struct): 254 | _FIELDS = {'value': Bitfield({'ordinal': 20, 'addend': 6, 'next': 5, 'bind': 1})} 255 | SIZE = 4 256 | 257 | def __init__(self, byte_order="little"): 258 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 259 | 260 | 261 | class dyld_chained_ptr_32_cache_rebase(Struct): 262 | _FIELDS = {'value': Bitfield({'target': 30, 'next': 2})} 263 | SIZE = 4 264 | 265 | def __init__(self, byte_order="little"): 266 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 267 | 268 | 269 | class dyld_chained_ptr_32_firmware_rebase(Struct): 270 | _FIELDS = {'value': Bitfield({'target': 26, 'next': 6})} 271 | SIZE = 4 272 | 273 | def __init__(self, byte_order="little"): 274 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 275 | 276 | 277 | class ChainedPointerArm64E(StructUnion): 278 | SIZE = 8 279 | 280 | def __init__(self): 281 | super().__init__(uint64_t, [dyld_chained_ptr_arm64e_auth_rebase, dyld_chained_ptr_arm64e_auth_bind, 282 | dyld_chained_ptr_arm64e_rebase, dyld_chained_ptr_arm64e_bind, dyld_chained_ptr_arm64e_bind24, 283 | dyld_chained_ptr_arm64e_auth_bind24, ]) 284 | 285 | 286 | class ChainedPointerGeneric64(StructUnion): 287 | SIZE = 8 288 | 289 | def __init__(self): 290 | super().__init__(uint64_t, [dyld_chained_ptr_64_rebase, dyld_chained_ptr_64_bind, ]) 291 | 292 | 293 | class ChainedPointerGeneric32(StructUnion): 294 | SIZE = 8 295 | 296 | def __init__(self): 297 | super().__init__(uint64_t, [dyld_chained_ptr_32_rebase, dyld_chained_ptr_32_bind, 298 | dyld_chained_ptr_32_firmware_rebase]) 299 | 300 | 301 | class ChainedFixupPointer64Union(StructUnion): 302 | SIZE = 8 303 | 304 | def __init__(self): 305 | super().__init__(uint64_t, [ChainedPointerArm64E, ChainedPointerGeneric64]) 306 | 307 | 308 | class ChainedFixupPointer32(Struct): 309 | _FIELDS = {'generic32': ChainedPointerGeneric32} 310 | SIZE = 4 311 | 312 | def __init__(self, byte_order="little"): 313 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 314 | 315 | 316 | class ChainedFixupPointer64(Struct): 317 | _FIELDS = {'generic64': ChainedFixupPointer64Union} 318 | SIZE = 8 319 | 320 | def __init__(self, byte_order="little"): 321 | super().__init__(fields=self._FIELDS.keys(), sizes=self._FIELDS.values(), byte_order=byte_order) 322 | -------------------------------------------------------------------------------- /src/ktool_macho/load_commands.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool 3 | # load_commands.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | from typing import Union 15 | 16 | from ktool_macho import segment_command, segment_command_64, section_64, section, SectionType, S_FLAGS_MASKS, Struct, \ 17 | symtab_command, LOAD_COMMAND 18 | from ktool_macho.base import Constructable 19 | 20 | 21 | class LoadCommand(Constructable): 22 | @classmethod 23 | def from_image(cls, *args, **kwargs): 24 | pass 25 | 26 | @classmethod 27 | def from_values(cls, *args, **kwargs): 28 | pass 29 | 30 | def raw_bytes(self): 31 | pass 32 | 33 | 34 | class Section: 35 | """ 36 | 37 | """ 38 | 39 | def __init__(self, cmd): 40 | self.cmd = cmd 41 | self.name = cmd.sectname 42 | self.vm_address = cmd.addr 43 | self.file_address = cmd.offset 44 | self.size = cmd.size 45 | 46 | def serialize(self): 47 | return { 48 | 'command': self.cmd.serialize(), 49 | 'name': self.name, 50 | 'vm_address': self.vm_address, 51 | 'file_address': self.file_address, 52 | 'size': self.size 53 | } 54 | 55 | 56 | class SegmentLoadCommand(LoadCommand): 57 | 58 | @classmethod 59 | def from_image(cls, image, command: Union[segment_command, segment_command_64]) -> 'SegmentLoadCommand': 60 | lc = SegmentLoadCommand() 61 | 62 | lc.cmd = command 63 | 64 | lc.vm_address = command.vmaddr 65 | lc.file_address = command.fileoff 66 | lc.size = command.vmsize 67 | lc.name = command.segname 68 | lc.type = SectionType(S_FLAGS_MASKS.SECTION_TYPE & command.flags) 69 | lc.is64 = isinstance(command, segment_command_64) 70 | 71 | ea = command.off + command.size() 72 | 73 | for sect in range(command.nsects): 74 | sect = image.read_struct(ea, section_64 if lc.is64 else section) 75 | _section = Section(sect) 76 | lc.sections[sect.name] = _section 77 | ea += section_64.size() if lc.is64 else section.size() 78 | 79 | return lc 80 | 81 | @classmethod 82 | def from_values(cls, is_64, name, vm_addr, vm_size, file_addr, file_size, maxprot, initprot, flags, sections): 83 | lc = SegmentLoadCommand() 84 | 85 | assert len(name) <= 16 86 | 87 | command_type = segment_command_64 if is_64 else segment_command 88 | section_type = section_64 if is_64 else section 89 | cmd = 0x19 if is_64 else 0x1 90 | 91 | cmdsize = command_type.size() 92 | cmdsize += (len(sections) * section_type.size()) 93 | 94 | command = Struct.create_with_values(command_type, 95 | [cmd, cmdsize, name, vm_addr, vm_size, file_addr, file_size, maxprot, 96 | initprot, len(sections), flags]) 97 | 98 | lc.cmd = command 99 | 100 | lc.vm_address = command.vmaddr 101 | lc.file_address = command.fileoff 102 | lc.size = command.vmsize 103 | lc.name = command.segname 104 | lc.type = SectionType(S_FLAGS_MASKS.SECTION_TYPE & command.flags) 105 | lc.is64 = isinstance(command, segment_command_64) 106 | 107 | lc.sections = {_section.name: _section for _section in sections} 108 | 109 | return lc 110 | 111 | def raw_bytes(self): 112 | 113 | data = bytearray() 114 | data += bytearray(self.cmd.raw) 115 | for _section in self.sections.values(): 116 | data += bytearray(_section.cmd.raw) 117 | 118 | return data 119 | 120 | def __init__(self): 121 | 122 | self.cmd = None 123 | 124 | self.is64 = False 125 | 126 | self.vm_address = 0 127 | self.file_address = 0 128 | self.size = 0 129 | self.name = "" 130 | self.type = None 131 | 132 | self.sections = {} 133 | 134 | 135 | class SymtabLoadCommand(LoadCommand): 136 | @classmethod 137 | def from_image(cls, command: symtab_command): 138 | lc = SymtabLoadCommand() 139 | 140 | lc.cmd = command 141 | 142 | lc.symtab_offset = command.symoff 143 | lc.symtab_entry_count = command.nsyms 144 | lc.string_table_offset = command.stroff 145 | lc.string_table_size = command.strsize 146 | 147 | return lc 148 | 149 | @classmethod 150 | def from_values(cls, symtab_offset, symtab_size, string_table_offset, string_table_size): 151 | cmd = Struct.create_with_values(symtab_command, [LOAD_COMMAND.SYMTAB.value, symtab_command.size(), symtab_offset, 152 | symtab_size, string_table_offset, string_table_size]) 153 | 154 | return cls.from_image(cmd) 155 | 156 | def __init__(self): 157 | self.cmd = None 158 | 159 | self.symtab_offset = 0 160 | self.symtab_entry_count = 0 161 | self.string_table_offset = 0 162 | self.string_table_size = 0 163 | 164 | def raw_bytes(self): 165 | return self.cmd.raw 166 | 167 | # TODO: Constructable wrapper for dylinker_command, build_version_command 168 | -------------------------------------------------------------------------------- /src/ktool_macho/mach_header.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # mach_header.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2021. 13 | # 14 | 15 | 16 | from enum import IntEnum 17 | 18 | from ktool_macho.structs import * 19 | 20 | MH_MAGIC = 0xFEEDFACE 21 | MH_CIGAM = 0xCEFAEDFE 22 | MH_MAGIC_64 = 0xFEEDFACF 23 | MH_CIGAM_64 = 0xCFFAEDFE 24 | FAT_MAGIC = 0xCAFEBABE 25 | FAT_CIGAM = 0xBEBAFECA 26 | 27 | 28 | class MH_FLAGS(IntEnum): 29 | NOUNDEFS = 0x1 30 | INCRLINK = 0x2 31 | DYLDLINK = 0x4 32 | BINDATLOAD = 0x8 33 | PREBOUND = 0x10 34 | SPLIT_SEGS = 0x20 35 | LAZY_INIT = 0x40 36 | TWOLEVEL = 0x80 37 | FORCE_FLAT = 0x100 38 | NOMULTIEFS = 0x200 39 | NOFIXPREBINDING = 0x400 40 | PREBINDABLE = 0x800 41 | ALLMODSBOUND = 0x1000 42 | SUBSECTIONS_VIA_SYMBOLS = 0x2000 43 | CANONICAL = 0x4000 44 | WEAK_DEFINES = 0x8000 45 | BINDS_TO_WEAK = 0x10000 46 | ALLOW_STACK_EXECUTION = 0x20000 47 | ROOT_SAFE = 0x40000 48 | SETUID_SAFE = 0x80000 49 | NO_REEXPORTED_DYLIBS = 0x100000 50 | PIE = 0x200000 51 | DEAD_STRIPPABLE_DYLIB = 0x400000 52 | HAS_TLV_DESCRIPTORS = 0x800000 53 | NO_HEAP_EXECUTION = 0x1000000 54 | APP_EXTENSION_SAFE = 0x02000000 55 | NLIST_OUTOFSYNC_WITH_DYLDINFO = 0x04000000 56 | SIM_SUPPORT = 0x08000000 57 | 58 | 59 | class MH_FILETYPE(IntEnum): 60 | UNK = 0 61 | OBJECT = 0x1 62 | EXECUTE = 0x2 63 | FVMLIB = 0x3 64 | CORE = 0x4 65 | PRELOAD = 0x5 66 | DYLIB = 0x6 67 | DYLINKER = 0x7 68 | BUNDLE = 0x8 69 | DYLIB_STUB = 0x9 70 | DSYM = 0xA 71 | KEXT_BUNDLE = 0xB 72 | 73 | 74 | LC_REQ_DYLD = 0x80000000 75 | 76 | 77 | class LOAD_COMMAND(IntEnum): 78 | SEGMENT = 0x1 79 | SYMTAB = 0x2 80 | SYMSEG = 0x3 81 | THREAD = 0x4 82 | UNIXTHREAD = 0x5 83 | LOADFVMLIB = 0x6 84 | IDFVMLIB = 0x7 85 | IDENT = 0x8 86 | FVMFILE = 0x9 87 | PREPAGE = 0xA 88 | DYSYMTAB = 0xB 89 | LOAD_DYLIB = 0xC 90 | ID_DYLIB = 0xD 91 | LOAD_DYLINKER = 0xE 92 | ID_DYLINKER = 0xF 93 | PREBOUND_DYLIB = 0x10 94 | ROUTINES = 0x11 95 | SUB_FRAMEWORK = 0x12 96 | SUB_UMBRELLA = 0x13 97 | SUB_CLIENT = 0x14 98 | SUB_image = 0x15 99 | TWOLEVEL_HINTS = 0x16 100 | PREBIND_CKSUM = 0x17 101 | LOAD_WEAK_DYLIB = 0x18 | LC_REQ_DYLD 102 | SEGMENT_64 = 0x19 103 | ROUTINES_64 = 0x1a 104 | UUID = 0x1b 105 | RPATH = 0x1C | LC_REQ_DYLD 106 | CODE_SIGNATURE = 0x1D 107 | SEGMENT_SPLIT_INFO = 0x1E 108 | REEXPORT_DYLIB = 0x1F | LC_REQ_DYLD 109 | LAZY_LOAD_DYLIB = 0x20 110 | ENCRYPTION_INFO = 0x21 111 | DYLD_INFO = 0x22 112 | DYLD_INFO_ONLY = 0x22 | LC_REQ_DYLD 113 | LOAD_UPWARD_DYLIB = 0x23 | LC_REQ_DYLD 114 | VERSION_MIN_MACOSX = 0x24 115 | VERSION_MIN_IPHONEOS = 0x25 116 | FUNCTION_STARTS = 0x26 117 | DYLD_ENVIRONMENT = 0x27 118 | MAIN = 0x28 | LC_REQ_DYLD 119 | DATA_IN_CODE = 0x29 120 | SOURCE_VERSION = 0x2A 121 | DYLIB_CODE_SIGN_DRS = 0x2B 122 | ENCRYPTION_INFO_64 = 0x2C 123 | LINKER_OPTION = 0x2D 124 | LINKER_OPTIMIZATION_HINT = 0x2E 125 | VERSION_MIN_TVOS = 0x2F 126 | VERSION_MIN_WATCHOS = 0x30 127 | NOTE = 0x31 128 | BUILD_VERSION = 0x32 129 | LC_DYLD_EXPORTS_TRIE = 0x33 | LC_REQ_DYLD 130 | LC_DYLD_CHAINED_FIXUPS = 0x34 | LC_REQ_DYLD 131 | 132 | 133 | LOAD_COMMAND_MAP = { 134 | LOAD_COMMAND.SEGMENT: segment_command, 135 | LOAD_COMMAND.SYMTAB: symtab_command, 136 | LOAD_COMMAND.DYSYMTAB: dysymtab_command, 137 | LOAD_COMMAND.THREAD: thread_command, 138 | LOAD_COMMAND.UNIXTHREAD: thread_command, 139 | LOAD_COMMAND.LOAD_DYLIB: dylib_command, 140 | LOAD_COMMAND.ID_DYLIB: dylib_command, 141 | LOAD_COMMAND.REEXPORT_DYLIB: dylib_command, 142 | LOAD_COMMAND.LOAD_DYLINKER: dylinker_command, 143 | LOAD_COMMAND.SUB_CLIENT: sub_client_command, 144 | LOAD_COMMAND.LOAD_WEAK_DYLIB: dylib_command, 145 | LOAD_COMMAND.LOAD_UPWARD_DYLIB: dylib_command, 146 | LOAD_COMMAND.SEGMENT_64: segment_command_64, 147 | LOAD_COMMAND.UUID: uuid_command, 148 | LOAD_COMMAND.CODE_SIGNATURE: linkedit_data_command, 149 | LOAD_COMMAND.SEGMENT_SPLIT_INFO: linkedit_data_command, 150 | LOAD_COMMAND.SOURCE_VERSION: source_version_command, 151 | LOAD_COMMAND.DYLD_INFO_ONLY: dyld_info_command, 152 | LOAD_COMMAND.FUNCTION_STARTS: linkedit_data_command, 153 | LOAD_COMMAND.DYLD_ENVIRONMENT: dylinker_command, 154 | LOAD_COMMAND.DATA_IN_CODE: linkedit_data_command, 155 | LOAD_COMMAND.BUILD_VERSION: build_version_command, 156 | LOAD_COMMAND.MAIN: entry_point_command, 157 | LOAD_COMMAND.RPATH: rpath_command, 158 | LOAD_COMMAND.ENCRYPTION_INFO: encryption_info_command, 159 | LOAD_COMMAND.ENCRYPTION_INFO_64: encryption_info_command_64, 160 | LOAD_COMMAND.VERSION_MIN_MACOSX: version_min_command, 161 | LOAD_COMMAND.VERSION_MIN_IPHONEOS: version_min_command, 162 | LOAD_COMMAND.VERSION_MIN_TVOS: version_min_command, 163 | LOAD_COMMAND.VERSION_MIN_WATCHOS: version_min_command, 164 | LOAD_COMMAND.LC_DYLD_EXPORTS_TRIE: linkedit_data_command, 165 | LOAD_COMMAND.LC_DYLD_CHAINED_FIXUPS: linkedit_data_command 166 | } 167 | 168 | 169 | class S_FLAGS_MASKS(IntEnum): 170 | SECTION_TYPE = 0x000000ff 171 | SECTION_ATTRIBUTES = 0xffffff00 172 | SECTION_ATTRIBUTES_USR = 0xff000000 173 | SECTION_ATTRIBUTES_SYS = 0x00ffff00 174 | 175 | 176 | class SectionType(IntEnum): 177 | S_REGULAR = 0x00 # Regular section 178 | S_ZEROFILL = 0x01 # Zero fill on demand section. 179 | S_CSTRING_LITERALS = 0x02 # Section with literal C strings 180 | S_4BYTE_LITERALS = 0x03 # Section with 4 byte literals. 181 | S_8BYTE_LITERALS = 0x04 # Section with 8 byte literals. 182 | S_LITERAL_POINTERS = 0x05 # Section with pointers to literals. 183 | S_NON_LAZY_SYMBOL_POINTERS = 0x06 # Section with non-lazy symbol pointers. 184 | S_LAZY_SYMBOL_POINTERS = 0x07 # Section with lazy symbol pointers. 185 | S_SYMBOL_STUBS = 0x08 # Section with symbol stubs, byte size of stub in the Reserved2 field. 186 | S_MOD_INIT_FUNC_POINTERS = 0x09 # Section with only function pointers for initialization. 187 | S_MOD_TERM_FUNC_POINTERS = 0x0A # Section with only function pointers for initialization. 188 | S_COALESCED = 0x0B # Section contains symbols that are to be coalesced. 189 | S_GB_ZEROFILL = 0x0C # Zero fill on demand section (that can be larger than 4 gigabytes). 190 | S_INTERPOSING = 0x0D # Section with only pairs of function pointers for interposing. 191 | S_16BYTE_LITERALS = 0x0E # Section with only 16 byte literals. 192 | S_DTRACE_DOF = 0x0F # Section contains DTrace Object Format. 193 | S_LAZY_DYLIB_SYMBOL_POINTERS = 0x10 # Section with lazy symbol pointers to lazy loaded dylibs. 194 | S_THREAD_LOCAL_REGULAR = 0x11 # Thread local data section. 195 | S_THREAD_LOCAL_ZEROFILL = 0x12 # Thread local zerofill section. 196 | S_THREAD_LOCAL_VARIABLES = 0x13 # Section with thread local variable structure data. 197 | S_THREAD_LOCAL_VARIABLE_POINTERS = 0x14 # Section with pointers to thread local structures. 198 | S_THREAD_LOCAL_INIT_FUNCTION_POINTERS = 0x15 # Section with thread local variable initialization pointers to functions. 199 | 200 | 201 | class SectionAttributesUser(IntEnum): 202 | S_ATTR_PURE_INSTRUCTIONS = 0x80000000 # Section contains only true machine instructions. 203 | S_ATTR_NO_TOC = 0x40000000 # Section contains coalesced symbols that are not to be in a ranlib table of contents. 204 | S_ATTR_STRIP_STATIC_SYMS = 0x20000000 # Ok to strip static symbols in this section in files with the MY_DYLDLINK flag. 205 | S_ATTR_NO_DEAD_STRIP = 0x10000000 # No dead stripping. 206 | S_ATTR_LIVE_SUPPORT = 0x08000000 # Blocks are live if they reference live blocks. 207 | S_ATTR_SELF_MODIFYING_CODE = 0x04000000 # Used with i386 code stubs written on by 208 | S_ATTR_DEBUG = 0x02000000 # A debug section. 209 | 210 | 211 | class SectionAttributesSys(IntEnum): 212 | S_ATTR_SOME_INSTRUCTIONS = 0x00000400 213 | S_ATTR_EXT_RELOC = 0x00000200 214 | S_ATTR_LOC_RELOC = 0x00000100 215 | 216 | 217 | CPU_ARCH_MASK = 0xff000000 # Mask for architecture bits 218 | CPU_ARCH_ABI64 = 0x01000000 219 | CPU_ARCH_ABI6432 = 0x02000000 220 | 221 | 222 | class CPUType(IntEnum): 223 | ANY = -1 224 | X86 = 7 225 | X86_64 = X86 | CPU_ARCH_ABI64 226 | MC98000 = 10 227 | ARM = 12 228 | ARM64 = ARM | CPU_ARCH_ABI64 229 | SPARC = 14 230 | POWERPC = 18 231 | POWERPC64 = POWERPC | CPU_ARCH_ABI64 232 | ARM6432 = ARM | CPU_ARCH_ABI6432 233 | 234 | 235 | class CPUSubTypeX86(IntEnum): 236 | ALL = 3 237 | ARCH1 = 4 238 | 239 | 240 | class CPUSubTypeX86_64(IntEnum): 241 | ALL = 3 242 | H = 8 243 | 244 | 245 | class CPUSubTypeARM(IntEnum): 246 | ALL = 0 247 | V4T = 5 248 | V6 = 6 249 | V5 = 7 250 | V5TEJ = 7 251 | XSCALE = 8 252 | V7 = 9 253 | ARM_V7F = 10 254 | V7S = 11 255 | V7K = 12 256 | V6M = 14 257 | V7M = 15 258 | V7EM = 16 259 | 260 | 261 | class CPUSubTypeARM64(IntEnum): 262 | ALL = 0 263 | ARM64E = 2 264 | 265 | 266 | class CPUSubTypeSPARC(IntEnum): 267 | ALL = 0 268 | 269 | 270 | class CPUSubTypePowerPC(IntEnum): 271 | ALL = 0 272 | _601 = 1 273 | _602 = 2 274 | _603 = 3 275 | _603e = 4 276 | _603ev = 5 277 | _604 = 6 278 | _604e = 7 279 | _620 = 8 280 | _750 = 9 281 | _7400 = 10 282 | _7450 = 11 283 | _970 = 100 284 | 285 | 286 | class CPUSubTypeARM6432(IntEnum): 287 | ALL = 0 288 | V8 = 1 289 | 290 | 291 | CPU_SUBTYPES = { 292 | CPUType.X86: CPUSubTypeX86, 293 | CPUType.X86_64: CPUSubTypeX86_64, 294 | CPUType.POWERPC: CPUSubTypePowerPC, 295 | CPUType.ARM: CPUSubTypeARM, 296 | CPUType.ARM64: CPUSubTypeARM64, 297 | CPUType.ARM6432: CPUSubTypeARM6432, 298 | CPUType.SPARC: CPUSubTypeSPARC 299 | } 300 | -------------------------------------------------------------------------------- /src/ktool_macho/structs.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_macho 3 | # structs.py 4 | # 5 | # the __init__ defs here are unnecessary and only required for my IDE (pycharm) to recognize and autocomplete 6 | # the struct attributes 7 | # 8 | # This file is part of ktool. ktool is free software that 9 | # is made available under the MIT license. Consult the 10 | # file "LICENSE" that is distributed together with this file 11 | # for the exact licensing terms. 12 | # 13 | # Copyright (c) 0cyn 2021. 14 | # 15 | import ktool_macho 16 | from lib0cyn.structs import * 17 | 18 | 19 | def os_tuple_composer(struct: Struct, field: str): 20 | value = struct.__getattribute__(field) 21 | x = (value >> 16) & 0xFFFF 22 | y = (value >> 8) & 0xFF 23 | z = value & 0xff 24 | dot = Struct.t_token('.') 25 | return f'{Struct.t_base(x)}{dot}{Struct.t_base(y)}{dot}{Struct.t_base(z)}' 26 | 27 | 28 | def cmd_composer(struct: Struct, field: str): 29 | value = struct.__getattribute__(field) 30 | return f'{Struct.t_base(ktool_macho.LOAD_COMMAND(value).name)} {Struct.t_token("(")}{Struct.t_base(hex(value))}{Struct.t_token(")")}' 31 | 32 | 33 | class fat_header(Struct): 34 | """ 35 | First 8 Bytes of a FAT MachO File 36 | 37 | Attributes: 38 | self.magic: FAT MachO Magic 39 | 40 | self.nfat_archs: Number of Fat Arch entries after these bytes 41 | """ 42 | FIELDS = { 43 | 'magic': uint32_t, 44 | 'nfat_archs': uint32_t 45 | } 46 | 47 | def __init__(self, byte_order="little"): 48 | super().__init__(byte_order=byte_order) 49 | self.magic = 0 50 | self.nfat_archs = 0 51 | 52 | 53 | class fat_arch(Struct): 54 | """ 55 | Struct representing a slice in a FAT MachO 56 | 57 | Attribs: 58 | cpu_type: 59 | """ 60 | FIELDS = { 61 | 'cpu_type': uint32_t, 62 | 'cpu_subtype': uint32_t, 63 | 'offset': uint32_t, 64 | 'size': uint32_t, 65 | 'align': uint32_t 66 | } 67 | 68 | def __init__(self, byte_order="little"): 69 | super().__init__(byte_order=byte_order) 70 | self.cpu_type = 0 71 | self.cpu_subtype = 0 72 | self.offset = 0 73 | self.size = 0 74 | self.align = 0 75 | 76 | 77 | class mach_header(Struct): 78 | FIELDS = { 79 | 'magic': uint32_t, 80 | 'cpu_type': uint32_t, 81 | 'cpu_subtype': uint32_t, 82 | 'filetype': uint32_t, 83 | 'loadcnt': uint32_t, 84 | 'loadsize': uint32_t, 85 | 'flags': uint32_t 86 | } 87 | 88 | def __init__(self, byte_order="little"): 89 | super().__init__(byte_order=byte_order) 90 | self.magic = 0 91 | self.cpu_type = 0 92 | self.cpu_subtype = 0 93 | self.filetype = 0 94 | self.loadcnt = 0 95 | self.loadsize = 0 96 | self.flags = 0 97 | 98 | 99 | class mach_header_64(Struct): 100 | FIELDS = { 101 | 'magic': uint32_t, 102 | 'cpu_type': uint32_t, 103 | 'cpu_subtype': uint32_t, 104 | 'filetype': uint32_t, 105 | 'loadcnt': uint32_t, 106 | 'loadsize': uint32_t, 107 | 'flags': uint32_t, 108 | 'reserved': uint32_t 109 | } 110 | 111 | def __init__(self, byte_order="little"): 112 | super().__init__(byte_order=byte_order) 113 | self.magic = 0 114 | self.cpu_type = 0 115 | self.cpu_subtype = 0 116 | self.filetype = 0 117 | self.loadcnt = 0 118 | self.loadsize = 0 119 | self.flags = 0 120 | self.reserved = 0 121 | 122 | 123 | class unk_command(Struct): 124 | FIELDS = { 125 | 'cmd': uint32_t, 126 | 'cmdsize': uint32_t 127 | } 128 | 129 | def __init__(self, byte_order="little"): 130 | super().__init__(byte_order=byte_order) 131 | self.cmd = 0 132 | self.add_field_composer('cmd', cmd_composer) 133 | self.cmdsize = 0 134 | 135 | 136 | class segment_command(Struct): 137 | FIELDS = { 138 | 'cmd': uint32_t, 139 | 'cmdsize': uint32_t, 140 | 'segname': char_t[16], 141 | 'vmaddr': uint32_t, 142 | 'vmsize': uint32_t, 143 | 'fileoff': uint32_t, 144 | 'filesize': uint32_t, 145 | 'maxprot': uint32_t, 146 | 'initprot': uint32_t, 147 | 'nsects': uint32_t, 148 | 'flags': uint32_t 149 | } 150 | 151 | def __init__(self, byte_order="little"): 152 | super().__init__(byte_order=byte_order) 153 | self.cmd = 0 154 | self.add_field_composer('cmd', cmd_composer) 155 | self.cmdsize = 0 156 | self.segname = 0 157 | self.vmaddr = 0 158 | self.vmsize = 0 159 | self.fileoff = 0 160 | self.filesize = 0 161 | self.maxprot = 0 162 | self.initprot = 0 163 | self.nsects = 0 164 | self.flags = 0 165 | 166 | 167 | class segment_command_64(Struct): 168 | FIELDS = { 169 | 'cmd': uint32_t, 170 | 'cmdsize': uint32_t, 171 | 'segname': char_t[16], 172 | 'vmaddr': uint64_t, 173 | 'vmsize': uint64_t, 174 | 'fileoff': uint64_t, 175 | 'filesize': uint64_t, 176 | 'maxprot': uint32_t, 177 | 'initprot': uint32_t, 178 | 'nsects': uint32_t, 179 | 'flags': uint32_t 180 | } 181 | 182 | def __init__(self, byte_order="little"): 183 | super().__init__(byte_order=byte_order) 184 | self.cmd = 0 185 | self.add_field_composer('cmd', cmd_composer) 186 | self.cmdsize = 0 187 | self.segname = 0 188 | self.vmaddr = 0 189 | self.vmsize = 0 190 | self.fileoff = 0 191 | self.filesize = 0 192 | self.maxprot = 0 193 | self.initprot = 0 194 | self.nsects = 0 195 | self.flags = 0 196 | 197 | 198 | class section(Struct): 199 | FIELDS = { 200 | 'sectname': char_t[16], 201 | 'segname': char_t[16], 202 | 'addr': uint32_t, 203 | 'size': uint32_t, 204 | 'offset': uint32_t, 205 | 'align': uint32_t, 206 | 'reloff': uint32_t, 207 | 'nreloc': uint32_t, 208 | 'flags': uint32_t, 209 | 'reserved1': uint32_t, 210 | 'reserved2': uint32_t 211 | } 212 | 213 | def __init__(self, byte_order="little"): 214 | super().__init__(byte_order=byte_order) 215 | self.sectname = 0 216 | self.segname = 0 217 | self.addr = 0 218 | self.size = 0 219 | self.offset = 0 220 | self.align = 0 221 | self.reloff = 0 222 | self.nreloc = 0 223 | self.flags = 0 224 | self.reserved1 = 0 225 | self.reserved2 = 0 226 | 227 | 228 | class section_64(Struct): 229 | FIELDS = { 230 | 'sectname': char_t[16], 231 | 'segname': char_t[16], 232 | 'addr': uint64_t, 233 | 'size': uint64_t, 234 | 'offset': uint32_t, 235 | 'align': uint32_t, 236 | 'reloff': uint32_t, 237 | 'nreloc': uint32_t, 238 | 'flags': uint32_t, 239 | 'reserved1': uint32_t, 240 | 'reserved2': uint32_t, 241 | 'reserved3': uint32_t 242 | } 243 | 244 | def __init__(self, byte_order="little"): 245 | super().__init__(byte_order=byte_order) 246 | self.sectname = 0 247 | self.segname = 0 248 | self.addr = 0 249 | self.size = 0 250 | self.offset = 0 251 | self.align = 0 252 | self.reloff = 0 253 | self.nreloc = 0 254 | self.flags = 0 255 | self.reserved1 = 0 256 | self.reserved2 = 0 257 | self.reserved3 = 0 258 | 259 | 260 | class symtab_command(Struct): 261 | FIELDS = { 262 | 'cmd': uint32_t, 263 | 'cmdsize': uint32_t, 264 | 'symoff': uint32_t, 265 | 'nsyms': uint32_t, 266 | 'stroff': uint32_t, 267 | 'strsize': uint32_t 268 | } 269 | 270 | def __init__(self, byte_order="little"): 271 | super().__init__(byte_order=byte_order) 272 | self.cmd = 0 273 | self.add_field_composer('cmd', cmd_composer) 274 | self.cmdsize = 0 275 | self.symoff = 0 276 | self.nsyms = 0 277 | self.stroff = 0 278 | self.strsize = 0 279 | 280 | 281 | class dysymtab_command(Struct): 282 | FIELDS = { 283 | 'cmd': uint32_t, 284 | 'cmdsize': uint32_t, 285 | 'ilocalsym': uint32_t, 286 | 'nlocalsym': uint32_t, 287 | 'iextdefsym': uint32_t, 288 | 'nextdefsym': uint32_t, 289 | 'iundefsym': uint32_t, 290 | 'nundefsym': uint32_t, 291 | 'tocoff': uint32_t, 292 | 'ntoc': uint32_t, 293 | 'modtaboff': uint32_t, 294 | 'nmodtab': uint32_t, 295 | 'extrefsymoff': uint32_t, 296 | 'nextrefsyms': uint32_t, 297 | 'indirectsymoff': uint32_t, 298 | 'nindirectsyms': uint32_t, 299 | 'extreloff': uint32_t, 300 | 'nextrel': uint32_t, 301 | 'locreloff': uint32_t, 302 | 'nlocrel': uint32_t 303 | } 304 | 305 | def __init__(self, byte_order="little"): 306 | super().__init__(byte_order=byte_order) 307 | self.cmd = 0 308 | self.add_field_composer('cmd', cmd_composer) 309 | self.cmdsize = 0 310 | self.ilocalsym = 0 311 | self.nlocalsym = 0 312 | self.iextdefsym = 0 313 | self.nextdefsym = 0 314 | self.iundefsym = 0 315 | self.nundefsym = 0 316 | self.tocoff = 0 317 | self.ntoc = 0 318 | self.modtaboff = 0 319 | self.nmodtab = 0 320 | self.extrefsymoff = 0 321 | self.nextrefsyms = 0 322 | self.indirectsymoff = 0 323 | self.nindirectsyms = 0 324 | self.extreloff = 0 325 | self.nextrel = 0 326 | self.locreloff = 0 327 | self.nlocrel = 0 328 | 329 | 330 | class dylib(Struct): 331 | FIELDS = { 332 | 'name': uint32_t, 333 | 'timestamp': uint32_t, 334 | 'current_version': uint32_t, 335 | 'compatibility_version': uint32_t 336 | } 337 | 338 | def __init__(self, byte_order="little"): 339 | super().__init__(byte_order=byte_order) 340 | self.name = 0 341 | self.timestamp = 0 342 | self.current_version = 0 343 | self.compatibility_version = 0 344 | self.add_field_composer('current_version', os_tuple_composer) 345 | self.add_field_composer('compatibility_version', os_tuple_composer) 346 | 347 | 348 | class dylib_command(Struct): 349 | FIELDS = { 350 | 'cmd': uint32_t, 351 | 'cmdsize': uint32_t, 352 | 'dylib': dylib 353 | } 354 | 355 | def __init__(self, byte_order="little"): 356 | super().__init__(byte_order=byte_order) 357 | self.cmd = 0 358 | self.add_field_composer('cmd', cmd_composer) 359 | self.cmdsize = 0 360 | self.dylib = 0 361 | 362 | 363 | class dylinker_command(Struct): 364 | FIELDS = { 365 | 'cmd': uint32_t, 366 | 'cmdsize': uint32_t, 367 | 'name': uint32_t 368 | } 369 | 370 | def __init__(self, byte_order="little"): 371 | super().__init__(byte_order=byte_order) 372 | self.cmd = 0 373 | self.add_field_composer('cmd', cmd_composer) 374 | self.cmdsize = 0 375 | self.name = 0 376 | 377 | 378 | class sub_client_command(Struct): 379 | FIELDS = { 380 | 'cmd': uint32_t, 381 | 'cmdsize': uint32_t, 382 | 'offset': uint32_t 383 | } 384 | 385 | def __init__(self, byte_order="little"): 386 | super().__init__(byte_order=byte_order) 387 | self.cmd = 0 388 | self.add_field_composer('cmd', cmd_composer) 389 | self.cmdsize = 0 390 | self.offset = 0 391 | 392 | 393 | class uuid_command(Struct): 394 | FIELDS = { 395 | 'cmd': uint32_t, 396 | 'cmdsize': uint32_t, 397 | 'uuid': bytes_t[16] 398 | } 399 | 400 | def __init__(self, byte_order="little"): 401 | super().__init__(byte_order=byte_order) 402 | self.cmd = 0 403 | self.add_field_composer('cmd', cmd_composer) 404 | self.cmdsize = 0 405 | self.uuid = 0 406 | self.add_field_composer('uuid', uuid_command.uuid_field_composer) 407 | 408 | @staticmethod 409 | def uuid_field_composer(struct, field): 410 | assert field == "uuid" 411 | byte_array = struct.uuid 412 | return Struct.t_base(f'"{byte_array[0]:02x}{byte_array[1]:02x}{byte_array[2]:02x}{byte_array[3]:02x}-' \ 413 | f'{byte_array[4]:02x}{byte_array[5]:02x}-' \ 414 | f'{byte_array[6]:02x}{byte_array[7]:02x}-' \ 415 | f'{byte_array[8]:02x}{byte_array[9]:02x}-' \ 416 | f'{byte_array[10]:02x}{byte_array[11]:02x}{byte_array[12]:02x}{byte_array[13]:02x}{byte_array[14]:02x}{byte_array[15]:02x}"') 417 | 418 | 419 | class build_version_command(Struct): 420 | FIELDS = { 421 | 'cmd': uint32_t, 422 | 'cmdsize': uint32_t, 423 | 'platform': uint32_t, 424 | 'minos': uint32_t, 425 | 'sdk': uint32_t, 426 | 'ntools': uint32_t 427 | } 428 | 429 | def __init__(self, byte_order="little"): 430 | super().__init__(byte_order=byte_order) 431 | self.cmd = 0 432 | self.add_field_composer('cmd', cmd_composer) 433 | self.cmdsize = 0 434 | self.platform = 0 435 | self.minos = 0 436 | self.sdk = 0 437 | self.ntools = 0 438 | self.add_field_composer('minos', os_tuple_composer) 439 | self.add_field_composer('sdk', os_tuple_composer) 440 | 441 | 442 | class entry_point_command(Struct): 443 | FIELDS = { 444 | 'cmd': uint32_t, 445 | 'cmdsize': uint32_t, 446 | 'entryoff': uint64_t, 447 | 'stacksize': uint64_t 448 | } 449 | 450 | def __init__(self, byte_order="little"): 451 | super().__init__(byte_order=byte_order) 452 | self.cmd = 0 453 | self.add_field_composer('cmd', cmd_composer) 454 | self.cmdsize = 0 455 | self.entryoff = 0 456 | self.stacksize = 0 457 | 458 | 459 | class rpath_command(Struct): 460 | FIELDS = { 461 | 'cmd': uint32_t, 462 | 'cmdsize': uint32_t, 463 | 'path': uint32_t 464 | } 465 | 466 | def __init__(self, byte_order="little"): 467 | super().__init__(byte_order=byte_order) 468 | self.cmd = 0 469 | self.add_field_composer('cmd', cmd_composer) 470 | self.cmdsize = 0 471 | self.path = 0 472 | 473 | 474 | class source_version_command(Struct): 475 | FIELDS = { 476 | 'cmd': uint32_t, 477 | 'cmdsize': uint32_t, 478 | 'version': uint64_t 479 | } 480 | 481 | def __init__(self, byte_order="little"): 482 | super().__init__(byte_order=byte_order) 483 | self.cmd = 0 484 | self.add_field_composer('cmd', cmd_composer) 485 | self.cmdsize = 0 486 | self.version = 0 487 | self.add_field_composer('version', os_tuple_composer) 488 | 489 | 490 | class linkedit_data_command(Struct): 491 | FIELDS = { 492 | 'cmd': uint32_t, 493 | 'cmdsize': uint32_t, 494 | 'dataoff': uint32_t, 495 | 'datasize': uint32_t 496 | } 497 | 498 | def __init__(self, byte_order="little"): 499 | super().__init__(byte_order=byte_order) 500 | self.cmd = 0 501 | self.add_field_composer('cmd', cmd_composer) 502 | self.cmdsize = 0 503 | self.dataoff = 0 504 | self.datasize = 0 505 | 506 | 507 | class dyld_info_command(Struct): 508 | FIELDS = { 509 | 'cmd': uint32_t, 510 | 'cmdsize': uint32_t, 511 | 'rebase_off': uint32_t, 512 | 'rebase_size': uint32_t, 513 | 'bind_off': uint32_t, 514 | 'bind_size': uint32_t, 515 | 'weak_bind_off': uint32_t, 516 | 'weak_bind_size': uint32_t, 517 | 'lazy_bind_off': uint32_t, 518 | 'lazy_bind_size': uint32_t, 519 | 'export_off': uint32_t, 520 | 'export_size': uint32_t 521 | } 522 | 523 | def __init__(self, byte_order="little"): 524 | super().__init__(byte_order=byte_order) 525 | self.cmd = 0 526 | self.add_field_composer('cmd', cmd_composer) 527 | self.cmdisze = 0 528 | self.rebase_off = 0 529 | self.rebase_size = 0 530 | self.bind_off = 0 531 | self.bind_size = 0 532 | self.weak_bind_off = 0 533 | self.weak_bind_size = 0 534 | self.lazy_bind_off = 0 535 | self.lazy_bind_size = 0 536 | self.export_off = 0 537 | self.export_size = 0 538 | 539 | 540 | class symtab_entry_32(Struct): 541 | FIELDS = { 542 | 'str_index': uint32_t, 543 | 'type': uint8_t, 544 | 'sect_index': uint8_t, 545 | 'desc': uint16_t, 546 | 'value': uint32_t 547 | } 548 | 549 | def __init__(self, byte_order="little"): 550 | super().__init__(byte_order=byte_order) 551 | self.str_index = 0 552 | self.type = 0 553 | self.sect_index = 0 554 | self.desc = 0 555 | self.value = 0 556 | 557 | 558 | class symtab_entry(Struct): 559 | FIELDS = { 560 | 'str_index': uint32_t, 561 | 'type': uint8_t, 562 | 'sect_index': uint8_t, 563 | 'desc': uint16_t, 564 | 'value': uint64_t 565 | } 566 | 567 | def __init__(self, byte_order="little"): 568 | super().__init__(byte_order=byte_order) 569 | self.str_index = 0 570 | self.type = 0 571 | self.sect_index = 0 572 | self.desc = 0 573 | self.value = 0 574 | 575 | 576 | class version_min_command(Struct): 577 | FIELDS = { 578 | 'cmd': uint32_t, 579 | 'cmdsize': uint32_t, 580 | 'version': uint32_t, 581 | 'reserved': uint32_t 582 | } 583 | 584 | def __init__(self, byte_order="little"): 585 | super().__init__(byte_order=byte_order) 586 | self.cmd = 0 587 | self.add_field_composer('cmd', cmd_composer) 588 | self.cmdsize = 0 589 | self.version = 0 590 | self.reserved = 0 591 | self.add_field_composer('version', os_tuple_composer) 592 | 593 | 594 | class encryption_info_command(Struct): 595 | FIELDS = { 596 | 'cmd': uint32_t, 597 | 'cmdsize': uint32_t, 598 | 'cryptoff': uint32_t, 599 | 'cryptsize': uint32_t, 600 | 'cryptid': uint32_t 601 | } 602 | 603 | def __init__(self, byte_order="little"): 604 | super().__init__(byte_order=byte_order) 605 | self.cmd = 0 606 | self.add_field_composer('cmd', cmd_composer) 607 | self.cmdsize = 0 608 | self.cryptoff = 0 609 | self.cryptsize = 0 610 | self.cryptid = 0 611 | 612 | 613 | class encryption_info_command_64(Struct): 614 | FIELDS = { 615 | 'cmd': uint32_t, 616 | 'cmdsize': uint32_t, 617 | 'cryptoff': uint32_t, 618 | 'cryptsize': uint32_t, 619 | 'cryptid': uint32_t, 620 | 'pad': uint32_t 621 | } 622 | 623 | def __init__(self, byte_order="little"): 624 | super().__init__(byte_order=byte_order) 625 | self.cmd = 0 626 | self.add_field_composer('cmd', cmd_composer) 627 | self.cmdsize = 0 628 | self.cryptoff = 0 629 | self.cryptsize = 0 630 | self.cryptid = 0 631 | self.pad = 0 632 | 633 | 634 | class thread_command(Struct): 635 | FIELDS = { 636 | 'cmd': uint32_t, 637 | 'cmdsize': uint32_t, 638 | 'flavor': uint32_t, 639 | 'count': uint32_t 640 | } 641 | 642 | def __init__(self, byte_order="little"): 643 | super().__init__(byte_order=byte_order) 644 | self.cmd = 0 645 | self.add_field_composer('cmd', cmd_composer) 646 | self.cmdsize = 0 647 | self.flavor = 0 648 | self.count = 0 649 | -------------------------------------------------------------------------------- /src/ktool_swift/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_swift 3 | # __init__.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | -------------------------------------------------------------------------------- /src/ktool_swift/demangle.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_swift 3 | # demangle.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | 15 | def demangle(name): 16 | """ 17 | Very basic, very sloppy bare minimum POC for swift classname demangling 18 | 19 | :param name: 20 | :return: 21 | """ 22 | 23 | project = "" 24 | typename = "" 25 | stage = 0 26 | skip = False 27 | 28 | for c in name: 29 | if c.isdigit(): 30 | if skip: 31 | continue 32 | else: 33 | stage += 1 34 | skip = True 35 | continue 36 | else: 37 | skip = False 38 | if stage == 0: 39 | continue 40 | elif stage == 1: 41 | project += c 42 | elif stage == 2: 43 | typename += c 44 | 45 | return project, typename 46 | -------------------------------------------------------------------------------- /src/ktool_swift/structs.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | ktool_swift 3 | # structs.py 4 | # 5 | # https://knight.sc/reverse%20engineering/2019/07/17/swift-metadata.html 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | 15 | import enum 16 | from lib0cyn.structs import * 17 | 18 | 19 | class ProtocolDescriptor(Struct): 20 | FIELDS = { 21 | 'Flags': uint32_t, 22 | 'Parent': int32_t, 23 | 'Name': int32_t, 24 | 'NumRequirementsInSignature': uint32_t, 25 | 'NumRequirements': uint32_t, 26 | 'AssociatedTypeNames': int32_t 27 | } 28 | 29 | 30 | class ProtocolConformanceDescriptor(Struct): 31 | FIELDS = { 32 | 'ProtocolDescriptor': int32_t, 33 | 'NominalTypeDescriptor': int32_t, 34 | 'ProtocolWitnessTable': int32_t, 35 | 'ConformanceFlags': uint32_t 36 | } 37 | 38 | 39 | class EnumDescriptor(Struct): 40 | FIELDS = { 41 | 'Flags': uint32_t, 42 | 'Parent': int32_t, 43 | 'Name': int32_t, 44 | 'AccessFunction': int32_t, 45 | 'FieldDescriptor': int32_t, 46 | 'NumPayloadCasesAndPayloadSizeOffset': uint32_t, 47 | 'NumEmptyCases': uint32_t 48 | } 49 | 50 | 51 | class StructDescriptor(Struct): 52 | FIELDS = { 53 | 'Flags': uint32_t, 54 | 'Parent': int32_t, 55 | 'Name': int32_t, 56 | 'AccessFunction': int32_t, 57 | 'FieldDescriptor': int32_t, 58 | 'NumFields': uint32_t, 59 | 'FieldOffsetVectorOffset': uint32_t 60 | } 61 | 62 | 63 | class ClassDescriptor(Struct): 64 | FIELDS = { 65 | 'Flags': uint32_t, 66 | 'Parent': int32_t, 67 | 'Name': int32_t, 68 | 'AccessFunction': int32_t, 69 | 'FieldDescriptor': int32_t, 70 | 'SuperclassType': int32_t, 71 | 'MetadataNegativeSizeInWords': uint32_t, 72 | 'MetadataPositiveSizeInWords': uint32_t, 73 | 'NumImmediateMembers': uint32_t, 74 | 'NumFields': uint32_t 75 | } 76 | 77 | 78 | class FieldDescriptor(Struct): 79 | FIELDS = { 80 | 'MangledTypeName': int32_t, 81 | 'Superclass': int32_t, 82 | 'Kind': uint16_t, 83 | 'FieldRecordSize': int16_t, 84 | 'NumFields': int32_t 85 | } 86 | 87 | 88 | class FieldRecord(Struct): 89 | FIELDS = { 90 | 'Flags': uint32_t, 91 | 'MangledTypeName': int32_t, 92 | 'FieldName': int32_t 93 | } 94 | 95 | 96 | class AssociatedTypeRecord(Struct): 97 | FIELDS = { 98 | 'Name': int32_t, 99 | 'SubstitutedTypename': int32_t 100 | } 101 | 102 | 103 | class AssociatedTypeDescriptor(Struct): 104 | FIELDS = { 105 | 'ConformingTypeName': int32_t, 106 | 'ProtocolTypeName': int32_t, 107 | 'NumAssociatedTypes': uint32_t, 108 | 'AssociatedTypeRecordSize': uint32_t 109 | } 110 | 111 | 112 | class BuiltinTypeDescriptor(Struct): 113 | FIELDS = { 114 | 'TypeName': int32_t, 115 | 'Size': uint32_t, 116 | 'AlignmentAndFlags': uint32_t, 117 | 'Stride': uint32_t, 118 | 'NumExtraInhabitants': uint32_t 119 | } 120 | 121 | 122 | class CaptureTypeRecord(Struct): 123 | FIELDS = { 124 | 'MangledTypeName': int32_t 125 | } 126 | 127 | 128 | class MetadataSourceRecord(Struct): 129 | FIELDS = { 130 | 'MangledTypeName': int32_t, 131 | 'MangledMetadataSource': int32_t 132 | } 133 | 134 | 135 | class CaptureDescriptor(Struct): 136 | FIELDS = { 137 | 'NumCaptureTypes': uint32_t, 138 | 'NumMetadataSources': uint32_t, 139 | 'NumBindings': uint32_t 140 | } 141 | 142 | 143 | class Replacement(Struct): 144 | FIELDS = { 145 | 'ReplacedFunctionKey': int32_t, 146 | 'NewFunction': int32_t, 147 | 'Replacement': int32_t, 148 | 'Flags': uint32_t 149 | } 150 | 151 | 152 | class ReplacementScope(Struct): 153 | FIELDS = { 154 | 'Flags': uint32_t, 155 | 'NumReplacements': uint32_t 156 | } 157 | 158 | 159 | class AutomaticReplacements(Struct): 160 | FIELDS = { 161 | 'Flags': uint32_t, 162 | 'NumReplacements': uint32_t, 163 | 'Replacements': int32_t 164 | } 165 | 166 | 167 | class OpaqueReplacement(Struct): 168 | FIELDS = { 169 | 'Original': int32_t, 170 | 'Replacement': int32_t 171 | } 172 | 173 | 174 | class OpaqueAutomaticReplacement(Struct): 175 | FIELDS = { 176 | 'Flags': uint32_t, 177 | 'NumReplacements': uint32_t, 178 | } 179 | 180 | 181 | class ClassMethodListTable(Struct): 182 | FIELDS = { 183 | 'VTableOffset': uint32_t, 184 | 'VTableSize': uint32_t 185 | } 186 | 187 | 188 | class TargetMethodDescriptor(Struct): 189 | FIELDS = { 190 | 'Flags': uint32_t, 191 | 'Impl': int32_t 192 | } 193 | 194 | 195 | class ContextDescriptorKind(enum.Enum): 196 | Module = 0 197 | Extension = 1 198 | Anonymous = 2 199 | SwiftProtocol = 3 200 | OpaqueType = 4 201 | Class = 16 202 | Struct = 17 203 | Enum = 18 204 | Type_Last = 31 205 | -------------------------------------------------------------------------------- /src/lib0cyn/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | lib0cyn 3 | # __init__.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | 15 | from lib0cyn.structs import * 16 | -------------------------------------------------------------------------------- /src/lib0cyn/log.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | lib0cyn 3 | # log.py 4 | # 5 | # 6 | # 7 | # This file is part of ktool. ktool is free software that 8 | # is made available under the MIT license. Consult the 9 | # file "LICENSE" that is distributed together with this file 10 | # for the exact licensing terms. 11 | # 12 | # Copyright (c) 0cyn 2022. 13 | # 14 | 15 | from enum import Enum 16 | import sys 17 | import inspect 18 | import os 19 | 20 | from lib0cyn.structs import Struct 21 | 22 | 23 | class LogLevel(Enum): 24 | NONE = -1 25 | ERROR = 0 26 | WARN = 1 27 | INFO = 2 28 | DEBUG = 3 29 | DEBUG_MORE = 4 30 | # if this isn't being piped to a file it will be ridiculous 31 | # it will also likely slow down the processor a shit-ton if it's being output to term. 32 | DEBUG_TOO_MUCH = 5 33 | 34 | 35 | def print_err(msg): 36 | print(msg, file=sys.stderr) 37 | 38 | 39 | class log: 40 | """ 41 | Python's default logging image is absolute garbage 42 | 43 | so we use this. 44 | """ 45 | 46 | LOG_LEVEL = LogLevel.ERROR 47 | # Should be a function name, without () 48 | # We make this dynamically changeable for the sake of being able to redirect output in GUI tools. 49 | LOG_FUNC = print 50 | LOG_ERR = print_err 51 | 52 | @staticmethod 53 | def get_class_from_frame(fr): 54 | fr: inspect.FrameInfo = fr 55 | if 'self' in fr.frame.f_locals: 56 | return type(fr.frame.f_locals["self"]).__name__ 57 | elif 'cls' in fr.frame.f_locals: 58 | return fr.frame.f_locals['cls'].__name__ 59 | 60 | return None 61 | 62 | @staticmethod 63 | def line(): 64 | stack_frame = inspect.stack()[2] 65 | filename = os.path.basename(stack_frame[1]).split('.')[0] 66 | line_name = f'L#{stack_frame[2]}' 67 | cn = log.get_class_from_frame(stack_frame) 68 | call_from = cn + ':' if cn is not None else "" 69 | call_from += stack_frame[3] 70 | return 'ktool.' + filename + ":" + line_name + ":" + call_from + '()' 71 | 72 | @staticmethod 73 | def debug(msg=""): 74 | if log.LOG_LEVEL.value >= LogLevel.DEBUG.value: 75 | if issubclass(msg.__class__, Struct): 76 | msg = str(msg) 77 | log.LOG_FUNC(f'DEBUG - {log.line()} - {msg}') 78 | 79 | @staticmethod 80 | def debug_more(msg: str = ""): 81 | if log.LOG_LEVEL.value >= LogLevel.DEBUG_MORE.value: 82 | if issubclass(msg.__class__, Struct): 83 | msg = str(msg) 84 | log.LOG_FUNC(f'DEBUG-2 - {log.line()} - {msg}') 85 | 86 | @staticmethod 87 | def debug_tm(msg: str = ""): 88 | if log.LOG_LEVEL.value >= LogLevel.DEBUG_TOO_MUCH.value: 89 | if issubclass(msg.__class__, Struct): 90 | msg = str(msg) 91 | log.LOG_FUNC(f'DEBUG-3 - {log.line()} - {msg}') 92 | 93 | @staticmethod 94 | def info(msg: str = ""): 95 | if log.LOG_LEVEL.value >= LogLevel.INFO.value: 96 | if issubclass(msg.__class__, Struct): 97 | msg = str(msg) 98 | log.LOG_FUNC(f'INFO - {log.line()} - {msg}') 99 | 100 | @staticmethod 101 | def warn(msg: str = ""): 102 | if log.LOG_LEVEL.value >= LogLevel.WARN.value: 103 | if issubclass(msg.__class__, Struct): 104 | msg = str(msg) 105 | log.LOG_ERR(f'WARN - {log.line()} - {msg}') 106 | 107 | @staticmethod 108 | def warning(msg: str = ""): 109 | if log.LOG_LEVEL.value >= LogLevel.WARN.value: 110 | if issubclass(msg.__class__, Struct): 111 | msg = str(msg) 112 | log.LOG_ERR(f'WARN - {log.line()} - {msg}') 113 | 114 | @staticmethod 115 | def error(msg: str = ""): 116 | if log.LOG_LEVEL.value >= LogLevel.ERROR.value: 117 | if issubclass(msg.__class__, Struct): 118 | msg = str(msg) 119 | log.LOG_ERR(f'ERROR - {log.line()} - {msg}') 120 | -------------------------------------------------------------------------------- /src/lib0cyn/structs.py: -------------------------------------------------------------------------------- 1 | # 2 | # ktool | lib0cyn 3 | # structs.py 4 | # 5 | # Custom Struct implementation reflecting behavior of named tuples while also handling behind-the-scenes 6 | # packing/unpacking 7 | # 8 | # This file is part of ktool. ktool is free software that 9 | # is made available under the MIT license. Consult the 10 | # file "LICENSE" that is distributed together with this file 11 | # for the exact licensing terms. 12 | # 13 | # Copyright (c) 0cyn 2022. 14 | # 15 | from typing import List 16 | import inspect 17 | import enum 18 | import re 19 | ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') 20 | 21 | def strip_ansi(msg): 22 | return ansi_escape.sub('', msg) 23 | 24 | # At a glance this looks insane for python, 25 | # But size calc is *very* hot code, and if we can reduce the computation of sizes to bit manipulation, 26 | # it provides a sizable speedup. 27 | type_mask = 0xffff0000 28 | size_mask = 0xffff 29 | 30 | type_uint = 0 31 | type_sint = 0x10000 32 | type_str = 0x20000 33 | type_bytes = 0x30000 34 | 35 | uint8_t = 1 36 | uint16_t = 2 37 | uint32_t = 4 38 | uint64_t = 8 39 | 40 | int8_t = type_sint | 1 41 | int16_t = type_sint | 2 42 | int32_t = type_sint | 4 43 | int64_t = type_sint | 8 44 | 45 | # "wtf is going on here?" 46 | # this is a bit cursed, but makes it possible to specify an arbitrary length of characters/bytes in a field 47 | # by passing, e.g. char_t[16] for the size. This is just making an array of size values with a str type 48 | # and size between 1 and 64 49 | # if you need more than 64 just pass `(type_str | n)` where `n` is your size, as the size. 50 | char_t = [type_str | i for i in range(65)] 51 | bytes_t = [type_bytes | i for i in range(65)] 52 | # can you tell I miss C 53 | 54 | 55 | class uintptr_t: 56 | pass 57 | 58 | 59 | class pad_for_64_bit_only: 60 | """ Sometimes, arm64/x64 variations of structures may differ from 32 bit ones only in variables to pad 61 | things out for the sake of byte-aligned reads. This allows us to account for that without having to make 62 | a separate 64 and 32 bit struct. 63 | 64 | This acts as a variable length field, and will have a size of 0 if ptr_size passed to struct code isn't 8 65 | """ 66 | def __init__(self, size=4): 67 | self.size = size 68 | 69 | 70 | class Bitfield: 71 | """ Horrible class for decoding bitfields. This basically just exists for chained fixups do not write anything 72 | that uses this because I hardly understand what i've even wrote. 73 | 74 | Initialize with dict of field name to size in bits. 75 | Load fields with ``myBitfieldInstance.decode_bitfield(myProperlySizedBytearray)`` 76 | Access by myBitfieldInstance.field_name 77 | """ 78 | 79 | def __init__(self, fields: dict): 80 | self.fields = fields 81 | self.size = sum(self.fields.values()) // 8 82 | self.size_bits = sum(self.fields.values()) 83 | self.decoded_fields = {} 84 | 85 | def decode_bitfield(self, value): 86 | # welcom to my night mare 87 | int_value = int.from_bytes(value, 'little') 88 | # print(bin(int_value)) 89 | bit_pos = 0 90 | for field_name, bit_size in self.fields.items(): 91 | mask = (1 << bit_size) - 1 92 | # print(f'{field_name} - {bin((int_value >> bit_pos) & mask)} {bin(mask)}') 93 | self.decoded_fields[field_name] = (int_value >> bit_pos) & mask 94 | setattr(self, field_name, self.decoded_fields[field_name]) 95 | bit_pos += bit_size 96 | 97 | 98 | class StructUnion: 99 | """ This class is a horrible one; 100 | This struct code was not written with Unions in mind, or much in mind in general; 101 | 102 | This implementation of unions has a couple of rules: 103 | * It can only contain structs or other unions 104 | * It will implicitly assume all types are the same size and probably die horribly if that isn't true. 105 | 106 | Create one like: 107 | class MySubClass(StructUnion): 108 | def __init__(): # |size | list of Struct types like `mach_header`, etc 109 | super().__init__(8, [my_struct_1, my_structtype_2]) 110 | 111 | Use like: 112 | unionInst = MySubClass() 113 | unionInst.load_from_bytes(my_epic_bytearray_that_is_properly_sized) 114 | valueIWant = unionInst.my_struct_1.someFieldInIt 115 | 116 | """ 117 | 118 | def __init__(self, size: int, types: List[object]): 119 | self.size = size 120 | self.types = types 121 | 122 | def load_from_bytes(self, data): 123 | for t in self.types: 124 | if issubclass(t, Struct): 125 | setattr(self, t.__name__, Struct.create_with_bytes(t, data, "little")) 126 | elif issubclass(t, StructUnion): 127 | setattr(self, t.__name__, t()) 128 | getattr(self, t.__name__).load_from_bytes(data) 129 | 130 | def __int__(self): 131 | return self.__class__.size() 132 | 133 | 134 | def _bytes_to_hex(data) -> str: 135 | return data.hex() 136 | 137 | 138 | def _uint_to_int(uint, bits): 139 | """ 140 | Assume an int was read from binary as an unsigned int, 141 | 142 | decode it as a two's compliment signed integer 143 | 144 | :param uint: 145 | :param bits: 146 | :return: 147 | """ 148 | if (uint & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 149 | uint = uint - (1 << bits) # compute negative value 150 | return uint # return positive value as is 151 | 152 | 153 | # noinspection PyUnresolvedReferences 154 | class Struct: 155 | """ 156 | Custom namedtuple-esque Struct representation. Can be unpacked from bytes or manually created with existing 157 | field values 158 | 159 | Subclassed and passed `fields` and `sizes` values. 160 | 161 | Fields are exposed as read-write attributes, and when written to, will update the backend 162 | byte representation of the struct, accessible via the .raw attribute 163 | 164 | """ 165 | 166 | class StructFieldColorType(enum.IntEnum): 167 | BASETYPE_ITEM = 0 168 | TOKEN_ITEM = 1 169 | NAME_ITEM = 2 170 | 171 | @staticmethod 172 | def t(ty: 'Struct.StructFieldColorType', st): 173 | colors = { 174 | Struct.StructFieldColorType.BASETYPE_ITEM: 141, 175 | Struct.StructFieldColorType.TOKEN_ITEM: 189, 176 | Struct.StructFieldColorType.NAME_ITEM: 60, 177 | } 178 | code = f'\x1b[38;5;{colors[ty]}m' 179 | return f'{code}{str(st)}\x1b[0m' 180 | 181 | @staticmethod 182 | def t_base(st): 183 | return Struct.t(Struct.StructFieldColorType.BASETYPE_ITEM, st) 184 | 185 | @staticmethod 186 | def t_token(st): 187 | return Struct.t(Struct.StructFieldColorType.TOKEN_ITEM, st) 188 | 189 | @staticmethod 190 | def t_name(st): 191 | return Struct.t(Struct.StructFieldColorType.NAME_ITEM, st) 192 | 193 | @classmethod 194 | def size(cls, ptr_size=None): 195 | if not hasattr(cls, 'FIELDS'): 196 | return cls.SIZE 197 | if not hasattr(cls, '___SIZE'): 198 | size = 0 199 | for _, value in cls.FIELDS.items(): 200 | if isinstance(value, int): 201 | size += value & size_mask 202 | elif isinstance(value, Bitfield): 203 | size += value.size 204 | elif isinstance(value, pad_for_64_bit_only): 205 | if ptr_size is None: 206 | from lib0cyn.log import log 207 | err = "Trying to get size on variable (ptr) sized type directly without a ptr_size! This is programmer error!" 208 | print(err) 209 | log.error(err) 210 | import traceback 211 | traceback.print_stack() 212 | print(err) 213 | log.error(err) 214 | log.error("Exiting now. Go fix this.") 215 | exit(404) 216 | if ptr_size == 8: 217 | size += value.size 218 | elif issubclass(value, uintptr_t): 219 | if ptr_size is None: 220 | from lib0cyn.log import log 221 | err = "Trying to get size on variable (ptr) sized type directly without a ptr_size! This is programmer error!" 222 | print(err) 223 | log.error(err) 224 | import traceback 225 | traceback.print_stack() 226 | print(err) 227 | log.error(err) 228 | log.error("Exiting now. Go fix this.") 229 | exit(404) 230 | size += ptr_size 231 | setattr(cls, '___VARIABLE_SIZE', True) 232 | elif issubclass(value, StructUnion): 233 | size += value.size 234 | elif issubclass(value, Struct): 235 | if value == cls: 236 | raise AssertionError(f"Recursive type definition on {cls.__name__}") 237 | if hasattr(value, 'FIELDS'): 238 | size += value.size(ptr_size=ptr_size) 239 | else: 240 | size += value.size(ptr_size=ptr_size) 241 | if not hasattr(cls, '___VARIABLE_SIZE'): 242 | setattr(cls, "___SIZE", size) 243 | return size 244 | else: 245 | return getattr(cls, "___SIZE") 246 | 247 | # noinspection PyProtectedMember 248 | @staticmethod 249 | def create_with_bytes(struct_class, raw, byte_order="little", ptr_size=8): 250 | """ 251 | Unpack a struct from raw bytes 252 | 253 | :param struct_class: Struct subclass 254 | :param raw: Bytes 255 | :param ptr_size: 256 | :param byte_order: Little/Big Endian Struct Unpacking 257 | :return: struct_class Instance 258 | """ 259 | instance: Struct = struct_class(byte_order) 260 | current_off = 0 261 | raw = bytearray(raw) 262 | inst_raw = bytearray() 263 | 264 | # I *Genuinely* cannot figure out where in the program the size mismatch is happening. This should hotfix? 265 | raw = raw[:struct_class.size(ptr_size=ptr_size)] 266 | 267 | for field in instance._fields: 268 | value = instance._field_sizes[field] 269 | instance._field_offsets[field] = current_off 270 | 271 | field_value = None 272 | 273 | if isinstance(value, int): 274 | field_type = type_mask & value 275 | size = size_mask & value 276 | 277 | data = raw[current_off:current_off + size] 278 | 279 | if field_type == type_str: 280 | field_value = data.decode('utf-8').replace('\x00', '') 281 | 282 | elif field_type == type_bytes: 283 | field_value = bytes(data) 284 | 285 | elif field_type == type_uint: 286 | field_value = int.from_bytes(data, byte_order) 287 | 288 | elif field_type == type_sint: 289 | field_value = int.from_bytes(data, byte_order) 290 | field_value = _uint_to_int(field_value, size * 8) 291 | 292 | elif isinstance(value, Bitfield): 293 | size = value.size 294 | data = raw[current_off:current_off + size] 295 | assert len(data) == size 296 | value.decode_bitfield(data) 297 | for f, fv in value.decoded_fields.items(): 298 | setattr(instance, f, fv) 299 | field_value = None 300 | 301 | elif isinstance(value, pad_for_64_bit_only): 302 | size = value.size if ptr_size == 8 else 0 303 | if size != 0: 304 | data = raw[current_off:current_off + size] 305 | field_value = int.from_bytes(data, byte_order) 306 | else: 307 | data = bytearray() 308 | field_value = 0 309 | 310 | elif issubclass(value, uintptr_t): 311 | size = ptr_size 312 | data = raw[current_off:current_off + size] 313 | field_value = int.from_bytes(data, byte_order) 314 | 315 | elif issubclass(value, StructUnion): 316 | data = raw[current_off:current_off + value.SIZE] 317 | size = value.SIZE 318 | field_value = value() 319 | field_value.load_from_bytes(data) 320 | 321 | elif issubclass(value, Struct): 322 | size = value.size(ptr_size=ptr_size) 323 | data = raw[current_off:current_off + size] 324 | field_value = Struct.create_with_bytes(value, data) 325 | 326 | else: 327 | raise AssertionError 328 | 329 | if field_value is not None: 330 | setattr(instance, field, field_value) 331 | inst_raw += data 332 | current_off += size 333 | 334 | instance.pre_init() 335 | instance.initialized = True 336 | instance.post_init() 337 | 338 | return instance 339 | 340 | @staticmethod 341 | def create_with_values(struct_class, values, byte_order="little"): 342 | """ 343 | Pack/Create a struct given field values 344 | 345 | :param byte_order: 346 | :param struct_class: Struct subclass 347 | :param values: List of values 348 | :return: struct_class Instance 349 | """ 350 | 351 | instance: Struct = struct_class(byte_order) 352 | 353 | # noinspection PyProtectedMember 354 | for i, field in enumerate(instance._fields): 355 | setattr(instance, field, values[i]) 356 | 357 | instance.pre_init() 358 | instance.initialized = True 359 | instance.post_init() 360 | return instance 361 | 362 | @property 363 | def type_name(self): 364 | return self.__class__.__name__ 365 | 366 | @property 367 | def description(self): 368 | return "" 369 | 370 | @property 371 | def raw(self): 372 | raw = bytearray() 373 | for field in self._fields: 374 | size = self._field_sizes[field] 375 | 376 | field_dat = getattr(self, field) 377 | 378 | data = None 379 | 380 | if isinstance(field_dat, int): 381 | data = field_dat.to_bytes(size, byteorder=self.byte_order) 382 | elif isinstance(field_dat, bytearray) or isinstance(field_dat, bytes): 383 | data = field_dat 384 | elif isinstance(field_dat, str): 385 | data = field_dat.encode('utf-8') 386 | pad_size = size & size_mask 387 | if len(data) < pad_size: 388 | data += b'\x00' * (pad_size - len(data)) 389 | elif issubclass(size, Struct): 390 | data = field_dat.raw 391 | 392 | assert data is not None 393 | 394 | raw += bytearray(data) 395 | 396 | return raw 397 | 398 | def __eq__(self, other): 399 | try: 400 | for field in self._fields: 401 | if getattr(self, field) != getattr(other, field): 402 | return False 403 | except AttributeError: 404 | return False 405 | return True 406 | 407 | def __ne__(self, other): 408 | return not self.__eq__(other) 409 | 410 | def __repr__(self): 411 | return str(self) 412 | 413 | @staticmethod 414 | def _default_field_render(struct, field, indent_size=2, newline_breaks=False): 415 | try: 416 | attr = getattr(struct, field) 417 | except AttributeError: 418 | attr = struct._field_sizes[field] 419 | if isinstance(attr, str): 420 | field_item = struct.t_base(f'"{attr}"') 421 | elif isinstance(attr, bytearray) or isinstance(attr, bytes): 422 | field_item = struct.t_base(attr) 423 | elif isinstance(attr, int): 424 | field_item = struct.t_base(hex(attr)) 425 | elif isinstance(attr, Bitfield): 426 | if newline_breaks: 427 | attr: Bitfield = attr 428 | field_item = '\n' 429 | for subfield in attr.fields: 430 | field_item += " " * (indent_size + 2) + subfield + '=' + str(getattr(struct, subfield)) + '\n' 431 | else: 432 | attr: Bitfield = attr 433 | field_item = '' 434 | for subfield in attr.fields: 435 | field_item += subfield + '=' + str(getattr(struct, subfield)) + ', ' 436 | elif issubclass(attr.__class__, Struct): 437 | if newline_breaks: 438 | field_item = '\n' + " " * (indent_size + 2) + attr.render_indented(indent_size + 2) 439 | else: 440 | field_item = attr.render_color() 441 | else: 442 | field_item = str(attr) 443 | return field_item 444 | 445 | def __str__(self): 446 | return strip_ansi(self.render_color()) 447 | 448 | def render_color(self): 449 | text = f'{Struct.t_name(self.__class__.__name__)} {Struct.t_token("{")} ' 450 | for field in self._fields: 451 | composer = self._field_composers[field] if field in self._field_composers else self._default_field_render 452 | composer_args = inspect.getfullargspec(composer).args 453 | args = {} 454 | if 'indent_size' in composer_args: 455 | args['indent_size'] = 0 456 | if 'newline_breaks' in composer_args: 457 | args['newline_breaks'] = False 458 | field_item = composer(self, field, **args) 459 | has_end_comma = self._fields.index(field) + 1 != len(self._fields) 460 | text += f'{Struct.t_token(field)}{Struct.t_token("=")}{field_item}{Struct.t_token(",") if has_end_comma else ""} ' 461 | return text + Struct.t_token("}") 462 | 463 | def render_indented(self, indent_size=2) -> str: 464 | text = f'{Struct.t_name(self.__class__.__name__)}\n' 465 | for field in self._fields: 466 | composer = self._field_composers[field] if field in self._field_composers else self._default_field_render 467 | composer_args = inspect.getfullargspec(composer).args 468 | args = {} 469 | if 'indent_size' in composer_args: 470 | args['indent_size'] = indent_size 471 | if 'newline_breaks' in composer_args: 472 | args['newline_breaks'] = True 473 | field_item = composer(self, field, **args) 474 | text += f'{" " * indent_size}{field}{Struct.t_token("=")}{field_item}\n' 475 | return text 476 | 477 | def serialize(self): 478 | struct_dict = {'type': self.__class__.__name__} 479 | 480 | for field in self._fields: 481 | field_item = None 482 | if isinstance(getattr(self, field), str): 483 | field_item = getattr(self, field) 484 | elif isinstance(getattr(self, field), bytearray) or isinstance(getattr(self, field), bytes): 485 | field_item = _bytes_to_hex(getattr(self, field)) 486 | elif isinstance(getattr(self, field), int): 487 | field_item = getattr(self, field) 488 | elif issubclass(getattr(self, field).__class__, Struct): 489 | field_item = getattr(self, field).serialize() 490 | struct_dict[field] = field_item 491 | 492 | return struct_dict 493 | 494 | def __init__(self, fields=None, sizes=None, byte_order="little"): 495 | if hasattr(self.__class__, 'FIELDS'): 496 | # new method 497 | fields = list(self.__class__.FIELDS.keys()) 498 | sizes = list(self.__class__.FIELDS.values()) 499 | else: 500 | if sizes is None: 501 | raise AssertionError( 502 | "Do not use the bare Struct class; it must be implemented in an actual type; Missing Sizes") 503 | 504 | if fields is None: 505 | raise AssertionError( 506 | "Do not use the bare Struct class; it must be implemented in an actual type; Missing Fields") 507 | 508 | fields = list(fields) 509 | sizes = list(sizes) 510 | 511 | self.initialized = False 512 | 513 | self.super = super() 514 | self._fields = fields 515 | self.byte_order = byte_order 516 | 517 | self._field_sizes = {} 518 | self._field_offsets = {} 519 | self._field_composers = {} 520 | 521 | for index, i in enumerate(fields): 522 | self._field_sizes[i] = sizes[index] 523 | 524 | self.off = 0 525 | 526 | def add_field_composer(self, field, func): 527 | self._field_composers[field] = func 528 | 529 | def pre_init(self): 530 | """stub for subclasses. gets called before patch code is enabled""" 531 | pass 532 | 533 | def post_init(self): 534 | """stub for subclasses. gets called *after* patch code is enabled""" 535 | pass 536 | -------------------------------------------------------------------------------- /tests/build.ninja: -------------------------------------------------------------------------------- 1 | 2 | 3 | rule build_library_x86 4 | command = clang $in -o $out -framework Foundation -dynamiclib -arch x86_64 5 | 6 | rule build_bin_x86 7 | command = clang $in -o $out -framework Foundation -arch x86_64 8 | 9 | rule build_bin_arm64 10 | command = clang $in -o $out -framework Foundation -arch arm64 11 | 12 | rule build_bin_armv7 13 | command = clang $in -o $out -framework Foundation -arch armv7 -target armv7-apple-ios -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk 14 | 15 | rule build_bin_arm6432 16 | command = clang $in -o $out -L./src/ -framework Foundation -Wl,-undefined,dynamic_lookup -target arm64_32-apple-watchos7.0 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/ 17 | 18 | rule lipo_combine 19 | command = lipo -create $in -output $out 20 | 21 | rule sign 22 | command = cp $in $out; codesign -s - --ent src/testent.xml $out --force 23 | 24 | rule clean 25 | command = #rm -r $out 26 | 27 | 28 | build bins/testlib1.dylib: build_library_x86 src/testlib1.m 29 | build bins/testbin1: build_bin_x86 src/testbin1.m 30 | build bins/testbin1.signed: sign bins/testbin1 31 | 32 | build .build/testbin1_arm: build_bin_arm64 src/testbin1.m 33 | build .build/testbin1_x86: build_bin_x86 src/testbin1.m 34 | build .build/testbin1_v7: build_bin_armv7 src/testbin1.m 35 | build .build/testbin1_6432: build_bin_arm6432 src/testbin1.m 36 | build bins/testbin1.fat: lipo_combine .build/testbin1_arm .build/testbin1_x86 .build/testbin1_v7 .build/testbin1_6432 37 | build .build: clean bins/testbin1.fat 38 | 39 | -------------------------------------------------------------------------------- /tests/src/testbin1.m: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // clang testbin1.m -o testbin1 -framework Foundation 4 | 5 | @interface TestClassOne : NSObject 6 | 7 | @property CGRect testPropertyOne; 8 | @property (atomic, readonly) BOOL testPropertyTwo; 9 | 10 | -(NSUInteger)testInstanceMethodOne; 11 | +(BOOL)testClassMethodOne; 12 | 13 | @end 14 | 15 | @implementation TestClassOne 16 | 17 | - (NSUInteger)testInstanceMethodOne 18 | { 19 | return 0; 20 | } 21 | 22 | + (BOOL)testClassMethodOne 23 | { 24 | return YES; 25 | } 26 | 27 | @end 28 | 29 | int main(void) { 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /tests/src/testent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.apple.security.cs.allow-jit 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.cs.debugger 10 | 11 | com.apple.security.get-task-allow 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/src/testlib1.m: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // clang testbin1.m -o testlib1.dylib -framework Foundation -dynamiclib 4 | 5 | @interface TestClassOne : NSObject 6 | 7 | @property CGRect testPropertyOne; 8 | @property (atomic, readonly) BOOL testPropertyTwo; 9 | 10 | -(NSUInteger)testInstanceMethodOne; 11 | +(BOOL)testClassMethodOne; 12 | 13 | @end 14 | 15 | @implementation TestClassOne 16 | 17 | - (NSUInteger)testInstanceMethodOne 18 | { 19 | return 0; 20 | } 21 | 22 | + (BOOL)testClassMethodOne 23 | { 24 | return YES; 25 | } 26 | 27 | @end 28 | 29 | //int main(void) { 30 | // return 0; 31 | //} 32 | --------------------------------------------------------------------------------