From c1c0b2294ebb674a2d1913820b1d9eeabc4743ab Mon Sep 17 00:00:00 2001 From: zx Date: Wed, 18 Sep 2024 21:39:02 -0400 Subject: [PATCH] feat, refactor: entitlement merging, MainExecutable --- cyan/__main__.py | 4 + cyan/logic.py | 2 + cyan/tbhtypes/__init__.py | 2 + cyan/tbhtypes/app_bundle.py | 3 +- cyan/tbhtypes/executable.py | 111 ++------------------------- cyan/tbhtypes/main_executable.py | 124 +++++++++++++++++++++++++++++++ cyan/tbhutils.py | 11 +++ 7 files changed, 153 insertions(+), 104 deletions(-) create mode 100644 cyan/tbhtypes/main_executable.py diff --git a/cyan/__main__.py b/cyan/__main__.py index d21ed39..417ac49 100755 --- a/cyan/__main__.py +++ b/cyan/__main__.py @@ -50,6 +50,10 @@ def main() -> None: "-k", metavar="icon", help="modify the app's icon" ) + parser.add_argument( + "-x", metavar="entitlements", + help="add or modify entitlements to the main binary" + ) parser.add_argument( "-u", "--remove-supported-devices", action="store_true", diff --git a/cyan/logic.py b/cyan/logic.py index 3286391..7558b62 100644 --- a/cyan/logic.py +++ b/cyan/logic.py @@ -61,6 +61,8 @@ def main(parser: ArgumentParser) -> None: app.plist.change_minimum_version(args.m) if args.k is not None: app.change_icon(args.k, tmpdir) + if args.x is not None: # `validate_inputs()` made it a dict + app.executable.merge_entitlements(args.x, tmpdir) if args.remove_supported_devices: app.plist.remove_uisd() diff --git a/cyan/tbhtypes/__init__.py b/cyan/tbhtypes/__init__.py index 3741132..6970955 100644 --- a/cyan/tbhtypes/__init__.py +++ b/cyan/tbhtypes/__init__.py @@ -1,12 +1,14 @@ from .app_bundle import AppBundle from .executable import Executable from .leaving_cm import LeavingCM +from .main_executable import MainExecutable from .plist import Plist __all__ = [ "AppBundle", "Executable", "LeavingCM", + "MainExecutable", "Plist" ] diff --git a/cyan/tbhtypes/app_bundle.py b/cyan/tbhtypes/app_bundle.py index 9066ed6..5980b3e 100644 --- a/cyan/tbhtypes/app_bundle.py +++ b/cyan/tbhtypes/app_bundle.py @@ -5,6 +5,7 @@ from uuid import uuid4 from typing import Optional, Literal from .executable import Executable +from .main_executable import MainExecutable from .plist import Plist class AppBundle: @@ -12,7 +13,7 @@ class AppBundle: self.path = path self.plist = Plist(f"{path}/Info.plist", path) - self.executable = Executable( + self.executable = MainExecutable( f"{path}/{self.plist['CFBundleExecutable']}", path ) diff --git a/cyan/tbhtypes/executable.py b/cyan/tbhtypes/executable.py index 08b966e..964a084 100644 --- a/cyan/tbhtypes/executable.py +++ b/cyan/tbhtypes/executable.py @@ -1,6 +1,5 @@ import os import sys -import shutil import subprocess from typing import Optional @@ -33,7 +32,7 @@ class Executable: "CepheiPrefs.framework": "CepheiPrefs.framework" } - def __init__(self, path: str, bundle_path: Optional[str] = None): + def __init__(self, path: str): if not os.path.isfile(path): print(f"[!] {path} does not exist (executable)", file=sys.stderr) sys.exit( @@ -43,7 +42,6 @@ class Executable: ) self.path = path - self.bundle_path = bundle_path self.bn = os.path.basename(path) self.inj: Optional = None # type: ignore @@ -61,16 +59,14 @@ class Executable: return b"cryptid 1" in proc.stdout - def inject(self, tweaks: dict[str, str], tmpdir: str) -> None: - # we only inject into the main executable - assert self.bundle_path is not None + def remove_signature(self) -> None: + subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL) - has_entitlements = False - ENT_PATH = f"{self.bundle_path}/cyan.entitlements" - PLUGINS_DIR = f"{self.bundle_path}/PlugIns" - FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks" + def fakesign(self) -> bool: + return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0 - with open(ENT_PATH, "wb") as entf: + def write_entitlements(self, output: str) -> bool: + with open(output, "wb") as entf: proc = subprocess.run( [self.ldid, "-e", self.path], capture_output=True @@ -78,98 +74,7 @@ class Executable: entf.write(proc.stdout) - if os.path.getsize(ENT_PATH) > 0: - has_entitlements = True - - # iirc, injecting doesnt work (sometimes) if the file isn't signed - self.remove_signature() - - if any(t.endswith(".appex") for t in tweaks): - os.makedirs(PLUGINS_DIR, exist_ok=True) - - if any( - t.endswith(k) - for t in tweaks - for k in (".deb", ".dylib", ".framework") - ): - os.makedirs(FRAMEWORKS_DIR, exist_ok=True) - - # some apps really dont have this lol - subprocess.run( - [self.nt, "-add_rpath", "@executable_path/Frameworks", self.path], - stderr=subprocess.DEVNULL - ) - - # `extract_deb()` will modify `tweaks`, which is why we make a copy - cwd = os.getcwd() - for bn, path in dict(tweaks).items(): - if bn.endswith(".deb"): - tbhutils.extract_deb(path, tweaks, tmpdir) - continue - os.chdir(cwd) # i fucking hate jailbroken iOS utils. - - needed: set[str] = set() - for bn, path in tweaks.items(): - if bn.endswith(".appex"): - fpath = f"{PLUGINS_DIR}/{bn}" - existed = tbhutils.delete_if_exists(fpath, bn) - shutil.copytree(path, fpath) - elif bn.endswith(".dylib"): - path = shutil.copy2(path, tmpdir) - Executable(path).fix_dependencies(tweaks, needed) - - fpath = f"{FRAMEWORKS_DIR}/{bn}" - existed = tbhutils.delete_if_exists(fpath, bn) - self.inj_func(f"@rpath/{bn}") - shutil.move(path, FRAMEWORKS_DIR) - elif bn.endswith(".framework"): - fpath = f"{FRAMEWORKS_DIR}/{bn}" - existed = tbhutils.delete_if_exists(fpath, bn) - self.inj_func(f"@rpath/{bn}/{bn[:-10]}") - shutil.copytree(path, fpath) - else: - fpath = f"{self.bundle_path}/{bn}" - existed = tbhutils.delete_if_exists(fpath, bn) - try: - shutil.copytree(path, fpath) - except NotADirectoryError: - shutil.copy2(path, self.bundle_path) - - if not existed: - print(f"[*] injected {bn}") - - # orion has a *weak* dependency to substrate, - # but will still crash without it. nice !!!!!!!!!!! - if "Orion.framework" in needed: - needed.add("CydiaSubstrate.framework") - - for missing in needed: - real = self.common[missing] # "real" name, thanks substrate! - ip = f"{FRAMEWORKS_DIR}/{real}" - existed = tbhutils.delete_if_exists(ip, real) - shutil.copytree(f"{self.install_dir}/extras/{real}", ip) - - if not existed: - print(f"[*] auto-injected {real}") - - # FINALLY !! - if self.inj is not None: # type: ignore - self.inj.write(self.path) # type: ignore - - if has_entitlements: - subprocess.run([self.ldid, f"-S{ENT_PATH}", self.path]) - print("[*] restored entitlements") - - def remove_signature(self) -> None: - subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL) - - def fakesign(self, keep_entitlements: bool = True) -> bool: - cmd = [self.ldid, "-S"] - if keep_entitlements: - cmd.append("-M") - - subprocess.run(cmd + [self.path]) - return True + return os.path.getsize(output) > 0 def thin(self) -> bool: return subprocess.run( diff --git a/cyan/tbhtypes/main_executable.py b/cyan/tbhtypes/main_executable.py new file mode 100644 index 0000000..1eb104a --- /dev/null +++ b/cyan/tbhtypes/main_executable.py @@ -0,0 +1,124 @@ +import os +import shutil +import plistlib +import subprocess +from typing import Any +from plistlib import dump as pdump + +from cyan import tbhutils +from .executable import Executable + +class MainExecutable(Executable): + def __init__(self, path: str, bundle_path: str): + super().__init__(path) + self.bundle_path = bundle_path + + def inject(self, tweaks: dict[str, str], tmpdir: str) -> None: + ENT_PATH = f"{self.bundle_path}/cyan.entitlements" + PLUGINS_DIR = f"{self.bundle_path}/PlugIns" + FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks" + has_entitlements = self.write_entitlements(ENT_PATH) + + # iirc, injecting doesnt work (sometimes) if the file isn't signed + self.remove_signature() + + if any(t.endswith(".appex") for t in tweaks): + os.makedirs(PLUGINS_DIR, exist_ok=True) + + if any( + t.endswith(k) + for t in tweaks + for k in (".deb", ".dylib", ".framework") + ): + os.makedirs(FRAMEWORKS_DIR, exist_ok=True) + + # some apps really dont have this lol + subprocess.run( + [self.nt, "-add_rpath", "@executable_path/Frameworks", self.path], + stderr=subprocess.DEVNULL + ) + + # `extract_deb()` will modify `tweaks`, which is why we make a copy + cwd = os.getcwd() + for bn, path in dict(tweaks).items(): + if bn.endswith(".deb"): + tbhutils.extract_deb(path, tweaks, tmpdir) + continue + os.chdir(cwd) # i fucking hate jailbroken iOS utils. + + needed: set[str] = set() + for bn, path in tweaks.items(): + if bn.endswith(".appex"): + fpath = f"{PLUGINS_DIR}/{bn}" + existed = tbhutils.delete_if_exists(fpath, bn) + shutil.copytree(path, fpath) + elif bn.endswith(".dylib"): + path = shutil.copy2(path, tmpdir) + Executable(path).fix_dependencies(tweaks, needed) + + fpath = f"{FRAMEWORKS_DIR}/{bn}" + existed = tbhutils.delete_if_exists(fpath, bn) + self.inj_func(f"@rpath/{bn}") + shutil.move(path, FRAMEWORKS_DIR) + elif bn.endswith(".framework"): + fpath = f"{FRAMEWORKS_DIR}/{bn}" + existed = tbhutils.delete_if_exists(fpath, bn) + self.inj_func(f"@rpath/{bn}/{bn[:-10]}") + shutil.copytree(path, fpath) + else: + fpath = f"{self.bundle_path}/{bn}" + existed = tbhutils.delete_if_exists(fpath, bn) + try: + shutil.copytree(path, fpath) + except NotADirectoryError: + shutil.copy2(path, self.bundle_path) + + if not existed: + print(f"[*] injected {bn}") + + # orion has a *weak* dependency to substrate, + # but will still crash without it. nice !!!!!!!!!!! + if "Orion.framework" in needed: + needed.add("CydiaSubstrate.framework") + + for missing in needed: + real = self.common[missing] # "real" name, thanks substrate! + ip = f"{FRAMEWORKS_DIR}/{real}" + existed = tbhutils.delete_if_exists(ip, real) + shutil.copytree(f"{self.install_dir}/extras/{real}", ip) + + if not existed: + print(f"[*] auto-injected {real}") + + # FINALLY !! + if self.inj is not None: # type: ignore + self.inj.write(self.path) # type: ignore + + if has_entitlements: + self.sign_with_entitlements(ENT_PATH) + print("[*] restored entitlements") + + def merge_entitlements( + self, entitlements: dict[str, Any], tmpdir: str + ) -> None: + ENT_PATH = f"{tmpdir}/new.entitlements" + existing: dict[str, Any] + + if self.write_entitlements(ENT_PATH): # has entitlements + with open(ENT_PATH, "rb") as f: + existing = plistlib.load(f) + else: + existing = {} + + new = existing | entitlements + with open(ENT_PATH, "wb") as f2: + pdump(new, f2) + + self.sign_with_entitlements(ENT_PATH) + print("[*] modified entitlement keys:", ", ".join(entitlements)) + + def sign_with_entitlements(self, entitlements: str) -> bool: + return subprocess.run( + [self.ldid, f"-S{entitlements}", self.path] + ).returncode == 0 + diff --git a/cyan/tbhutils.py b/cyan/tbhutils.py index a8fda0e..c722d5b 100644 --- a/cyan/tbhutils.py +++ b/cyan/tbhutils.py @@ -9,6 +9,7 @@ from uuid import uuid4 from glob import glob, iglob from argparse import Namespace from typing import Optional, Any +from plistlib import load as pload HAS_ZIP = shutil.which("zip") is not None HAS_UNZIP = shutil.which("unzip") is not None @@ -68,6 +69,16 @@ def validate_inputs(args: Namespace) -> Optional[str]: if args.cyan is not None and not os.path.isfile(args.cyan): sys.exit(f"[!] {args.cyan} does not exist") + if args.x is not None: + if not os.path.isfile(args.x): + sys.exit(f"[!] {args.x} does not exist") + + try: + with open(args.x, "rb") as f: + args.x = pload(f) + except Exception: + sys.exit("[!] couldn't parse given entitlements file") + def get_app(path: str, tmpdir: str, is_ipa: bool) -> str: payload = f"{tmpdir}/Payload"