feat, refactor: entitlement merging, MainExecutable

This commit is contained in:
zx
2024-09-18 21:39:02 -04:00
parent 2d16015c78
commit c1c0b2294e
7 changed files with 153 additions and 104 deletions

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",

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: # `validate_inputs()` made it a dict
app.executable.merge_entitlements(args.x, tmpdir)
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,124 @@
import os
import shutil
import plistlib
import subprocess
from typing import Any
from plistlib import dump as pdump
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: dict[str, Any], tmpdir: str
) -> None:
ENT_PATH = f"{tmpdir}/new.entitlements"
existing: dict[str, Any]
if self.write_entitlements(ENT_PATH): # has entitlements
with open(ENT_PATH, "rb") as f:
existing = plistlib.load(f)
else:
existing = {}
new = existing | entitlements
with open(ENT_PATH, "wb") as f2:
pdump(new, f2)
self.sign_with_entitlements(ENT_PATH)
print("[*] modified entitlement keys:", ", ".join(entitlements))
def sign_with_entitlements(self, entitlements: str) -> bool:
return subprocess.run(
[self.ldid, f"-S{entitlements}", 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:
args.x = 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"