30 Commits
v1.1.6 ... dev

Author SHA1 Message Date
zx
740d3716dc chore: start satisfying Designated Requirement
the ipas are now more demure

this shouldnt break nor add anything i mean
2025-03-18 19:00:40 -04:00
zx
5ce7a0cb43 fix: unchecked @loader_path dependency
fuck this
2025-02-17 08:22:13 -05:00
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
zx
73d1495eff chore: bump version to v1.4 (stable) 2024-11-02 10:43:45 -04:00
zx
31169d6c65 refactor: move orion check out of loop 2024-11-02 10:42:47 -04:00
zx
0808fd8c1b feat: manually modified dylib support (#6) 2024-10-31 14:35:22 -04:00
zx
8c873a84c8 chore: bump version to v1.3 2024-10-28 17:31:19 -04:00
zx
7543bb3d8d fix: can't extract entitlements with codesign (ProcursusTeam/ldid#45)
this issue was originally reported in #8
2024-10-28 17:27:02 -04:00
zx
c0b0324a12 chore: move injection functions to MainExecutable 2024-10-25 23:04:40 -04:00
zx
42517c90fb chore: move write_entitlements() to MainExecutable
the func is only used in `MainExecutable`, so we should move it there to make it a bit more "clean"
2024-10-25 22:44:25 -04:00
zx
b4e3e6aaf5 chore: add question issue template 2024-10-15 12:14:53 -04:00
zx
b60788d167 chore: bump version to v1.2.3 2024-10-14 21:09:10 -04:00
zx
5f37ab3f23 chore: add support for .tipa 2024-10-14 21:08:34 -04:00
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
16 changed files with 383 additions and 212 deletions

22
.github/ISSUE_TEMPLATE/question.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
# thanks to uYouEnhanced for the issue template:
# https://github.com/arichornlover/uYouEnhanced/blob/main/.github/ISSUE_TEMPLATE/bug.yaml?plain=1
name: question
description: have a question about cyan or its code? use this template!
title: "[question] "
labels: question
body:
- type: checkboxes
attributes:
label: have you searched the existing issues?
options:
- label: this is a unique question. i agree that if this isn't a unique question, i'll be BLOCKED from the cyan repo
required: true
- type: textarea
attributes:
label: what's up?
description: what's the question?
validations:
required: true

8
.gitignore vendored
View File

@@ -1,5 +1,11 @@
__pycache__/ __pycache__/
build/ build/
*.egg-info *.egg-info/
*.cyan
.DS_Store
*.ipa
*.app/

View File

@@ -2,6 +2,8 @@
a rewrite of [pyzule](https://github.com/asdfzxcvbn/pyzule) that doesn't (completely) suck !! 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 ## 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) you can open an issue to request a feature :D !! also see my [recommended flags](https://github.com/asdfzxcvbn/pyzule-rw/wiki/recommended-flags)
@@ -14,7 +16,9 @@ you can open an issue to request a feature :D !! also see my [recommended flags]
- remove UISupportedDevices - remove UISupportedDevices
- remove watch app - remove watch app
- change the app icon - change the app icon
- fakesign the output ipa/app - 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! - 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!)
@@ -47,8 +51,6 @@ the `zip` and `unzip` commands are *optional* dependencies, they may [fix issues
</ol> </ol>
</details> </details>
note: if you installed cyan before v1.1.3 using `pip`, make sure you `pip uninstall cyan`, then verify you have the latest version with `cyan --version`
## making cyan files ## making cyan files
cyan comes bundled with the `cgen` command, which lets you generate `.cyan` files to pass to `-z`/`--cyan` ! cyan comes bundled with the `cgen` command, which lets you generate `.cyan` files to pass to `-z`/`--cyan` !

View File

@@ -42,6 +42,14 @@ def main() -> None:
"-k", metavar="icon", "-k", metavar="icon",
help="modify the app's 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( parser.add_argument(
"-u", "--remove-supported-devices", action="store_true", "-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}") parser.error(f"invalid minimum OS version: {args.m}")
if args.k is not None and not os.path.isfile(args.k): if args.k is not None and not os.path.isfile(args.k):
parser.error(f"{args.k} does not exist") 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: if args.f is not None:
fake = [f for f in args.f if not os.path.exists(f)] 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} real_args = {k: v for k, v in dict(vars(args)).items() if v}
del real_args["output"] del real_args["output"]
for key in "fk": # these need files for key in "fkxl": # these need files
if key in real_args: if key in real_args:
real_args[key] = True real_args[key] = True
@@ -136,6 +148,12 @@ def generate_cyan(parser: argparse.ArgumentParser) -> None:
if args.k is not None: if args.k is not None:
zf.write(args.k, "icon.idk") 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__": if __name__ == "__main__":
main() main()

