├── .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 |
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 |
--------------------------------------------------------------------------------