chore: python package

This commit is contained in:
zx
2024-09-03 19:46:22 -04:00
parent d7f38047be
commit 800bb8d5bf
11 changed files with 90 additions and 59 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
__pycache__/ __pycache__/
build/
*.egg-info

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
graft extras
graft tools

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cyan, aka pyzule-rw; by zx, 2024 # cyan, aka pyzule-rw; by zx, 2024
import os
import sys import sys
import argparse import argparse
@@ -11,11 +10,6 @@ def main() -> None:
sys.exit("[!] please upgrade to python 3.12 or higher") sys.exit("[!] please upgrade to python 3.12 or higher")
elif sys.platform == "win32": elif sys.platform == "win32":
sys.exit("[!] windows is not supported") 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( parser = argparse.ArgumentParser(
description="cyan, an azule \"clone\" for modifying iOS apps" description="cyan, an azule \"clone\" for modifying iOS apps"
@@ -40,8 +34,8 @@ def main() -> None:
help="skip main binary encryption check" help="skip main binary encryption check"
) )
import cyan from cyan import logic
cyan.main(parser) logic.main(parser)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -5,8 +5,7 @@ from subprocess import run
from argparse import ArgumentParser from argparse import ArgumentParser
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import tbhutils from cyan import tbhutils, tbhtypes
import tbhtypes
def main(parser: ArgumentParser) -> None: def main(parser: ArgumentParser) -> None:
@@ -46,10 +45,15 @@ def main(parser: ArgumentParser) -> None:
# done! # done!
if OUTPUT_IS_IPA: if OUTPUT_IS_IPA:
print("[*] generating ipa..") print("[*] generating ipa..")
run(["bash", f"{tbhtypes.Executable.isd}/tools/compressIPA.sh"], env={
"TMPDIR": tmpdir, run(
"OUTPUT": args.o ["bash", f"{tbhtypes.Executable.install_dir}/tools/compressIPA.sh"],
}) env={
"TMPDIR": tmpdir,
"OUTPUT": args.o
}
)
print(f"[*] generated ipa at {args.o}") print(f"[*] generated ipa at {args.o}")
else: else:
# create subdirectories if necessary # create subdirectories if necessary

10
cyan/tbhtypes/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
from .app_bundle import AppBundle
from .executable import Executable
from .leaving_cm import LeavingCM
__all__ = [
"AppBundle",
"Executable",
"LeavingCM"
]

View File

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

0
cyan/tbhtypes/dylib.py Normal file
View File

View File

@@ -6,31 +6,23 @@ from typing import Optional
import lief import lief
import tbhutils from cyan 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
)
class Executable: class Executable:
isd, td = tbhutils.get_tools_dir() install_dir, specific = tbhutils.get_tools_dir()
nt = f"{td}/install_name_tool" nt = f"{specific}/install_name_tool"
ldid = f"{td}/ldid" ldid = f"{specific}/ldid"
otool = f"{td}/otool" 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): if not os.path.isfile(path):
sys.exit(f"[!] {path} does not exist (executable)") sys.exit(f"[!] {path} does not exist (executable)")
self.path = path self.path = path
self.bundle = bundle self.bundle_path = bundle_path
self.bn = os.path.basename(path) self.bn = os.path.basename(path)
self.inj: Optional[lief.MachO.Binary] = None self.inj: Optional[lief.MachO.Binary] = None
@@ -46,12 +38,12 @@ class Executable:
def inject(self, tweaks: dict[str, str], tmpdir: str) -> None: def inject(self, tweaks: dict[str, str], tmpdir: str) -> None:
# we only inject into the main executable # we only inject into the main executable
assert self.bundle is not None assert self.bundle_path is not None
has_entitlements = False has_entitlements = False
ENT_PATH = f"{self.bundle.path}/cyan.entitlements" ENT_PATH = f"{self.bundle_path}/cyan.entitlements"
PLUGINS_DIR = f"{self.bundle.path}/PlugIns" PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
FRAMEWORKS_DIR = f"{self.bundle.path}/Frameworks" FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
with open(ENT_PATH, "wb") as entf: with open(ENT_PATH, "wb") as entf:
proc = subprocess.run( proc = subprocess.run(
@@ -115,11 +107,11 @@ class Executable:
if not dbn.endswith(".dylib"): if not dbn.endswith(".dylib"):
continue continue
dylib = Dylib(path) dylib = Executable(path)
dylib.fakesign() dylib.fakesign()
# fix dependencies # fix dependencies
for dep in dylib.dependencies: for dep in dylib.get_dependencies():
for cname in (common | tweaks): for cname in (common | tweaks):
if cname in dep: if cname in dep:
if cname.endswith(".framework"): if cname.endswith(".framework"):
@@ -149,7 +141,7 @@ class Executable:
for missing in needed: for missing in needed:
ip = f"{FRAMEWORKS_DIR}/{missing}" ip = f"{FRAMEWORKS_DIR}/{missing}"
existed = tbhutils.delete_if_exists(ip, 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: if not existed:
print(f"[*] auto-injected {missing}") print(f"[*] auto-injected {missing}")
@@ -171,12 +163,12 @@ class Executable:
self.insert_cmd(f"@rpath/{bn}/{bn[:-10]}") self.insert_cmd(f"@rpath/{bn}/{bn[:-10]}")
shutil.copytree(path, fpath) shutil.copytree(path, fpath)
else: else:
fpath = f"{self.bundle.path}/{bn}" fpath = f"{self.bundle_path}/{bn}"
existed = tbhutils.delete_if_exists(fpath, bn) existed = tbhutils.delete_if_exists(fpath, bn)
try: try:
shutil.copytree(path, fpath) shutil.copytree(path, fpath)
except NotADirectoryError: except NotADirectoryError:
shutil.copy2(path, self.bundle.path) shutil.copy2(path, self.bundle_path)
if not existed: if not existed:
print(f"[*] injected {bn}") print(f"[*] injected {bn}")
@@ -205,14 +197,6 @@ class Executable:
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore 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]: def get_dependencies(self) -> list[str]:
proc = subprocess.run( proc = subprocess.run(
[self.otool, "-L", self.path], [self.otool, "-L", self.path],
@@ -232,11 +216,3 @@ class Dylib(Executable):
return deps return deps
class LeavingCM:
def __enter__(self):
pass
def __exit__(self, i, d, c): # type: ignore
print("[*] deleting temporary directory..")

View File

@@ -0,0 +1,7 @@
class LeavingCM:
def __enter__(self):
pass
def __exit__(self, i, d, c): # type: ignore
print("[*] deleting temporary directory..")

View File

@@ -7,6 +7,7 @@ import plistlib
import subprocess import subprocess
from glob import glob from glob import glob
from argparse import Namespace from argparse import Namespace
from importlib import resources # type: ignore
from typing import Optional, Any from typing import Optional, Any
from tempfile import TemporaryDirectory 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]: def get_tools_dir() -> tuple[str, str]:
mach = platform.machine() mach = platform.machine()
system = platform.system() system = platform.system()
prefix = ""
if "iPhone" in mach or "iPad" in mach: if "iPhone" in mach or "iPad" in mach:
mach = "arm64" 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]: def get_plist(path: str) -> dict[str, Any]:

17
setup.py Normal file
View File

@@ -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"],
}
)