diff --git a/README.md b/README.md index dbb4aba..9cb4120 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ more coming soon! i'm trying to copy pyzule in order to deprecate it in favor of - remove watch app - fakesign the output ipa/app - thin all binaries to arm64, it can LARGELY reduce app size sometimes! +- remove all app extensions (or just encrypted ones!) ## notes @@ -38,9 +39,9 @@ to install or update cyan, just `pip install --force-reinstall git+https://githu [x] feat: fakesign -[] feat: thin binaries +[x] feat: thin binaries -[] feat: plugin operations (-q, -e) +[x] feat: plugin operations (-q, -e) [] feat: .cyan files (lol rip .pyzule files) diff --git a/cyan/__main__.py b/cyan/__main__.py index 383ab72..7c4db34 100755 --- a/cyan/__main__.py +++ b/cyan/__main__.py @@ -47,6 +47,14 @@ def main() -> None: "-q", "--thin", action="store_true", help="thin all binaries to arm64, may largely reduce size" ) + parser.add_argument( + "-e", "--remove-extensions", action="store_true", + help="remove all app extensions" + ) + parser.add_argument( + "-g", "--remove-encrypted", action="store_true", + help="only remove encrypted app extensions" + ) parser.add_argument( "-c", "--compress", metavar="level", type=int, default=6, diff --git a/cyan/logic.py b/cyan/logic.py index f5c84eb..858ca55 100644 --- a/cyan/logic.py +++ b/cyan/logic.py @@ -29,8 +29,8 @@ def main(parser: ArgumentParser) -> None: OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM(): - app_path, plist_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA) - app = tbhtypes.AppBundle(app_path, plist_path) + app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA) + app = tbhtypes.AppBundle(app_path) if app.executable.is_encrypted(): if args.ignore_encrypted: @@ -38,6 +38,13 @@ def main(parser: ArgumentParser) -> None: else: sys.exit("[!] main binary is encrypted; exiting") + # this goes before injection, + # since user might inject their own extensions + if args.remove_extensions: + app.remove_all_extensions() + elif args.remove_encrypted: + app.remove_encrypted_extensions() + if args.f is not None: app.executable.inject(args.f, tmpdir) diff --git a/cyan/tbhtypes/app_bundle.py b/cyan/tbhtypes/app_bundle.py index 5b3604d..946c09d 100644 --- a/cyan/tbhtypes/app_bundle.py +++ b/cyan/tbhtypes/app_bundle.py @@ -1,15 +1,15 @@ import os import shutil from glob import glob -from typing import Optional +from typing import Optional, Literal from .executable import Executable from .plist import Plist class AppBundle: - def __init__(self, path: str, plist_path: str): + def __init__(self, path: str): self.path = path - self.plist = Plist(plist_path) + self.plist = Plist(f"{path}/Info.plist") self.executable = Executable( f"{path}/{self.plist['CFBundleExecutable']}", @@ -18,25 +18,29 @@ class AppBundle: self.cached_executables: Optional[list[str]] = None - def remove(self, name: str) -> bool: - path = f"{self.path}/{name}" - if not os.path.exists(path): - return False + def remove(self, *names: str) -> bool: + existed = False - try: - shutil.rmtree(path) - except NotADirectoryError: - os.remove(path) + for name in names: + if self.path in name: # i do this in `remove_encrypted_extensions()` + path = name + else: + path = f"{self.path}/{name}" - return True + if not os.path.exists(path): + continue + + try: + shutil.rmtree(path) + except NotADirectoryError: + os.remove(path) + + existed = True + + return existed def remove_watch_apps(self) -> None: - removed = False - for name in ("Watch", "WatchKit", "com.apple.WatchPlaceholder"): - if self.remove(name): - removed = True - - if removed: + if self.remove("Watch", "WatchKit", "com.apple.WatchPlaceholder"): print("[*] removed watch app") else: print("[?] watch app not present") @@ -48,7 +52,7 @@ class AppBundle: glob(f"{self.path}/**/*.framework", recursive=True) ), []) # type: ignore - def mass_operate(self, op: str, func: str) -> None: + def mass_operate(self, op: str, func: Literal["fakesign", "thin"]) -> None: # this works since we call this after injecting if self.cached_executables is None: self.cached_executables = self.get_executables() @@ -76,3 +80,26 @@ class AppBundle: def thin_all(self) -> None: self.mass_operate("thinned", "thin") + def remove_all_extensions(self) -> None: + if self.remove("Extensions", "PlugIns"): + print("[*] removed app extensions") + else: + print("[?] no app extensions") + + def remove_encrypted_extensions(self) -> None: + removed: list[str] = [] + + # a singular * is used to not detect watch apps + for plugin in glob( + f"{self.path}/*/*.appex", recursive=True + ): + bundle = AppBundle(plugin) + if bundle.executable.is_encrypted(): + self.remove(plugin) + removed.append(os.path.basename(bundle.executable.path)) + + if len(removed) == 0: + print("[?] no encrypted plugins") + else: + print("[*] removed encrypted plugins:", ", ".join(removed)) + diff --git a/cyan/tbhutils.py b/cyan/tbhutils.py index 5072d3e..efde02b 100644 --- a/cyan/tbhutils.py +++ b/cyan/tbhutils.py @@ -8,7 +8,6 @@ from uuid import uuid4 from typing import Optional from glob import glob, iglob from argparse import Namespace -from tempfile import TemporaryDirectory def validate_inputs(args: Namespace) -> Optional[str]: @@ -50,7 +49,7 @@ def validate_inputs(args: Namespace) -> Optional[str]: sys.exit(1) -def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]: +def get_app(path: str, tmpdir: str, is_ipa: bool) -> str: payload = f"{tmpdir}/Payload" if is_ipa: @@ -67,7 +66,6 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]: ipa.extractall(tmpdir) app = glob(f"{payload}/*.app")[0] - plist = f"{app}/Info.plist" except (KeyError, IndexError): sys.exit("[!] couldn't find either Payload or app folder, invalid ipa") except zipfile.BadZipFile: @@ -75,14 +73,14 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]: print("[*] extracted ipa") else: - if not os.path.isfile((plist := f"{path}/Info.plist")): + if not os.path.isfile(f"{path}/Info.plist"): sys.exit("[!] no Info.plist, invalid app") print("[*] copying app..") shutil.copytree(path, (app := f"{payload}/{os.path.basename(path)}")) print("[*] copied app") - return app, plist + return app def get_tools_dir() -> tuple[str, str]: @@ -118,6 +116,7 @@ def delete_if_exists(path: str, bn: str) -> bool: # damn it, literally EVERY FUCKING python version before 3.12 FUCKING SUCKS +# no `delete` in `TemporaryDirectory` ?! GREAT !!! def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None: t2 = f"{tmpdir}/{uuid4()}" os.mkdir(t2)