7 Commits

Author SHA1 Message Date
zx
4e05892e1d chore: bump version to v1.2.2 2024-10-12 14:56:32 -04:00
zx
54359e0935 fix: don't zip hidden files (lol @whoeevee) 2024-10-12 14:53:42 -04:00
zx
46752d0a26 chore: update orion to v1.0.2 2024-09-30 20:16:19 -04:00
zx
ce20baae4a chore: bump version to v1.2.1
fuck my life
2024-09-18 22:03:48 -04:00
zx
91494bc3a3 turns out ldid can already do this... 2024-09-18 22:02:51 -04:00
zx
edd34f6713 chore: bump version to v1.2 2024-09-18 21:45:21 -04:00
zx
c1c0b2294e feat, refactor: entitlement merging, MainExecutable 2024-09-18 21:39:02 -04:00
11 changed files with 142 additions and 108 deletions

View File

@@ -15,6 +15,7 @@ you can open an issue to request a feature :D !! also see my [recommended flags]
- remove watch app - remove watch app
- change the app icon - change the app icon
- fakesign the output ipa/app - fakesign the output ipa/app
- add custom entitlements to the main executable
- thin all binaries to arm64, it can LARGELY reduce app size sometimes! - thin all binaries to arm64, it can LARGELY reduce app size sometimes!
- remove all app extensions (or just encrypted ones!) - remove all app extensions (or just encrypted ones!)

View File

@@ -50,6 +50,10 @@ def main() -> None:
"-k", metavar="icon", "-k", metavar="icon",
help="modify the app's 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( parser.add_argument(
"-u", "--remove-supported-devices", action="store_true", "-u", "--remove-supported-devices", action="store_true",
@@ -95,7 +99,7 @@ def main() -> None:
) )
parser.add_argument( parser.add_argument(
"--version", action="version", version="cyan v1.1.6" "--version", action="version", version="cyan v1.2.2"
) )
from cyan import logic from cyan import logic

View File

@@ -12,7 +12,7 @@
<string>Orion</string> <string>Orion</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.1</string> <string>1.0.2</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>

Binary file not shown.

View File

@@ -61,6 +61,8 @@ def main(parser: ArgumentParser) -> None:
app.plist.change_minimum_version(args.m) app.plist.change_minimum_version(args.m)
if args.k is not None: if args.k is not None:
app.change_icon(args.k, tmpdir) app.change_icon(args.k, tmpdir)
if args.x is not None:
app.executable.merge_entitlements(args.x)
if args.remove_supported_devices: if args.remove_supported_devices:
app.plist.remove_uisd() app.plist.remove_uisd()

View File

@@ -1,12 +1,14 @@
from .app_bundle import AppBundle from .app_bundle import AppBundle
from .executable import Executable from .executable import Executable
from .leaving_cm import LeavingCM from .leaving_cm import LeavingCM
from .main_executable import MainExecutable
from .plist import Plist from .plist import Plist
__all__ = [ __all__ = [
"AppBundle", "AppBundle",
"Executable", "Executable",
"LeavingCM", "LeavingCM",
"MainExecutable",
"Plist" "Plist"
] ]

View File

@@ -5,6 +5,7 @@ from uuid import uuid4
from typing import Optional, Literal from typing import Optional, Literal
from .executable import Executable from .executable import Executable
from .main_executable import MainExecutable
from .plist import Plist from .plist import Plist
class AppBundle: class AppBundle:
@@ -12,7 +13,7 @@ class AppBundle:
self.path = path self.path = path
self.plist = Plist(f"{path}/Info.plist", path) self.plist = Plist(f"{path}/Info.plist", path)
self.executable = Executable( self.executable = MainExecutable(
f"{path}/{self.plist['CFBundleExecutable']}", f"{path}/{self.plist['CFBundleExecutable']}",
path path
) )

View File

@@ -1,6 +1,5 @@
import os import os
import sys import sys
import shutil
import subprocess import subprocess
from typing import Optional from typing import Optional
@@ -33,7 +32,7 @@ class Executable:
"CepheiPrefs.framework": "CepheiPrefs.framework" "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): if not os.path.isfile(path):
print(f"[!] {path} does not exist (executable)", file=sys.stderr) print(f"[!] {path} does not exist (executable)", file=sys.stderr)
sys.exit( sys.exit(
@@ -43,7 +42,6 @@ class Executable:
) )
self.path = path self.path = path
self.bundle_path = bundle_path
self.bn = os.path.basename(path) self.bn = os.path.basename(path)
self.inj: Optional = None # type: ignore self.inj: Optional = None # type: ignore
@@ -61,16 +59,14 @@ class Executable:
return b"cryptid 1" in proc.stdout return b"cryptid 1" in proc.stdout
def inject(self, tweaks: dict[str, str], tmpdir: str) -> None: def remove_signature(self) -> None:
# we only inject into the main executable subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL)
assert self.bundle_path is not None
has_entitlements = False def fakesign(self) -> bool:
ENT_PATH = f"{self.bundle_path}/cyan.entitlements" return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0
PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
with open(ENT_PATH, "wb") as entf: def write_entitlements(self, output: str) -> bool:
with open(output, "wb") as entf:
proc = subprocess.run( proc = subprocess.run(
[self.ldid, "-e", self.path], [self.ldid, "-e", self.path],
capture_output=True capture_output=True
@@ -78,98 +74,7 @@ class Executable:
entf.write(proc.stdout) entf.write(proc.stdout)
if os.path.getsize(ENT_PATH) > 0: return os.path.getsize(output) > 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
def thin(self) -> bool: def thin(self) -> bool:
return subprocess.run( return subprocess.run(

View File

@@ -0,0 +1,106 @@
import os
import shutil
import subprocess
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: str) -> None:
self.sign_with_entitlements(entitlements)
print("[*] merged new entitlements")
def sign_with_entitlements(self, entitlements: str) -> bool:
return subprocess.run(
[self.ldid, f"-S{entitlements}", "-M", self.path]
).returncode == 0

View File

@@ -9,6 +9,7 @@ from uuid import uuid4
from glob import glob, iglob from glob import glob, iglob
from argparse import Namespace from argparse import Namespace
from typing import Optional, Any from typing import Optional, Any
from plistlib import load as pload
HAS_ZIP = shutil.which("zip") is not None HAS_ZIP = shutil.which("zip") is not None
HAS_UNZIP = shutil.which("unzip") 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): if args.cyan is not None and not os.path.isfile(args.cyan):
sys.exit(f"[!] {args.cyan} does not exist") 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:
pload(f)
except Exception:
sys.exit("[!] couldn't parse given entitlements file")
def get_app(path: str, tmpdir: str, is_ipa: bool) -> str: def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
payload = f"{tmpdir}/Payload" payload = f"{tmpdir}/Payload"
@@ -197,8 +208,10 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
except FileNotFoundError: except FileNotFoundError:
pass pass
# don't zip hidden files to fix an installd error sometimes
# thanks a lot eevee 😭
subprocess.run( subprocess.run(
["zip", f"-{level}", "-r", output, "Payload"], ["zip", f"-{level}", "-r", output, "Payload", "-x", "*/.*"],
stdout=subprocess.DEVNULL stdout=subprocess.DEVNULL
) )
else: else:

View File

@@ -4,7 +4,7 @@ from setuptools import setup
setup( setup(
name="cyan", name="cyan",
version="1.1.6", version="1.2.2",
description="finally, pyzule doesn't suck", description="finally, pyzule doesn't suck",
author="zx", author="zx",
author_email="zx@hrzn.email", author_email="zx@hrzn.email",