mirror of
https://github.com/NohamR/pyzule-rw.git
synced 2026-10-11 02:49:40 +00:00
249 lines
7.3 KiB
Python
249 lines
7.3 KiB
Python
import os
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
try:
|
|
import lief
|
|
except Exception:
|
|
pass
|
|
|
|
from cyan import tbhutils
|
|
|
|
|
|
class Executable:
|
|
install_dir, specific = tbhutils.get_tools_dir()
|
|
nt = f"{specific}/install_name_tool"
|
|
ldid = f"{specific}/ldid"
|
|
lipo = f"{specific}/lipo"
|
|
otool = f"{specific}/otool"
|
|
idylib = f"{specific}/insert_dylib"
|
|
|
|
starters = ("\t/Library/", "\t@rpath", "\t@executable_path")
|
|
common = {
|
|
# substrate could show up as
|
|
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
|
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
|
|
|
"CydiaSubstrate.framework": "CydiaSubstrate.framework",
|
|
"Orion.framework": "Orion.framework",
|
|
"Cephei.framework": "Cephei.framework",
|
|
"CepheiUI.framework": "CepheiUI.framework",
|
|
"CepheiPrefs.framework": "CepheiPrefs.framework"
|
|
}
|
|
|
|
def __init__(self, path: str, bundle_path: Optional[str] = None):
|
|
if not os.path.isfile(path):
|
|
print(f"[!] {path} does not exist (executable)", file=sys.stderr)
|
|
sys.exit(
|
|
"[?] check the wiki for info: "
|
|
"https://github.com/asdfzxcvbn/pyzule-rw/wiki/"
|
|
"file-does-not-exist-(executable)-%3F"
|
|
)
|
|
|
|
self.path = path
|
|
self.bundle_path = bundle_path
|
|
|
|
self.bn = os.path.basename(path)
|
|
self.inj: Optional = None # type: ignore
|
|
|
|
if os.path.isfile(self.idylib):
|
|
self.inj_func = self.idyl_inject
|
|
else:
|
|
self.inj_func = self.lief_inj
|
|
|
|
def is_encrypted(self) -> bool:
|
|
proc = subprocess.run(
|
|
[self.otool, "-l", self.path],
|
|
capture_output=True
|
|
)
|
|
|
|
return b"cryptid 1" in proc.stdout
|
|
|
|
def inject(self, tweaks: dict[str, str], tmpdir: str) -> None:
|
|
# we only inject into the main executable
|
|
assert self.bundle_path is not None
|
|
|
|
has_entitlements = False
|
|
ENT_PATH = f"{self.bundle_path}/cyan.entitlements"
|
|
PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
|
|
FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
|
|
|
|
with open(ENT_PATH, "wb") as entf:
|
|
proc = subprocess.run(
|
|
[self.ldid, "-e", self.path],
|
|
capture_output=True
|
|
)
|
|
|
|
entf.write(proc.stdout)
|
|
|
|
if os.path.getsize(ENT_PATH) > 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:
|
|
return subprocess.run(
|
|
[self.lipo, "-thin", "arm64", self.path, "-output", self.path],
|
|
stderr=subprocess.DEVNULL
|
|
).returncode == 0
|
|
|
|
def change_dependency(self, old: str, new: str) -> None:
|
|
subprocess.run(
|
|
[self.nt, "-change", old, new, self.path],
|
|
stderr=subprocess.DEVNULL
|
|
)
|
|
|
|
def lief_inj(self, cmd: str) -> None:
|
|
if self.inj is None: # type: ignore
|
|
try:
|
|
lief.logging.disable() # type: ignore
|
|
except Exception:
|
|
sys.exit("[!] did you forget to install lief?")
|
|
|
|
self.inj = lief.parse(self.path) # type: ignore
|
|
|
|
try:
|
|
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore
|
|
except AttributeError:
|
|
sys.exit("[!] couldn't add LC (lief), did you use a valid app?")
|
|
|
|
def idyl_inject(self, cmd: str) -> None:
|
|
proc = subprocess.run(
|
|
[
|
|
self.idylib, "--weak", "--inplace", "--strip-codesig", "--all-yes",
|
|
cmd, self.path
|
|
], capture_output=True, text=True
|
|
)
|
|
|
|
if proc.returncode != 0:
|
|
sys.exit(f"[!] couldn't add LC (insert_dylib), error:\n{proc.stderr}")
|
|
|
|
def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
|
|
self.remove_signature()
|
|
|
|
for dep in self.get_dependencies():
|
|
for cname in (tweaks | self.common):
|
|
if cname in dep:
|
|
# i wonder if there's a better way to do this?
|
|
if cname.endswith(".framework"):
|
|
npath = f"@rpath/{cname}/{cname[:-10]}"
|
|
else:
|
|
npath = f"@rpath/{cname}"
|
|
|
|
if dep != npath:
|
|
self.change_dependency(dep, npath)
|
|
print(f"[*] fixed dependency in {self.bn}: {dep} -> {npath}")
|
|
|
|
if cname in self.common:
|
|
need.add(cname)
|
|
|
|
def get_dependencies(self) -> list[str]:
|
|
proc = subprocess.run(
|
|
[self.otool, "-L", self.path],
|
|
capture_output=True, text=True
|
|
)
|
|
|
|
# split at [2:] to avoid otool's line and dylib's id
|
|
tmp = proc.stdout.strip().split("\n")[2:]
|
|
for ind, dep in enumerate(tmp):
|
|
if "(architecture " in dep: # avoid checking duplicate deps
|
|
tmp = tmp[:ind]
|
|
|
|
deps: list[str] = []
|
|
for dep in tmp:
|
|
if any(dep.startswith(s) for s in self.starters):
|
|
deps.append(dep.split()[0]) # split() removes whitespace
|
|
|
|
return deps
|
|
|