mirror of
https://github.com/NohamR/pyzule-rw.git
synced 2026-10-10 18:39:41 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04a40f2aa6 | ||
|
|
6845281446 | ||
|
|
54da4485a5 | ||
|
|
f7b0169ee9 | ||
|
|
ffbff5c78d | ||
|
|
eba32c22e6 | ||
|
|
a96632f1b0 | ||
|
|
86e45cc186 | ||
|
|
e09351287b | ||
|
|
403e5d65a5 | ||
|
|
354a1d9ac5 | ||
|
|
73d1495eff | ||
|
|
31169d6c65 | ||
|
|
0808fd8c1b |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@ __pycache__/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
*.cyan
|
||||
.DS_Store
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
a rewrite of [pyzule](https://github.com/asdfzxcvbn/pyzule) that doesn't (completely) suck !!
|
||||
|
||||
wouldn't a go rewrite be really cool? (or in rust or something, adding features to this makes me realize PYTHONM FUCKING SUCKS SOMEONE PLEASE REWRITE IN LIKE ANY COMPILED AND STATICALLY TYPED LANGUAGE PLEASE!!!!!!!
|
||||
|
||||
## features
|
||||
|
||||
you can open an issue to request a feature :D !! also see my [recommended flags](https://github.com/asdfzxcvbn/pyzule-rw/wiki/recommended-flags)
|
||||
@@ -15,6 +17,7 @@ you can open an issue to request a feature :D !! also see my [recommended flags]
|
||||
- remove watch app
|
||||
- change the app icon
|
||||
- fakesign the output ipa/tipa/app
|
||||
- merge a plist into the app's existing Info.plist
|
||||
- add custom entitlements to the main executable
|
||||
- thin all binaries to arm64, it can LARGELY reduce app size sometimes!
|
||||
- remove all app extensions (or just encrypted ones!)
|
||||
|
||||
@@ -42,6 +42,14 @@ def main() -> None:
|
||||
"-k", metavar="icon",
|
||||
help="modify the app's icon"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", metavar="plist",
|
||||
help="a plist to merge with the app's Info.plist"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x", metavar="entitlements",
|
||||
help="add or modify entitlements to the main binary"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-u", "--remove-supported-devices", action="store_true",
|
||||
@@ -83,6 +91,10 @@ def generate_cyan(parser: argparse.ArgumentParser) -> None:
|
||||
parser.error(f"invalid minimum OS version: {args.m}")
|
||||
if args.k is not None and not os.path.isfile(args.k):
|
||||
parser.error(f"{args.k} does not exist")
|
||||
if args.l is not None and not os.path.isfile(args.l):
|
||||
parser.error(f"{args.l} does not exist")
|
||||
if args.x is not None and not os.path.isfile(args.x):
|
||||
parser.error(f"{args.x} does not exist")
|
||||
if args.f is not None:
|
||||
fake = [f for f in args.f if not os.path.exists(f)]
|
||||
|
||||
@@ -107,7 +119,7 @@ def generate_cyan(parser: argparse.ArgumentParser) -> None:
|
||||
real_args = {k: v for k, v in dict(vars(args)).items() if v}
|
||||
del real_args["output"]
|
||||
|
||||
for key in "fk": # these need files
|
||||
for key in "fkxl": # these need files
|
||||
if key in real_args:
|
||||
real_args[key] = True
|
||||
|
||||
@@ -136,6 +148,12 @@ def generate_cyan(parser: argparse.ArgumentParser) -> None:
|
||||
if args.k is not None:
|
||||
zf.write(args.k, "icon.idk")
|
||||
|
||||
if args.l:
|
||||
zf.write(args.l, "merge.plist")
|
||||
|
||||
if args.x:
|
||||
zf.write(args.x, "new.entitlements")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -21,15 +21,16 @@ def main() -> None:
|
||||
"-o", "--output", metavar="output",
|
||||
help="if unspecified, overwrites input"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-z", "--cyan", metavar="cyan",
|
||||
help="the .cyan file to use"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-z", "--cyan", metavar="cyan", nargs="+",
|
||||
help="the .cyan file(s) to use"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f", metavar="file", nargs="+",
|
||||
help="a tweak to inject/item to be added to the bundle"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-n", metavar="name",
|
||||
help="modify the app's name"
|
||||
@@ -50,6 +51,10 @@ def main() -> None:
|
||||
"-k", metavar="icon",
|
||||
help="modify the app's icon"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", metavar="plist",
|
||||
help="a plist to merge with the app's Info.plist"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x", metavar="entitlements",
|
||||
help="add or modify entitlements to the main binary"
|
||||
@@ -99,7 +104,7 @@ def main() -> None:
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version", action="version", version="cyan v1.3"
|
||||
"--version", action="version", version="cyan v1.4.4"
|
||||
)
|
||||
|
||||
from cyan import logic
|
||||
|
||||
@@ -45,7 +45,7 @@ def main(parser: ArgumentParser) -> None:
|
||||
|
||||
if args.cyan is not None:
|
||||
changing = vars(args)
|
||||
tbhutils.parse_cyan(changing, tmpdir)
|
||||
tbhutils.parse_cyans(changing, tmpdir)
|
||||
|
||||
# this goes before injection,
|
||||
# since user might inject their own extensions
|
||||
@@ -66,6 +66,8 @@ def main(parser: ArgumentParser) -> None:
|
||||
app.plist.change_minimum_version(args.m)
|
||||
if args.k is not None:
|
||||
app.change_icon(args.k, tmpdir)
|
||||
if args.l is not None:
|
||||
app.plist.merge_plist(args.l)
|
||||
if args.x is not None:
|
||||
app.executable.merge_entitlements(args.x)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class AppBundle:
|
||||
|
||||
def change_icon(self, path: str, tmpdir: str) -> None:
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import Image # type: ignore
|
||||
except Exception:
|
||||
return print("[?] pillow is not installed, -k is not available")
|
||||
|
||||
|
||||
@@ -13,17 +13,33 @@ class Executable:
|
||||
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.
|
||||
# adding /usr/lib/ now, idk why i didnt before. lets hope nothing breaks
|
||||
starters = ("\t/Library/", "\t/usr/lib/", "\t@rpath", "\t@executable_path")
|
||||
|
||||
"CydiaSubstrate.framework": "CydiaSubstrate.framework",
|
||||
"Orion.framework": "Orion.framework",
|
||||
"Cephei.framework": "Cephei.framework",
|
||||
"CepheiUI.framework": "CepheiUI.framework",
|
||||
"CepheiPrefs.framework": "CepheiPrefs.framework"
|
||||
# substrate could show up as
|
||||
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
||||
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
||||
common = {
|
||||
"substrate.": {
|
||||
"name": "CydiaSubstrate.framework",
|
||||
"path": "@rpath/CydiaSubstrate.framework/CydiaSubstrate"
|
||||
},
|
||||
"orion.": {
|
||||
"name": "Orion.framework",
|
||||
"path": "@rpath/Orion.framework/Orion"
|
||||
},
|
||||
"cephei.": {
|
||||
"name": "Cephei.framework",
|
||||
"path": "@rpath/Cephei.framework/Cephei"
|
||||
},
|
||||
"cepheiui.": {
|
||||
"name": "CepheiUI.framework",
|
||||
"path": "@rpath/CepheiUI.framework/CepheiUI"
|
||||
},
|
||||
"cepheiprefs.": {
|
||||
"name": "CepheiPrefs.framework",
|
||||
"path": "@rpath/CepheiPrefs.framework/CepheiPrefs"
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, path: str):
|
||||
@@ -64,14 +80,29 @@ class Executable:
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
|
||||
def fix_common_dependencies(self, needed: set[str]) -> None:
|
||||
self.remove_signature()
|
||||
|
||||
for dep in self.get_dependencies():
|
||||
for cname in (tweaks | self.common):
|
||||
for common, info in self.common.items():
|
||||
if common in dep.lower():
|
||||
needed.add(common)
|
||||
|
||||
if dep != info["path"]:
|
||||
self.change_dependency(dep, info["path"])
|
||||
print(
|
||||
f"[*] fixed common dependency in {self.bn}: "
|
||||
f"{dep} -> {info['path']}"
|
||||
)
|
||||
|
||||
def fix_dependencies(self, tweaks: dict[str, str]) -> None:
|
||||
for dep in self.get_dependencies():
|
||||
for cname in tweaks:
|
||||
if cname in dep:
|
||||
# i wonder if there's a better way to do this?
|
||||
if cname.endswith(".framework"):
|
||||
# nah, not gonna parse the plist,
|
||||
# i've never seen a framework with a "mismatched" name
|
||||
npath = f"@rpath/{cname}/{cname[:-10]}"
|
||||
else:
|
||||
npath = f"@rpath/{cname}"
|
||||
@@ -80,9 +111,6 @@ class Executable:
|
||||
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],
|
||||
|
||||
@@ -5,7 +5,7 @@ import subprocess
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import lief
|
||||
import lief # type: ignore
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -58,14 +58,22 @@ class MainExecutable(Executable):
|
||||
os.chdir(cwd) # i fucking hate jailbroken iOS utils.
|
||||
|
||||
needed: set[str] = set()
|
||||
|
||||
# inject/fix user things
|
||||
for bn, path in tweaks.items():
|
||||
if os.path.islink(path):
|
||||
continue # symlinks can potentially have some security implications
|
||||
|
||||
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)
|
||||
|
||||
e = Executable(path)
|
||||
e.fix_common_dependencies(needed)
|
||||
e.fix_dependencies(tweaks)
|
||||
|
||||
fpath = f"{FRAMEWORKS_DIR}/{bn}"
|
||||
existed = tbhutils.delete_if_exists(fpath, bn)
|
||||
@@ -89,11 +97,13 @@ class MainExecutable(Executable):
|
||||
|
||||
# orion has a *weak* dependency to substrate,
|
||||
# but will still crash without it. nice !!!!!!!!!!!
|
||||
if "Orion.framework" in needed:
|
||||
needed.add("CydiaSubstrate.framework")
|
||||
## edit: actually, maybe this is in case someone uses Internal backend?
|
||||
## someone test it pls!!!
|
||||
if "orion." in needed:
|
||||
needed.add("substrate.")
|
||||
|
||||
for missing in needed:
|
||||
real = self.common[missing] # "real" name, thanks substrate!
|
||||
real = self.common[missing]["name"] # e.g. "Orion.framework"
|
||||
ip = f"{FRAMEWORKS_DIR}/{real}"
|
||||
existed = tbhutils.delete_if_exists(ip, real)
|
||||
shutil.copytree(f"{self.install_dir}/extras/{real}", ip)
|
||||
|
||||
@@ -10,11 +10,13 @@ class Plist:
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
self.data: dict[str, Any] = plistlib.load(f)
|
||||
|
||||
self.success = True
|
||||
except Exception:
|
||||
if throw:
|
||||
sys.exit(f"[!] couldn't read {path}")
|
||||
else:
|
||||
return None
|
||||
|
||||
self.success = False
|
||||
|
||||
self.path = path
|
||||
self.app_path = app_path
|
||||
@@ -44,7 +46,7 @@ class Plist:
|
||||
try:
|
||||
if all(self[key] == val for key in keys):
|
||||
return False
|
||||
raise KeyError # lets pretend this was always here..
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
for key in keys:
|
||||
self[key] = val
|
||||
@@ -121,3 +123,18 @@ class Plist:
|
||||
else:
|
||||
print(f"[?] minimum version was already \"{minimum}\"")
|
||||
|
||||
def merge_plist(self, path: str) -> None:
|
||||
pl = Plist(path, throw=False)
|
||||
if not pl.success:
|
||||
return print(f"[!] couldn't parse {path}")
|
||||
|
||||
changed = False
|
||||
for k, v in pl.data.items():
|
||||
if self.change(v, k):
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
print("[?] no modified plist entries")
|
||||
else:
|
||||
print("[*] set plist keys:", ", ".join(pl.data))
|
||||
|
||||
|
||||
@@ -67,8 +67,13 @@ def validate_inputs(args: Namespace) -> Optional[str]:
|
||||
if args.k is not None and not os.path.isfile(args.k):
|
||||
sys.exit(f"[!] {args.k} does not exist")
|
||||
|
||||
if args.cyan is not None and not os.path.isfile(args.cyan):
|
||||
sys.exit(f"[!] {args.cyan} does not exist")
|
||||
if args.l is not None and not os.path.isfile(args.l):
|
||||
sys.exit(f"[!] {args.l} does not exist")
|
||||
|
||||
if args.cyan is not None:
|
||||
for cyan in args.cyan:
|
||||
if not os.path.isfile(cyan):
|
||||
sys.exit(f"[!] {cyan} does not exist")
|
||||
|
||||
if args.x is not None:
|
||||
if not os.path.isfile(args.x):
|
||||
@@ -186,7 +191,7 @@ def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
|
||||
glob(f"{t2}/**/*.framework", recursive=True)
|
||||
), []): # type: ignore
|
||||
if (
|
||||
os.path.islink(hi) # symlinks are broken iirc
|
||||
os.path.islink(hi) # symlinks are broken iirc, also for security
|
||||
or hi.count(".bundle") > 1 # prevent sub-bundle detection (rip)
|
||||
or hi.count(".framework") > 1
|
||||
):
|
||||
@@ -229,28 +234,37 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
|
||||
print(f"[?] was unable to zip {weird} file(s) due to timestamps")
|
||||
|
||||
|
||||
def parse_cyan(args: dict[str, Any], tmpdir: str) -> None:
|
||||
print("[*] parsing .cyan file..")
|
||||
with zipfile.ZipFile(args["cyan"]) as zf:
|
||||
DOT_PATH = f"{tmpdir}/cyan"
|
||||
os.mkdir(DOT_PATH)
|
||||
def parse_cyans(args: dict[str, Any], tmpdir: str) -> None:
|
||||
for ind, cyan in enumerate(args["cyan"]):
|
||||
print(f"[*] parsing {os.path.basename(cyan)} ..")
|
||||
|
||||
with zf.open("config.json") as f:
|
||||
config = json.load(f)
|
||||
with zipfile.ZipFile(cyan) as zf:
|
||||
DOT_PATH = f"{tmpdir}/cyan-{ind}"
|
||||
os.mkdir(DOT_PATH)
|
||||
|
||||
if "f" in config:
|
||||
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
|
||||
zf.extractall(DOT_PATH, NAMES)
|
||||
with zf.open("config.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# ensure not None
|
||||
args["f"] = args["f"] if args["f"] is not None else {}
|
||||
for e in os.scandir(f"{DOT_PATH}/inject"):
|
||||
args["f"][e.name] = e.path
|
||||
del config["f"]
|
||||
if "k" in config:
|
||||
args["k"] = zf.extract("icon.idk", DOT_PATH)
|
||||
del config["k"]
|
||||
if "f" in config:
|
||||
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
|
||||
zf.extractall(DOT_PATH, NAMES)
|
||||
|
||||
for k, v in config.items():
|
||||
args[k] = v
|
||||
# ensure not None
|
||||
args["f"] = args["f"] if args["f"] is not None else {}
|
||||
for e in os.scandir(f"{DOT_PATH}/inject"):
|
||||
args["f"][e.name] = e.path
|
||||
del config["f"]
|
||||
if "k" in config:
|
||||
args["k"] = zf.extract("icon.idk", DOT_PATH)
|
||||
del config["k"]
|
||||
if "l" in config:
|
||||
args["l"] = zf.extract("merge.plist", DOT_PATH)
|
||||
del config["l"]
|
||||
if "x" in config:
|
||||
args["x"] = zf.extract("new.entitlements", DOT_PATH)
|
||||
del config["x"]
|
||||
|
||||
# the rest of the config (not the ones above, we `del` them)
|
||||
for k, v in config.items():
|
||||
args[k] = v
|
||||
|
||||
|
||||
4
setup.py
4
setup.py
@@ -4,10 +4,10 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="cyan",
|
||||
version="1.3",
|
||||
version="1.4.4",
|
||||
description="finally, pyzule doesn't suck",
|
||||
author="zx",
|
||||
author_email="zx@hrzn.email",
|
||||
author_email="z@zxcvbn.fyi",
|
||||
packages=["cyan", "cyan.tbhtypes", "cgen"],
|
||||
python_requires=">=3.9",
|
||||
include_package_data=True,
|
||||
|
||||
Reference in New Issue
Block a user