Skip to content

IPA-Patch/Kanade

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kanade

Kanade icon

Kernel-Agnostic Native ARM64 Dylib Embedder — host-side Mach-O patching for IPA-Patch tweaks.

license platform arch org


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.

Why static patching

On iOS 18–26, runtime inline-rewrite hooking (MSHookFunction, Dobby, Frida) is killed by CSM on non-jailbroken targets. The IPA-Patch approach:

  1. Each hook site's first instruction is overwritten with B <cave>.
  2. A small trampoline cave is appended to the binary's __TEXT zero-fill tail.
  3. The cave reads a function pointer from a __DATA,__bss slot at runtime and calls the tweak dylib through it.
  4. The tweak dylib, on load, writes its hook address into that slot — a __DATA write, which CSM allows.

Steps 1–3 happen at build time on the host. This repo provides the Python tooling that performs them.

What's in this repo

  • 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 by lief.
  • 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=VALUE for one-off overrides).
  • verify_lc_load.py — post-patch smoke test that confirms the expected LC_LOAD_DYLIB is present in the patched binary.
  • verify_sites.py — cross-checks hook-site RVAs against a dump.cs index 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 directory layout

<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)

.gitignore additions

assets/*/dump.cs
assets/*/dump.cs.index.json
.cache/

The input IPA must be decrypted

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 .ipa straight 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 .ipa is 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.

Usage

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/Chinlan

Wire 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.ipa

Or 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).

Generating the dump index after a target app update

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 exists

Requires dotnet 8.x on PATH (apt install dotnet-sdk-8.0 on Ubuntu 24.04).

Checking for RVA drift after a target update

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.ipa

Any 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.

Development

make test       # pytest
make lint       # ruff check
make format     # ruff format

The 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.

License

MIT — see LICENSE.

About

Kernel-Agnostic Native ARM64 Dylib Embedder — host-side Python tooling for static binary patching on iOS 18+

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors