Kernel-Agnostic Native ARM64 Dylib Embedder — host-side Mach-O patching for IPA-Patch tweaks.
Kanade is the host-side Python tooling for IPA-Patch tweaks — it rewrites the target Mach-O binary on disk before signing, embedding code caves and dylib load commands so that runtime hook delivery never touches __TEXT. This is the only viable approach on iOS 18–26 sideloaded targets, where the kernel Code Signing Monitor kills any process that writes to its own __TEXT segment at runtime.
The runtime C/ObjC side (slot resolution, image lookup, orig-trampoline recovery) lives in IPA-Patch/Chinlan. Kanade and Chinlan are designed to be used together.
On iOS 18–26, runtime inline-rewrite hooking (MSHookFunction, Dobby, Frida) is killed by CSM on non-jailbroken targets. The IPA-Patch approach:
- Each hook site's first instruction is overwritten with
B <cave>. - A small trampoline cave is appended to the binary's
__TEXTzero-fill tail. - The cave reads a function pointer from a
__DATA,__bssslot at runtime and calls the tweak dylib through it. - The tweak dylib, on load, writes its hook address into that slot — a
__DATAwrite, which CSM allows.
Steps 1–3 happen at build time on the host. This repo provides the Python tooling that performs them.
encode.py— pure-Python ARM64 instruction encoders (4-byte little-endian output, no external assembler needed at build time).machoops.py— generic Mach-O ops:add_lc_load_dylib,reserve_hook_slot,assert_slot_in_bss,iter_thin_binaries. Backed bylief.caves.py— idempotent patch engine:apply_patches(target, patches, cave_patches, cave_region). Applies inline byte patches and routes one-instruction sites to code-cave payloads.patch_macho.py— CLI driver:python3 -m tools.patch_macho --recipe NAME TARGET. The recipe module owns every per-target constant.patch_plist.py— CLI:python3 -m tools.patch_plist --recipe NAME PLIST(or--set KEY=VALUEfor one-off overrides).verify_lc_load.py— post-patch smoke test that confirms the expectedLC_LOAD_DYLIBis present in the patched binary.verify_sites.py— cross-checks hook-site RVAs against adump.csindex to catch RVA drift after a target app update.build_patched_ipa.sh— end-to-end IPA pipeline: decrypt → patch Mach-O → patch plist → inject dylib → repack → sign. Driven by--recipe / --framework / --dylib / --input.scaffold_recipe.py— scaffolds a new per-target recipe module.
tools is a PEP 420 namespace package. Consumer projects can add tools/recipes/<name>.py alongside Kanade and import everything through the same tools.* namespace.
<consumer>/
├── shared/ ← this repo (git submodule)
├── Sources/Chinlan/ ← Chinlan runtime (git submodule)
│
├── assets/
│ └── <version>/ ← one directory per target app version
│ ├── AppName-<version>.ipa ← decrypted IPA (gitignored)
│ ├── dump.cs ← il2cpp dump (gitignored, generated by dump.py)
│ └── dump.cs.index.json ← method index (gitignored, generated by dump.py)
│
├── recipes/
│ ├── __init__.py ← version dispatcher; re-exports PATCHES / CAVE_PATCHES / _SITES
│ ├── common.py ← cave builders, hook ID enum, DYLIB_PATH, PLIST_KEYS
│ ├── v1_0_1.py ← per-version RVAs and slot addresses
│ └── v1_0_2.py
│
├── vendor/
│ └── dobby/ ← static Dobby for jailed builds (committed)
│
└── .cache/
└── Il2CppDumper/ ← downloaded on first use by dump.py (gitignored)
assets/*/dump.cs
assets/*/dump.cs.index.json
.cache/build_patched_ipa.sh --input (and verify_sites --ipa) both expect a
decrypted IPA — the recipe pipeline cannot operate on anything else.
Every App Store app ships FairPlay-encrypted: the main executable and its
frameworks are stored as ciphertext on disk, and the kernel only decrypts
them in memory at launch, on the device the purchase is tied to. Kanade
patches the target Mach-O on disk, before signing — it locates each hook
site by its machine-code prologue and overwrites it with B <cave>. Against
an encrypted binary those bytes are meaningless ciphertext, so the recipe can
neither find nor patch them. The input must be decrypted first.
A decrypted IPA is one whose Mach-O slices have had the FairPlay layer
stripped — the cryptid flag cleared and the encrypted pages dumped in their
plaintext form. You make one once per app version, on a device that can
already run the target app:
- TrollDecrypt (TrollStore)
— dumps a decrypted
.ipastraight to the Files app. Easiest. - palera1n + Filza — jailbreak, then pull the decrypted bundle off disk.
- Any on-device FairPlay dumper works; the only requirement is that the
resulting
.ipais decrypted.
Drop the result at assets/<version>/AppName-<version>.ipa (gitignored).
Kanade ships only the tooling — never the target app — so you always supply
your own legally-obtained, decrypted copy.
Add both submodules to your consumer project:
git submodule add https://github.com/IPA-Patch/Kanade.git shared
git submodule add https://github.com/IPA-Patch/Chinlan.git Sources/ChinlanWire the Python tooling via PYTHONPATH:
PYTHONPATH=shared:. python3 -m tools.patch_macho --recipe recipes TARGET
PYTHONPATH=shared:. python3 -m tools.verify_sites \
--recipe recipes \
--version 1.0.2 \
--index assets/1.0.2/dump.cs.index.json \
--ipa assets/1.0.2/AppName-1.0.2.ipaOr via pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["shared", "."]Write your recipe package under recipes/. The __init__.py reads an
environment variable of your choosing and re-exports the patch surface
that tools.patch_macho expects:
# recipes/common.py — shared across versions
TARGET_BASENAME = "UnityFramework"
DYLIB_PATH = "@executable_path/Frameworks/MyTweak.dylib"
PLIST_KEYS = {...}
# cave builders, hook ID enum, …
# recipes/v1_0_2.py — per-version constants only
CAVE_REGION = (0x826A000, 0x826C000)
HOOK_SLOT_RVA = 0x8F90CC0
SITES = [(rva, prologue_hex, hook_id, kind, label), ...]
# recipes/__init__.py — dispatcher
import os, importlib
_ver = os.environ.get("MY_TARGET_VERSION", "1.0.1")
_v = importlib.import_module(f"recipes.v{_ver.replace('.', '_')}")
PATCHES, CAVE_PATCHES, _SITES = common.build_exports(_v.SITES, ...)Per-target constants that live in each version module: RVAs, slot addresses,
AFK patch bytes, prologue bytes. Everything else (cave builders, enum,
dylib path) lives in common.py.
See the Chinlan README for the runtime wiring (slot table, publish_*() helpers, bootstrap constructor).
tools/dump.py automates the full pipeline — it scans assets/ for IPA
files that are missing dump.cs or dump.cs.index.json, downloads
Il2CppDumper into .cache/ on
first use, runs it against the extracted UnityFramework + global-metadata.dat,
and writes both files to assets/<version>/.
python3 shared/tools/dump.py # process all missing versions
python3 shared/tools/dump.py --force # re-dump even if dump.cs existsRequires dotnet 8.x on PATH (apt install dotnet-sdk-8.0 on Ubuntu 24.04).
When the target app updates, its RVAs will change. Run verify_sites against
the new build before patching:
PYTHONPATH=shared:. python3 -m tools.verify_sites \
--recipe recipes \
--version 1.0.2 \
--index assets/1.0.2/dump.cs.index.json \
--ipa assets/1.0.2/AppName-1.0.2.ipaAny mismatch is reported with the expected vs. found prologue bytes and the
dump line number. Create a new recipes/v<maj>_<min>_<patch>.py with the
updated addresses and register it in recipes/__init__.py.
make test # pytest
make lint # ruff check
make format # ruff formatThe encoder golden values were generated with the LLVM assembler shipped in theos's linux iphone toolchain. To regenerate one:
$THEOS/toolchain/linux/iphone/bin/llvm-mc \
-triple=arm64-apple-ios -show-encoding <<< 'stp x29, x30, [sp, #-0x90]!'Update the matching bytes.fromhex(...) literal in tests/test_encode.py.
MIT — see LICENSE.