11 Commits
v1.4 ... v1.4.4

Author SHA1 Message Date
zx
04a40f2aa6 chore: bump version to v1.4.4 2025-02-15 09:15:23 -05:00
zx
6845281446 fix: unchecked /usr/lib dependencies
idk why i didnt do this before. either im stupid or there was a reason so im just praying this doesnt break anything rn
2025-02-15 09:14:55 -05:00
zx
54da4485a5 chore: change contact email 2025-02-01 21:52:05 -05:00
zx
f7b0169ee9 chore: bump version to v1.4.3 2025-02-01 21:39:41 -05:00
zx
ffbff5c78d feat: use multiple cyans 2025-02-01 21:38:15 -05:00
zx
eba32c22e6 chore: bump version to v1.4.2 (stable) 2025-01-08 19:10:32 -05:00
zx
a96632f1b0 chore: ignore DS_Store
macos moment
2025-01-08 19:06:52 -05:00
zx
86e45cc186 feat(cgen): plist and entitlement merging 2024-11-26 22:21:13 -05:00
zx
e09351287b feat: plist merging 2024-11-26 22:07:22 -05:00
zx
403e5d65a5 chore: bump version to v1.4.1 2024-11-07 22:32:58 -05:00
zx
354a1d9ac5 fix: not auto-injecting already fixed dependency names
whoops
2024-11-07 22:31:43 -05:00
11 changed files with 114 additions and 45 deletions

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@ __pycache__/
build/
*.egg-info/
*.cyan
.DS_Store

View File

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

View File

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

View File

@@ -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.4"
"--version", action="version", version="cyan v1.4.4"
)
from cyan import logic

View File

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

View File

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

View File

@@ -13,7 +13,8 @@ class Executable:
otool = f"{specific}/otool"
idylib = f"{specific}/insert_dylib"
starters = ("\t/Library/", "\t@rpath", "\t@executable_path")
# adding /usr/lib/ now, idk why i didnt before. lets hope nothing breaks
starters = ("\t/Library/", "\t/usr/lib/", "\t@rpath", "\t@executable_path")
# substrate could show up as
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
@@ -84,15 +85,16 @@ class Executable:
for dep in self.get_dependencies():
for common, info in self.common.items():
if common in dep.lower() and dep != info["path"]:
self.change_dependency(dep, info["path"])
print(
f"[*] fixed common dependency in {self.bn}: "
f"{dep} -> {info['path']}"
)
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:

View File

@@ -5,7 +5,7 @@ import subprocess
from typing import Optional
try:
import lief
import lief # type: ignore
except Exception:
pass
@@ -61,6 +61,9 @@ class MainExecutable(Executable):
# 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)
@@ -94,6 +97,8 @@ class MainExecutable(Executable):
# orion has a *weak* dependency to substrate,
# but will still crash without it. nice !!!!!!!!!!!
## edit: actually, maybe this is in case someone uses Internal backend?
## someone test it pls!!!
if "orion." in needed:
needed.add("substrate.")

View File

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

View File

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

View File

@@ -4,10 +4,10 @@ from setuptools import setup
setup(
name="cyan",
version="1.4",
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,