From 800bb8d5bf81eb3930fa6a1da8064cca1b6cfbd0 Mon Sep 17 00:00:00 2001 From: zx Date: Tue, 3 Sep 2024 19:46:22 -0400 Subject: [PATCH] chore: python package --- .gitignore | 3 + MANIFEST.in | 3 + {src => cyan}/__main__.py | 10 +--- src/cyan.py => cyan/logic.py | 16 +++-- cyan/tbhtypes/__init__.py | 10 ++++ cyan/tbhtypes/app_bundle.py | 14 +++++ cyan/tbhtypes/dylib.py | 0 .../tbhtypes/executable.py | 60 ++++++------------- cyan/tbhtypes/leaving_cm.py | 7 +++ {src => cyan}/tbhutils.py | 9 ++- setup.py | 17 ++++++ 11 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 MANIFEST.in rename {src => cyan}/__main__.py (80%) rename src/cyan.py => cyan/logic.py (88%) create mode 100644 cyan/tbhtypes/__init__.py create mode 100644 cyan/tbhtypes/app_bundle.py create mode 100644 cyan/tbhtypes/dylib.py rename src/tbhtypes.py => cyan/tbhtypes/executable.py (83%) create mode 100644 cyan/tbhtypes/leaving_cm.py rename {src => cyan}/tbhutils.py (94%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 372c13e..1c9077a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__/ +build/ +*.egg-info + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8a7f94e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +graft extras +graft tools + diff --git a/src/__main__.py b/cyan/__main__.py similarity index 80% rename from src/__main__.py rename to cyan/__main__.py index 4214fd5..469fd35 100755 --- a/src/__main__.py +++ b/cyan/__main__.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # cyan, aka pyzule-rw; by zx, 2024 -import os import sys import argparse @@ -11,11 +10,6 @@ def main() -> None: sys.exit("[!] please upgrade to python 3.12 or higher") elif sys.platform == "win32": sys.exit("[!] windows is not supported") - elif not ( - os.path.isdir("/opt/cyan") - or os.path.isdir("/var/jb/opt/cyan") - ): - sys.exit("[!] cyan must be installed in /opt/cyan, refer to the README") parser = argparse.ArgumentParser( description="cyan, an azule \"clone\" for modifying iOS apps" @@ -40,8 +34,8 @@ def main() -> None: help="skip main binary encryption check" ) - import cyan - cyan.main(parser) + from cyan import logic + logic.main(parser) if __name__ == "__main__": diff --git a/src/cyan.py b/cyan/logic.py similarity index 88% rename from src/cyan.py rename to cyan/logic.py index 3c7cb3c..cd80e50 100644 --- a/src/cyan.py +++ b/cyan/logic.py @@ -5,8 +5,7 @@ from subprocess import run from argparse import ArgumentParser from tempfile import TemporaryDirectory -import tbhutils -import tbhtypes +from cyan import tbhutils, tbhtypes def main(parser: ArgumentParser) -> None: @@ -46,10 +45,15 @@ def main(parser: ArgumentParser) -> None: # done! if OUTPUT_IS_IPA: print("[*] generating ipa..") - run(["bash", f"{tbhtypes.Executable.isd}/tools/compressIPA.sh"], env={ - "TMPDIR": tmpdir, - "OUTPUT": args.o - }) + + run( + ["bash", f"{tbhtypes.Executable.install_dir}/tools/compressIPA.sh"], + env={ + "TMPDIR": tmpdir, + "OUTPUT": args.o + } + ) + print(f"[*] generated ipa at {args.o}") else: # create subdirectories if necessary diff --git a/cyan/tbhtypes/__init__.py b/cyan/tbhtypes/__init__.py new file mode 100644 index 0000000..2a3e38c --- /dev/null +++ b/cyan/tbhtypes/__init__.py @@ -0,0 +1,10 @@ +from .app_bundle import AppBundle +from .executable import Executable +from .leaving_cm import LeavingCM + +__all__ = [ + "AppBundle", + "Executable", + "LeavingCM" +] + diff --git a/cyan/tbhtypes/app_bundle.py b/cyan/tbhtypes/app_bundle.py new file mode 100644 index 0000000..38f661d --- /dev/null +++ b/cyan/tbhtypes/app_bundle.py @@ -0,0 +1,14 @@ +from cyan.tbhutils import get_plist +from .executable import Executable + + +class AppBundle: + def __init__(self, path: str, plist_path: str): + self.path = path + self.plist = get_plist(plist_path) + + self.executable = Executable( + f"{path}/{self.plist["CFBundleExecutable"]}", + path + ) + diff --git a/cyan/tbhtypes/dylib.py b/cyan/tbhtypes/dylib.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tbhtypes.py b/cyan/tbhtypes/executable.py similarity index 83% rename from src/tbhtypes.py rename to cyan/tbhtypes/executable.py index 00ef22c..1a20f95 100644 --- a/src/tbhtypes.py +++ b/cyan/tbhtypes/executable.py @@ -6,31 +6,23 @@ from typing import Optional import lief -import tbhutils - - -class AppBundle: - def __init__(self, path: str, plist_path: str): - self.path = path - self.plist = tbhutils.get_plist(plist_path) - self.executable = Executable( - f"{path}/{self.plist["CFBundleExecutable"]}", - self - ) +from cyan import tbhutils class Executable: - isd, td = tbhutils.get_tools_dir() - nt = f"{td}/install_name_tool" - ldid = f"{td}/ldid" - otool = f"{td}/otool" + install_dir, specific = tbhutils.get_tools_dir() + nt = f"{specific}/install_name_tool" + ldid = f"{specific}/ldid" + otool = f"{specific}/otool" - def __init__(self, path: str, bundle: Optional[AppBundle] = None): + starters = ("\t/Library/", "\t@rpath", "\t@executable_path") + + def __init__(self, path: str, bundle_path: Optional[str] = None): if not os.path.isfile(path): sys.exit(f"[!] {path} does not exist (executable)") self.path = path - self.bundle = bundle + self.bundle_path = bundle_path self.bn = os.path.basename(path) self.inj: Optional[lief.MachO.Binary] = None @@ -46,12 +38,12 @@ class Executable: def inject(self, tweaks: dict[str, str], tmpdir: str) -> None: # we only inject into the main executable - assert self.bundle is not None + assert self.bundle_path is not None 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" + ENT_PATH = f"{self.bundle_path}/cyan.entitlements" + PLUGINS_DIR = f"{self.bundle_path}/PlugIns" + FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks" with open(ENT_PATH, "wb") as entf: proc = subprocess.run( @@ -115,11 +107,11 @@ class Executable: if not dbn.endswith(".dylib"): continue - dylib = Dylib(path) + dylib = Executable(path) dylib.fakesign() # fix dependencies - for dep in dylib.dependencies: + for dep in dylib.get_dependencies(): for cname in (common | tweaks): if cname in dep: if cname.endswith(".framework"): @@ -149,7 +141,7 @@ class Executable: for missing in needed: ip = f"{FRAMEWORKS_DIR}/{missing}" existed = tbhutils.delete_if_exists(ip, missing) - shutil.copytree(f"{self.isd}/extras/{missing}", ip) + shutil.copytree(f"{self.install_dir}/extras/{missing}", ip) if not existed: print(f"[*] auto-injected {missing}") @@ -171,12 +163,12 @@ class Executable: self.insert_cmd(f"@rpath/{bn}/{bn[:-10]}") shutil.copytree(path, fpath) else: - fpath = f"{self.bundle.path}/{bn}" + 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) + shutil.copy2(path, self.bundle_path) if not existed: print(f"[*] injected {bn}") @@ -205,14 +197,6 @@ class Executable: self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore - -class Dylib(Executable): - starters = ("\t/Library/", "\t@rpath", "\t@executable_path") - - def __init__(self, path: str): - super().__init__(path) - self.dependencies = self.get_dependencies() - def get_dependencies(self) -> list[str]: proc = subprocess.run( [self.otool, "-L", self.path], @@ -232,11 +216,3 @@ class Dylib(Executable): return deps - -class LeavingCM: - def __enter__(self): - pass - - def __exit__(self, i, d, c): # type: ignore - print("[*] deleting temporary directory..") - diff --git a/cyan/tbhtypes/leaving_cm.py b/cyan/tbhtypes/leaving_cm.py new file mode 100644 index 0000000..0c4ac12 --- /dev/null +++ b/cyan/tbhtypes/leaving_cm.py @@ -0,0 +1,7 @@ +class LeavingCM: + def __enter__(self): + pass + + def __exit__(self, i, d, c): # type: ignore + print("[*] deleting temporary directory..") + diff --git a/src/tbhutils.py b/cyan/tbhutils.py similarity index 94% rename from src/tbhutils.py rename to cyan/tbhutils.py index b3c3ad6..dacb3d1 100644 --- a/src/tbhutils.py +++ b/cyan/tbhutils.py @@ -7,6 +7,7 @@ import plistlib import subprocess from glob import glob from argparse import Namespace +from importlib import resources # type: ignore from typing import Optional, Any from tempfile import TemporaryDirectory @@ -85,13 +86,15 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]: def get_tools_dir() -> tuple[str, str]: mach = platform.machine() system = platform.system() - prefix = "" if "iPhone" in mach or "iPad" in mach: mach = "arm64" - prefix = "/var/jb" # sorry, rootless only !! - return (f"{prefix}/opt/cyan", f"{prefix}/opt/cyan/tools/{system}/{mach}") + with resources.files() as files: # type: ignore + return ( + str(files), # type: ignore + str(files / "tools" / system / mach) # type: ignore + ) def get_plist(path: str) -> dict[str, Any]: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dec1479 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup + +setup( + name="cyan", + version="1.0", + description="finally, pyzule doesn't suck", + author="zx", + author_email="hi@zxcvbn.fyi", + packages=["cyan", "cyan.tbhtypes"], + install_requires=["lief"], + python_requires=">=3.12", + include_package_data=True, + entry_points={ + "console_scripts": ["cyan=cyan.__main__:main"], + } +) +