View File

@@ -21,15 +21,16 @@ def main() -> None:
"-o", "--output", metavar="output", "-o", "--output", metavar="output",
help="if unspecified, overwrites input" 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( parser.add_argument(
"-f", metavar="file", nargs="+", "-f", metavar="file", nargs="+",
help="a tweak to inject/item to be added to the bundle" help="a tweak to inject/item to be added to the bundle"
) )
parser.add_argument( parser.add_argument(
"-n", metavar="name", "-n", metavar="name",
help="modify the app's name" help="modify the app's name"
@@ -50,6 +51,14 @@ def main() -> None:
"-k", metavar="icon", "-k", metavar="icon",
help="modify the app's 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( parser.add_argument(
"-u", "--remove-supported-devices", action="store_true", "-u", "--remove-supported-devices", action="store_true",
@@ -95,7 +104,7 @@ def main() -> None:
) )
parser.add_argument( parser.add_argument(
"--version", action="version", version="cyan v1.1.6" "--version", action="version", version="cyan v1.4.4"
) )
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.

Binary file not shown.

View File

@@ -13,8 +13,12 @@ def main(parser: ArgumentParser) -> None:
if args.output is not None: if args.output is not None:
args.o = os.path.normpath(args.output) args.o = os.path.normpath(args.output)
if not (args.o.endswith(".app") or args.o.endswith(".ipa")): if not (
print("[?] output's file extension not specified; will create ipa") args.o.endswith(".app")
or args.o.endswith(".ipa")
or args.o.endswith(".tipa")
):
print("[?] valid file extension not found; will create ipa")
args.o += ".ipa" args.o += ".ipa"
else: else:
args.o = args.i args.o = args.i
@@ -25,8 +29,9 @@ def main(parser: ArgumentParser) -> None:
if arg_err is not None: if arg_err is not None:
parser.error(arg_err) parser.error(arg_err)
INPUT_IS_IPA = True if args.i.endswith(".ipa") else False # mfw when "True if True else False" HAHAHAH
OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False INPUT_IS_IPA = args.i.endswith(".ipa") or args.i.endswith(".tipa")
OUTPUT_IS_IPA = args.o.endswith(".ipa") or args.o.endswith(".tipa")
with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM(): with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM():
app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA) app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
@@ -40,7 +45,7 @@ def main(parser: ArgumentParser) -> None:
if args.cyan is not None: if args.cyan is not None:
changing = vars(args) changing = vars(args)
tbhutils.parse_cyan(changing, tmpdir) tbhutils.parse_cyans(changing, tmpdir)
# this goes before injection, # this goes before injection,
# since user might inject their own extensions # since user might inject their own extensions
@@ -61,6 +66,10 @@ 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.l is not None:
app.plist.merge_plist(args.l)
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
) )
@@ -104,7 +105,7 @@ class AppBundle:
def change_icon(self, path: str, tmpdir: str) -> None: def change_icon(self, path: str, tmpdir: str) -> None:
try: try:
from PIL import Image from PIL import Image # type: ignore
except Exception: except Exception:
return print("[?] pillow is not installed, -k is not available") return print("[?] pillow is not installed, -k is not available")

View File

