diff --git a/README.md b/README.md index adc16d0..4e3d55d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyzule-rw / cyan -rewriting [pyzule](https://github.com/asdfzxcvbn/pyzule), but actually good this time! +rewriting [pyzule](https://github.com/asdfzxcvbn/pyzule), but actually good this time! btw.. cyan/pyzule looks best with a monospace font !! :D this is pretty much just a hobby project that i'll work on when i'm bored. pyzule in its current state is kinda ugly, but it works and i haven't really had the motivation to work on it. there really isn't any need for improvements, anyway.. diff --git a/extras/CydiaSubstrate.framework/CydiaSubstrate b/extras/CydiaSubstrate.framework/CydiaSubstrate new file mode 100644 index 0000000..8deb418 Binary files /dev/null and b/extras/CydiaSubstrate.framework/CydiaSubstrate differ diff --git a/extras/CydiaSubstrate.framework/Info.plist b/extras/CydiaSubstrate.framework/Info.plist new file mode 100644 index 0000000..6bc23c5 --- /dev/null +++ b/extras/CydiaSubstrate.framework/Info.plist @@ -0,0 +1,25 @@ + + + + + CFBundleExecutable + CydiaSubstrate + + CFBundleIdentifier + ellekit + + CFBundleName + ElleKit + + CFBundleShortVersionString + 1.1.3 + + CFBundleVersion + 1.1.3 + + UIRequiredDeviceCapabilities + + arm64 + + + diff --git a/extras/CydiaSubstrate.framework/LICENSE b/extras/CydiaSubstrate.framework/LICENSE new file mode 100644 index 0000000..83ac4b7 --- /dev/null +++ b/extras/CydiaSubstrate.framework/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2022 Évelyne Bélanger All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither ElleKit nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/__main__.py b/src/__main__.py index d4275ca..85adcc9 100755 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,12 +1,18 @@ #!/usr/bin/env python3 # cyan, aka pyzule-rw; by zx, 2024 +import sys import argparse import cyan def main() -> None: + if sys.version_info < (3, 12): + sys.exit("[!] please upgrade to python 3.12 or higher") + elif sys.platform == "win32": + sys.exit("[!] windows is not supported") + parser = argparse.ArgumentParser( description="cyan, an azule \"clone\" for modifying iOS apps" ) @@ -29,21 +35,14 @@ def main() -> None: "--overwrite", action="store_true", help="skip overwrite confirmation" ) + parser.add_argument( + "--ignore-encrypted", action="store_true", + help="skip main binary encryption check" + ) cyan.main(parser) + if __name__ == "__main__": main() -# INPUT = os.path.realpath(args.input) -# WORKING_DIR = os.getcwd() -# PZ_DIR = os.path.expanduser("~/.config/cyan") -# changed = False - -# if not (INPUT.endswith(".ipa") or INPUT.endswith(".app")): -# sys.exit("[!] input must be a .ipa/.app") -# elif not os.path.exists(INPUT): -# sys.exit("[!] input does not exist") -# elif not args.o and (input("[<] override your input file? [y/N] ").strip() -# .lower() not in ("y", "yes")): -# sys.exit("[!] not overriding input, missing -o") diff --git a/src/cyan.py b/src/cyan.py index 90530fd..86481f3 100644 --- a/src/cyan.py +++ b/src/cyan.py @@ -1,7 +1,10 @@ import os +import sys from argparse import ArgumentParser +from tempfile import TemporaryDirectory import tbhutils +import tbhtypes def main(parser: ArgumentParser) -> None: @@ -20,6 +23,16 @@ def main(parser: ArgumentParser) -> None: if arg_err is not None: parser.error(arg_err) - # verbose notices - if args.o + INPUT_IS_IPA = True if args.i.endswith(".ipa") else False + # OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False + + with TemporaryDirectory() as tmpdir: + app_path, plist_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA) + app = tbhtypes.AppBundle(app_path, plist_path) + + if app.executable.is_encrypted(): + if args.ignore_encrypted: + print("[?] main binary is encrypted, ignoring") + else: + sys.exit("[!] main binary is encrypted; exiting") diff --git a/src/tbhtypes.py b/src/tbhtypes.py new file mode 100644 index 0000000..e2ac6ad --- /dev/null +++ b/src/tbhtypes.py @@ -0,0 +1,31 @@ +import os +import sys +from subprocess import run + +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"]}" + ) + + +class Executable: + td = tbhutils.get_tools_dir() + otool = f"{td}/otool" + + def __init__(self, path: str): + if not os.path.isfile(path): + sys.exit(f"[!] {path} does not exist (executable)") + + self.path = path + + def is_encrypted(self) -> bool: + proc = run([self.otool, "-l", self.path]) + return b"cryptid 1" in proc.stdout + + diff --git a/src/tbhutils.py b/src/tbhutils.py index 6c4281d..e33b0d3 100644 --- a/src/tbhutils.py +++ b/src/tbhutils.py @@ -1,6 +1,12 @@ import os -from typing import Optional +import sys +import shutil +import zipfile +import platform +import plistlib +from glob import glob from argparse import Namespace +from typing import Optional, Any def validate_inputs(args: Namespace) -> Optional[str]: @@ -13,5 +19,85 @@ def validate_inputs(args: Namespace) -> Optional[str]: if not os.path.exists(args.i): return f"{args.i} does not exist" + if os.path.exists(args.o): + if args.overwrite: + return print("[*] output already exists; will overwrite !") + + try: + overwrite = input( + f"[<] {args.o} already exists, overwrite it? [Y/n] " + if args.output is not None + else "[<] no output was specified. overwrite the input? [Y/n] " + ).strip().lower() + except KeyboardInterrupt: + sys.exit("[>] bye!") + + if overwrite not in ("y", "yes", ""): + print("[>] quitting.") + sys.exit(0) + + if args.f is not None: + args.f = {os.path.normpath(f) for f in args.f} + nonexistent = [f for f in args.f if not os.path.exists(f)] + + if len(nonexistent) != 0: + print("[!] please ensure the following file(s) exist:") + for ne in nonexistent: + print(f"[?] - {ne}") + sys.exit(1) +def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]: + payload = f"{tmpdir}/Payload" + + if is_ipa: + print("[*] extracting ipa..") + + try: + with zipfile.ZipFile(path) as ipa: + names = ipa.namelist() + + if not any(name.startswith("Payload/") for name in names): + raise KeyError + elif not any(name.endswith(".app/Info.plist") for name in names): + sys.exit("[!] no Info.plist, invalid app") + + 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: + sys.exit(f"[!] {path} is not a zipfile (ipa)") + + print("[*] extracted ipa") + else: + if not os.path.isfile((plist := 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 + + +def get_tools_dir() -> 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/tools/{system}/{mach}" + + +def get_plist(path: str) -> dict[str, Any]: + try: + with open(path, "rb") as f: + return plistlib.load(f) + except Exception: + sys.exit(f"[!] couldn't read {path}") + diff --git a/tools/Darwin/arm64/install_name_tool b/tools/Darwin/arm64/install_name_tool new file mode 100644 index 0000000..0639bea Binary files /dev/null and b/tools/Darwin/arm64/install_name_tool differ diff --git a/tools/Darwin/arm64/ldid b/tools/Darwin/arm64/ldid new file mode 100644 index 0000000..8ef9ead Binary files /dev/null and b/tools/Darwin/arm64/ldid differ diff --git a/tools/Darwin/arm64/otool b/tools/Darwin/arm64/otool new file mode 100644 index 0000000..6ba6101 Binary files /dev/null and b/tools/Darwin/arm64/otool differ diff --git a/tools/Darwin/x86_64/install_name_tool b/tools/Darwin/x86_64/install_name_tool new file mode 100644 index 0000000..c0443ce Binary files /dev/null and b/tools/Darwin/x86_64/install_name_tool differ diff --git a/tools/Darwin/x86_64/ldid b/tools/Darwin/x86_64/ldid new file mode 100644 index 0000000..0c8968e Binary files /dev/null and b/tools/Darwin/x86_64/ldid differ diff --git a/tools/Darwin/x86_64/otool b/tools/Darwin/x86_64/otool new file mode 100644 index 0000000..353f5b3 Binary files /dev/null and b/tools/Darwin/x86_64/otool differ diff --git a/tools/Linux/aarch64/install_name_tool b/tools/Linux/aarch64/install_name_tool new file mode 100755 index 0000000..3a70c23 Binary files /dev/null and b/tools/Linux/aarch64/install_name_tool differ diff --git a/tools/Linux/aarch64/ldid b/tools/Linux/aarch64/ldid new file mode 100755 index 0000000..a1b6ee2 Binary files /dev/null and b/tools/Linux/aarch64/ldid differ diff --git a/tools/Linux/aarch64/otool b/tools/Linux/aarch64/otool new file mode 100755 index 0000000..91b913a Binary files /dev/null and b/tools/Linux/aarch64/otool differ diff --git a/tools/Linux/x86_64/install_name_tool b/tools/Linux/x86_64/install_name_tool new file mode 100755 index 0000000..2d85506 Binary files /dev/null and b/tools/Linux/x86_64/install_name_tool differ diff --git a/tools/Linux/x86_64/ldid b/tools/Linux/x86_64/ldid new file mode 100755 index 0000000..3eca5f0 Binary files /dev/null and b/tools/Linux/x86_64/ldid differ diff --git a/tools/Linux/x86_64/otool b/tools/Linux/x86_64/otool new file mode 100755 index 0000000..aea9330 Binary files /dev/null and b/tools/Linux/x86_64/otool differ