@@ -1,13 +1,6 @@
import os import os
import sys import sys
import shutil
import subprocess import subprocess
from typing import Optional
try:
import lief
except Exception:
pass
from cyan import tbhutils from cyan import tbhutils
@@ -20,20 +13,38 @@ class Executable:
otool = f"{specific}/otool" otool = f"{specific}/otool"
idylib = f"{specific}/insert_dylib" 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
common = { ## LITERALLY 2 DAYS LATER. WHAT THE FUCK IS @LOADER_PATH HELP
## i will cry if only checking for '@' will break this.
starters = ("\t/Library/", "\t/usr/lib/", "\t@")
# substrate could show up as # substrate could show up as
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib # CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY. # AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
common = {
"CydiaSubstrate.framework": "CydiaSubstrate.framework", "substrate.": {
"Orion.framework": "Orion.framework", "name": "CydiaSubstrate.framework",
"Cephei.framework": "Cephei.framework", "path": "@rpath/CydiaSubstrate.framework/CydiaSubstrate"
"CepheiUI.framework": "CepheiUI.framework", },
"CepheiPrefs.framework": "CepheiPrefs.framework" "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, 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,15 +54,7 @@ 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
if os.path.isfile(self.idylib):
self.inj_func = self.idyl_inject
else:
self.inj_func = self.lief_inj
def is_encrypted(self) -> bool: def is_encrypted(self) -> bool:
proc = subprocess.run( proc = subprocess.run(
@@ -61,115 +64,11 @@ 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:
# 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: def remove_signature(self) -> None:
subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL) subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL)
def fakesign(self, keep_entitlements: bool = True) -> bool: def fakesign(self) -> bool:
cmd = [self.ldid, "-S"] return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0
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(
@@ -183,39 +82,29 @@ class Executable:
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
def lief_inj(self, cmd: str) -> None: def fix_common_dependencies(self, needed: set[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() self.remove_signature()
for dep in self.get_dependencies(): 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: if cname in dep:
# i wonder if there's a better way to do this? # i wonder if there's a better way to do this?
if cname.endswith(".framework"): 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]}" npath = f"@rpath/{cname}/{cname[:-10]}"
else: else:
npath = f"@rpath/{cname}" npath = f"@rpath/{cname}"
@@ -224,9 +113,6 @@ class Executable:
self.change_dependency(dep, npath) self.change_dependency(dep, npath)
print(f"[*] fixed dependency in {self.bn}: {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]: def get_dependencies(self) -> list[str]:
proc = subprocess.run( proc = subprocess.run(
[self.otool, "-L", self.path], [self.otool, "-L", self.path],

View File

@@ -0,0 +1,171 @@
import os
import sys
import shutil
import subprocess
from typing import Optional
try:
import lief # type: ignore
except Exception:
pass
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
self.inj: Optional = None # type: ignore
if os.path.isfile(self.idylib):
self.inj_func = self.idyl_inject
else:
self.inj_func = self.lief_inject
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 is 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()
# 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)
e = Executable(path)
e.fix_common_dependencies(needed)
e.fix_dependencies(tweaks)
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 !!!!!!!!!!!
## 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]["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)
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 write_entitlements(self, output: str) -> bool:
with open(output, "wb") as entf:
proc = subprocess.run(
[self.ldid, "-e", self.path],
capture_output=True
)
entf.write(proc.stdout)
return os.path.getsize(output) > 0
def merge_entitlements(self, entitlements: str) -> None:
if self.sign_with_entitlements(entitlements):
print("[*] merged new entitlements")
else:
print("[!] failed to merge new entitlements, are they valid?")
def sign_with_entitlements(self, entitlements: str) -> bool:
return subprocess.run([
self.ldid,
f"-S{entitlements}", "-M", "-Cadhoc",
f"-Q{self.install_dir}/extras/zero.requirements",
self.path
]).returncode == 0
def lief_inject(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", "--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}")

View File

@@ -10,11 +10,13 @@ class Plist:
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
self.data: dict[str, Any] = plistlib.load(f) self.data: dict[str, Any] = plistlib.load(f)
self.success = True
except Exception: except Exception:
if throw: if throw:
sys.exit(f"[!] couldn't read {path}") sys.exit(f"[!] couldn't read {path}")
else:
return None self.success = False
self.path = path self.path = path
self.app_path = app_path self.app_path = app_path
@@ -44,7 +46,7 @@ class Plist:
try: try:
if all(self[key] == val for key in keys): if all(self[key] == val for key in keys):
return False return False
raise KeyError # lets pretend this was always here.. raise KeyError
except KeyError: except KeyError:
for key in keys: for key in keys:
self[key] = val self[key] = val
@@ -121,3 +123,18 @@ class Plist:
else: else:
print(f"[?] minimum version was already \"{minimum}\"") 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

@@ -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
@@ -16,10 +17,11 @@ HAS_UNZIP = shutil.which("unzip") is not None
def validate_inputs(args: Namespace) -> Optional[str]: def validate_inputs(args: Namespace) -> Optional[str]:
if not ( if not (
args.i.endswith(".ipa") args.i.endswith(".app")
or args.i.endswith(".app") or args.i.endswith(".ipa")
or args.i.endswith(".tipa")
): ):
return "the input file must be an ipa/app" return "the input file must be an ipa/tipa/app"
if not os.path.exists(args.i): if not os.path.exists(args.i):
return f"{args.i} does not exist" return f"{args.i} does not exist"
@@ -65,8 +67,23 @@ def validate_inputs(args: Namespace) -> Optional[str]:
if args.k is not None and not os.path.isfile(args.k): if args.k is not None and not os.path.isfile(args.k):
sys.exit(f"[!] {args.k} does not exist") sys.exit(f"[!] {args.k} does not exist")
if args.cyan is not None and not os.path.isfile(args.cyan): if args.l is not None and not os.path.isfile(args.l):
sys.exit(f"[!] {args.cyan} does not exist") 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):
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:
@@ -174,7 +191,7 @@ def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
glob(f"{t2}/**/*.framework", recursive=True) glob(f"{t2}/**/*.framework", recursive=True)
), []): # type: ignore ), []): # type: ignore
if ( 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(".bundle") > 1 # prevent sub-bundle detection (rip)
or hi.count(".framework") > 1 or hi.count(".framework") > 1
): ):
@@ -197,8 +214,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:
@@ -215,10 +234,12 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
print(f"[?] was unable to zip {weird} file(s) due to timestamps") print(f"[?] was unable to zip {weird} file(s) due to timestamps")
def parse_cyan(args: dict[str, Any], tmpdir: str) -> None: def parse_cyans(args: dict[str, Any], tmpdir: str) -> None:
print("[*] parsing .cyan file..") for ind, cyan in enumerate(args["cyan"]):
with zipfile.ZipFile(args["cyan"]) as zf: print(f"[*] parsing {os.path.basename(cyan)} ..")
DOT_PATH = f"{tmpdir}/cyan"
with zipfile.ZipFile(cyan) as zf:
DOT_PATH = f"{tmpdir}/cyan-{ind}"
os.mkdir(DOT_PATH) os.mkdir(DOT_PATH)
with zf.open("config.json") as f: with zf.open("config.json") as f:
@@ -236,7 +257,14 @@ def parse_cyan(args: dict[str, Any], tmpdir: str) -> None:
if "k" in config: if "k" in config:
args["k"] = zf.extract("icon.idk", DOT_PATH) args["k"] = zf.extract("icon.idk", DOT_PATH)
del config["k"] 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(): for k, v in config.items():
args[k] = v args[k] = v

View File

@@ -4,10 +4,10 @@ from setuptools import setup
setup( setup(
name="cyan", name="cyan",
version="1.1.6", version="1.4.4",
description="finally, pyzule doesn't suck", description="finally, pyzule doesn't suck",
author="zx", author="zx",
author_email="zx@hrzn.email", author_email="z@zxcvbn.fyi",
packages=["cyan", "cyan.tbhtypes", "cgen"], packages=["cyan", "cyan.tbhtypes", "cgen"],
python_requires=">=3.9", python_requires=">=3.9",
include_package_data=True, include_package_data=True,