18 Commits
v1.0 ... v1.1.4

Author SHA1 Message Date
zx
06a2ea8dad chore: bump version to v1.1.4 2024-09-13 18:28:35 -04:00
zx
55b2ec9d07 fix: folders ending in / 2024-09-13 18:27:45 -04:00
zx
52b3b11a1a chore: use insert_dylib on more platforms 2024-09-13 18:07:00 -04:00
zx
4375b7f880 docs: update install instructions 2024-09-11 17:33:40 -04:00
zx
8f7db1438a chore: bump version to v1.1.3 2024-09-11 07:15:47 -04:00
zx
5e08afc0ac fix(cgen): folders ending in / 2024-09-11 07:14:50 -04:00
zx
0bd6393e78 chore: bundle cgen 2024-09-10 17:35:42 -04:00
zx
f5a5d7b5ed fix: sub-bundle detection 2024-09-10 17:23:30 -04:00
zx
c3a51e7e03 docs: update install instructions 2024-09-10 16:30:51 -04:00
zx
806db413e2 fix: crashing when jailbroken
i realized azule actually removes the signature (instead of fakesigning) before specific operations, maybe this is the fix?
2024-09-10 07:11:15 -04:00
zx
927df65b55 Merge branch 'main' of github.com:asdfzxcvbn/pyzule-rw 2024-09-09 19:37:24 -04:00
zx
851eedf629 fix: using wrong ldid 2024-09-09 19:36:38 -04:00
zx
126d17924b docs: feature requests 2024-09-09 10:32:34 -04:00
zx
5ebab86a44 whoopsies 2024-09-08 22:32:43 -04:00
zx
c8197a8c47 feat: .cyan files 2024-09-08 22:31:26 -04:00
zx
1e9607d9e1 feat: change app icons 2024-09-08 19:29:59 -04:00
zx
30fa189197 fix: validate minimum version 2024-09-08 18:54:35 -04:00
zx
5c0363b5a9 feat: plist operations 2024-09-08 18:48:05 -04:00
12 changed files with 427 additions and 67 deletions

View File

@@ -1,49 +1,59 @@
# pyzule-rw / cyan # pyzule-rw / cyan
rewriting [pyzule](https://github.com/asdfzxcvbn/pyzule), but actually good this time! btw.. cyan/pyzule looks best with a monospace font !! :D a rewrite of [pyzule](https://github.com/asdfzxcvbn/pyzule) that doesn't (completely) suck !!
this is pretty much just a hobby project that i'll work on when i'm bored. pyzule in its current state is kinda ugly, but it works and i haven't really had the motivation to work on it. there really isn't any need for improvements, anyway..
`cyan` will ~~target python v3.12 (finally!)~~, be pep8-compliant (hopefully..), and be type-hinted!
## features ## features
more coming soon! i'm trying to copy pyzule in order to deprecate it in favor of cyan, which is 1000x better you can open an issue to request a feature :D !!
- generate and use shareable .cyan files to configure IPAs!
- inject deb, dylib, framework, bundle, and appex files/folders - inject deb, dylib, framework, bundle, and appex files/folders
- automatically fix dependencies on CydiaSubstrate **(cyan uses [ElleKit](https://github.com/evelyneee/ellekit/)!)**, Cephei*, and Orion - automatically fix dependencies on CydiaSubstrate **(cyan uses [ElleKit](https://github.com/evelyneee/ellekit/)!)**, Cephei*, and Orion
- copy any unknown file/folder types to app root - copy any unknown file/folder types to app root
- change app name, version, bundle id, and minimum os version
- remove UISupportedDevices - remove UISupportedDevices
- remove watch app - remove watch app
- change the app icon
- fakesign the output ipa/app - fakesign the output ipa/app
- 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!)
## install instructions ## install instructions
cyan works on linux, macOS, WSL, and jailbroken iOS! cyan supports **linux, macOS, WSL, and jailbroken iOS!** all either x86_64 or arm64/aarch64 !!
first, make sure you have [ar](https://command-not-found.com/ar) and [tar](https://command-not-found.com/tar) installed first, make sure you have [ar](https://command-not-found.com/ar) and [tar](https://command-not-found.com/tar) installed
also obviously install python, version 3.9 or greater is required (the version available on the procursus repo for iOS) also obviously install python, version 3.9 or greater is required
**if you want to inject dylibs AND ARE NOT ON iOS,** make sure you install lief (you only have to do this once): `pip install -U lief` <details>
<summary><b>linux/WSL/macOS instructions</b></summary>
<br/>
<ol>
<li>install <a href="https://github.com/pypa/pipx?tab=readme-ov-file#install-pipx">pipx</a></li>
<li>install OR update cyan: <code>pipx install --force https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip</code></li>
<li>add cyan to PATH: <code>pipx ensurepath</code></li>
<li><b>if you want to inject dylibs ON AARCH64 LINUX</b>: <code>pipx inject cyan lief</code></li>
<li><b>if you want to change app icons (iOS NOT supported)</b>: <code>pipx inject cyan Pillow</code></li>
</ol>
</details>
then finally, to install or update cyan, just `pip install --force-reinstall git+https://github.com/asdfzxcvbn/pyzule-rw.git#egg=cyan` <details>
<summary><b>jailbroken iOS instructions</b></summary>
<br/>
<ol>
<li>install OR update cyan: <code>pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip</code></li>
</ol>
</details>
## todo 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`
[x] refactor: dont prepare, just copy and fix as you go ## making cyan files
[2] feat: plist operations (-n, -v, -b, -m, -l, -r) cyan comes bundled with the `cgen` command, which lets you generate `.cyan` files to pass to `-z`/`--cyan` !
[x] feat: remove watch app ## acknowledgements
[x] feat: fakesign - [Al4ise](https://github.com/Al4ise) for the original [Azule](https://github.com/Al4ise/Azule)
- [lief-project](https://github.com/lief-project) for [LIEF](https://github.com/lief-project/LIEF)
[x] feat: thin binaries
[x] feat: plugin operations (-q, -e)
[] feat: .cyan files (lol rip .pyzule files)

142
cgen/__main__.py Normal file
View File

@@ -0,0 +1,142 @@
import os
import sys
import json
import zipfile
import argparse
def main() -> None:
if sys.platform == "win32":
sys.exit("[!] windows is not supported")
parser = argparse.ArgumentParser(
description="a tool to generate .cyan files"
)
parser.add_argument(
"-o", "--output", metavar="output", required=True,
help="output of the .cyan file"
)
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"
)
parser.add_argument(
"-v", metavar="version",
help="modify the app's version"
)
parser.add_argument(
"-b", metavar="bundle id",
help="modify the app's bundle id"
)
parser.add_argument(
"-m", metavar="minimum",
help="modify the app's minimum OS version"
)
parser.add_argument(
"-k", metavar="icon",
help="modify the app's icon"
)
parser.add_argument(
"-u", "--remove-supported-devices", action="store_true",
help="remove UISupportedDevices"
)
parser.add_argument(
"-w", "--no-watch", action="store_true",
help="remove all watch apps"
)
parser.add_argument(
"-d", "--enable-documents", action="store_true",
help="enable documents support"
)
parser.add_argument(
"-s", "--fakesign", action="store_true",
help="fakesign all binaries for use with appsync/trollstore"
)
parser.add_argument(
"-q", "--thin", action="store_true",
help="thin all binaries to arm64, may largely reduce size"
)
parser.add_argument(
"-e", "--remove-extensions", action="store_true",
help="remove all app extensions"
)
parser.add_argument(
"-g", "--remove-encrypted", action="store_true",
help="only remove encrypted app extensions"
)
generate_cyan(parser)
def generate_cyan(parser: argparse.ArgumentParser) -> None:
args = parser.parse_args()
# input validation
if args.m is not None and any(c not in "0123456789." for c in args.m):
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.f is not None:
fake = [f for f in args.f if not os.path.exists(f)]
# it would be great if everyone was using python 3.12 !!!
if len(fake) != 0:
parser.error(f"the following file(s) do not exist: {', '.join(fake)}")
if not args.output.endswith(".cyan"):
print("[*] appended cyan file extension to output")
args.output += ".cyan"
if os.path.isfile(args.output):
try:
overwrite = input(
f"[<] {args.output} already exists. overwrite? [Y/n] "
).lower().strip()
except KeyboardInterrupt:
sys.exit("\n[?] guess not")
if overwrite not in ("y", "yes", ""):
sys.exit("[>] quitting.")
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
if key in real_args:
real_args[key] = True
print("[*] generating..")
with zipfile.ZipFile(
args.output, "w", zipfile.ZIP_DEFLATED, compresslevel=1
) as zf:
with zf.open("config.json", "w") as f:
f.write(json.dumps(real_args).encode())
if args.f is not None:
for f in args.f:
if os.path.isfile(f):
zf.write(f, f"inject/{os.path.basename(f)}")
else: # G YHUJMNFTGYHNFTGYHTGYHUT6Y7UJM8RFTYHNR564TY
if f.endswith("/"):
f = f[:-1] # yes this is needed to prevent a bug wtf
for dp, _, files in os.walk(f):
for f2 in files:
thing = f"{dp}/{f2}"
zf.write(
thing,
f"inject/{os.path.relpath(thing, os.path.dirname(f))}"
) # no, i don't know what this is doing.
if args.k is not None:
zf.write(args.k, "icon.idk")
if __name__ == "__main__":
main()

View File

@@ -21,11 +21,35 @@ 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( 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(
"-n", metavar="name",
help="modify the app's name"
)
parser.add_argument(
"-v", metavar="version",
help="modify the app's version"
)
parser.add_argument(
"-b", metavar="bundle id",
help="modify the app's bundle id"
)
parser.add_argument(
"-m", metavar="minimum",
help="modify the app's minimum OS version"
)
parser.add_argument(
"-k", metavar="icon",
help="modify the app's icon"
)
parser.add_argument( parser.add_argument(
"-u", "--remove-supported-devices", action="store_true", "-u", "--remove-supported-devices", action="store_true",
@@ -71,7 +95,7 @@ def main() -> None:
) )
parser.add_argument( parser.add_argument(
"-v", "--version", action="version", version="cyan v1.0" "--version", action="version", version="cyan v1.1.4"
) )
from cyan import logic from cyan import logic

View File

@@ -38,6 +38,10 @@ def main(parser: ArgumentParser) -> None:
else: else:
sys.exit("[!] main binary is encrypted; exiting") sys.exit("[!] main binary is encrypted; exiting")
if args.cyan is not None:
changing = vars(args)
tbhutils.parse_cyan(changing, tmpdir)
# this goes before injection, # this goes before injection,
# since user might inject their own extensions # since user might inject their own extensions
if args.remove_extensions: if args.remove_extensions:
@@ -47,6 +51,16 @@ def main(parser: ArgumentParser) -> None:
if args.f is not None: if args.f is not None:
app.executable.inject(args.f, tmpdir) app.executable.inject(args.f, tmpdir)
if args.n is not None:
app.plist.change_name(args.n)
if args.v is not None:
app.plist.change_version(args.v)
if args.b is not None:
app.plist.change_bundle_id(args.b)
if args.m is not None:
app.plist.change_minimum_version(args.m)
if args.k is not None:
app.change_icon(args.k, tmpdir)
if args.remove_supported_devices: if args.remove_supported_devices:
app.plist.remove_uisd() app.plist.remove_uisd()

View File

@@ -1,6 +1,7 @@
import os import os
import shutil import shutil
from glob import glob from glob import glob
from uuid import uuid4
from typing import Optional, Literal from typing import Optional, Literal
from .executable import Executable from .executable import Executable
@@ -9,7 +10,7 @@ from .plist import Plist
class AppBundle: class AppBundle:
def __init__(self, path: str): def __init__(self, path: str):
self.path = path self.path = path
self.plist = Plist(f"{path}/Info.plist") self.plist = Plist(f"{path}/Info.plist", path)
self.executable = Executable( self.executable = Executable(
f"{path}/{self.plist['CFBundleExecutable']}", f"{path}/{self.plist['CFBundleExecutable']}",
@@ -101,3 +102,45 @@ class AppBundle:
else: else:
print("[*] removed encrypted plugins:", ", ".join(removed)) print("[*] removed encrypted plugins:", ", ".join(removed))
def change_icon(self, path: str, tmpdir: str) -> None:
try:
from PIL import Image
except Exception:
return print("[?] pillow is not installed, -k is not available")
tmpath = f"{tmpdir}/icon.png"
if not path.endswith(".png"):
with Image.open(path) as img:
img.save(tmpath, "PNG")
else:
shutil.copyfile(path, tmpath)
uid = f"cyan_{uuid4().hex[:7]}a" # can't have it end with a num
i60 = f"{uid}60x60"
i76 = f"{uid}76x76"
with Image.open(tmpath) as img:
img.resize((120, 120)).save(f"{self.path}/{i60}@2x.png", "PNG")
img.resize((152, 152)).save(f"{self.path}/{i76}@2x~ipad.png", "PNG")
if "CFBundleIcons" not in self.plist:
self.plist["CFBundleIcons"] = {}
if "CFBundleIcons~ipad" not in self.plist:
self.plist["CFBundleIcons~ipad"] = {}
self.plist["CFBundleIcons"] = self.plist["CFBundleIcons"] | {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [i60],
"CFBundleIconName": uid
}
}
self.plist["CFBundleIcons~ipad"] = self.plist["CFBundleIcons~ipad"] | {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [i60, i76],
"CFBundleIconName": uid
}
}
self.plist.save()
print("[*] updated app icon")

View File

@@ -18,6 +18,7 @@ class Executable:
ldid = f"{specific}/ldid" ldid = f"{specific}/ldid"
lipo = f"{specific}/lipo" lipo = f"{specific}/lipo"
otool = f"{specific}/otool" otool = f"{specific}/otool"
idylib = f"{specific}/insert_dylib"
starters = ("\t/Library/", "\t@rpath", "\t@executable_path") starters = ("\t/Library/", "\t@rpath", "\t@executable_path")
common = { common = {
@@ -42,10 +43,10 @@ class Executable:
self.bn = os.path.basename(path) self.bn = os.path.basename(path)
self.inj: Optional = None # type: ignore self.inj: Optional = None # type: ignore
if self.specific.endswith("iOS"): if os.path.isfile(self.idylib):
self.inj_func = self.ios_inject self.inj_func = self.idyl_inject
else: else:
self.inj_func = self.insert_cmd self.inj_func = self.lief_inj
def is_encrypted(self) -> bool: def is_encrypted(self) -> bool:
proc = subprocess.run( proc = subprocess.run(
@@ -76,7 +77,7 @@ class Executable:
has_entitlements = True has_entitlements = True
# iirc, injecting doesnt work (sometimes) if the file isn't signed # iirc, injecting doesnt work (sometimes) if the file isn't signed
self.fakesign(False) self.remove_signature()
if any(t.endswith(".appex") for t in tweaks): if any(t.endswith(".appex") for t in tweaks):
os.makedirs(PLUGINS_DIR, exist_ok=True) os.makedirs(PLUGINS_DIR, exist_ok=True)
@@ -151,9 +152,12 @@ class Executable:
self.inj.write(self.path) # type: ignore self.inj.write(self.path) # type: ignore
if has_entitlements: if has_entitlements:
subprocess.run(["ldid", f"-S{ENT_PATH}", self.path]) subprocess.run([self.ldid, f"-S{ENT_PATH}", self.path])
print("[*] restored entitlements") 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: def fakesign(self, keep_entitlements: bool = True) -> bool:
cmd = [self.ldid, "-S"] cmd = [self.ldid, "-S"]
if keep_entitlements: if keep_entitlements:
@@ -174,7 +178,7 @@ class Executable:
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
def insert_cmd(self, cmd: str) -> None: def lief_inj(self, cmd: str) -> None:
if self.inj is None: # type: ignore if self.inj is None: # type: ignore
try: try:
lief.logging.disable() # type: ignore lief.logging.disable() # type: ignore
@@ -183,19 +187,24 @@ class Executable:
self.inj = lief.parse(self.path) # type: ignore self.inj = lief.parse(self.path) # type: ignore
try:
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore 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 ios_inject(self, cmd: str) -> None: def idyl_inject(self, cmd: str) -> None:
subprocess.run( proc = subprocess.run(
[ [
f"{self.specific}/insert_dylib", self.idylib, "--weak", "--inplace", "--strip-codesig", "--all-yes",
"--weak", "--inplace", "--no-strip-codesig", "--all-yes",
cmd, self.path cmd, self.path
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ], 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: def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
self.fakesign() # sometimes it doesnt work if we dont do this self.remove_signature()
for dep in self.get_dependencies(): for dep in self.get_dependencies():
for cname in (tweaks | self.common): for cname in (tweaks | self.common):

View File

@@ -1,16 +1,23 @@
import sys import sys
import plistlib import plistlib
from typing import Any from glob import glob
from typing import Optional, Any
class Plist: class Plist:
def __init__(self, path: str): def __init__(
self, path: str, app_path: Optional[str] = None, throw: bool = True
):
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)
except Exception: except Exception:
if throw:
sys.exit(f"[!] couldn't read {path}") sys.exit(f"[!] couldn't read {path}")
else:
return None
self.path = path self.path = path
self.app_path = app_path
def __getitem__(self, key: str) -> Any: def __getitem__(self, key: str) -> Any:
return self.data.get(key, None) return self.data.get(key, None)
@@ -18,6 +25,9 @@ class Plist:
def __setitem__(self, key: str, val: Any) -> None: def __setitem__(self, key: str, val: Any) -> None:
self.data[key] = val self.data[key] = val
def __contains__(self, key: str) -> bool:
return key in self.data
def save(self) -> None: def save(self) -> None:
with open(self.path, "wb") as f: with open(self.path, "wb") as f:
plistlib.dump(self.data, f) plistlib.dump(self.data, f)
@@ -25,33 +35,89 @@ class Plist:
def remove(self, key: str) -> bool: def remove(self, key: str) -> bool:
try: try:
del self.data[key] del self.data[key]
self.save()
return True return True
except KeyError: except KeyError:
return False return False
def change(self, key: str, val: Any) -> bool: def change(self, val: Any, *keys: str) -> bool:
try: try:
if self[key] == val: if all(self[key] == val for key in keys):
return False return False
except KeyError: except KeyError:
for key in keys:
self[key] = val self[key] = val
self.save()
return True return True
def remove_uisd(self) -> None: def remove_uisd(self) -> None:
if self.remove("UISupportedDevices"): if self.remove("UISupportedDevices"):
self.save()
print("[*] removed UISupportedDevices") print("[*] removed UISupportedDevices")
else: else:
print("[?] no UISupportedDevices") print("[?] no UISupportedDevices")
def enable_documents(self) -> None: def enable_documents(self) -> None:
c1 = self.change("UISupportsDocumentBrowser", True) c1 = self.change(True, "UISupportsDocumentBrowser")
c2 = self.change("UIFileSharingEnabled", True) c2 = self.change(True, "UIFileSharingEnabled")
if c1 or c2: if c1 or c2:
self.save()
print("[*] enabled documents support") print("[*] enabled documents support")
else: else:
print("[?] documents support was already enabled") print("[?] documents support was already enabled")
def change_name(self, name: str) -> None:
if self.change("CFBundleName", "CFBundleDisplayName", name):
print(f"[*] changed name to \"{name}\"")
changed = 0
for lproj in glob(f"{self.app_path}/*.lproj"):
try:
pl = Plist(f"{lproj}/InfoPlist.strings", None, False)
pl["CFBundleDisplayName"] = name
pl["CFBundleName"] = name
pl.save()
changed += 1
except Exception:
pass # file might not exist
if changed != 0:
print(f"[*] changed \033[96m{changed}\033[0m localized names")
else:
print(f"[?] name was already \"{name}\"")
def change_version(self, version: str) -> None:
if self.change("CFBundleVersion", "CFBundleShortVersionString", version):
print(f"[*] changed version to \"{version}\"")
else:
print(f"[?] version was already \"{version}\"")
def change_bundle_id(self, bundle_id: str) -> None:
orig = self["CFBundleIdentifier"]
if self.change("CFBundleIdentifier", bundle_id):
print(f"[*] changed bundle id to \"{bundle_id}\"")
changed = 0
# change all other bundle ids
for ext in glob(f"{self.app_path}/*/*.appex"):
try:
pl = Plist(f"{ext}/Info.plist", None, False)
current = pl["CFBundleIdentifier"]
pl["CFBundleIdentifier"] = current.replace(orig, bundle_id)
pl.save()
changed += 1
except Exception:
pass # how tf would it not exist? idk
if changed != 0:
print(f"[*] changed \033[96m{changed}\033[0m other bundle ids")
else:
print(f"[?] bundle id was already \"{bundle_id}\"")
def change_minimum_version(self, minimum: str) -> None:
if self.change("MinimumOSVersion", minimum):
print(f"[*] changed minimum version to \"{minimum}\"")
else:
print(f"[?] minimum version was already \"{minimum}\"")

View File

@@ -1,13 +1,14 @@
import os import os
import sys import sys
import json
import shutil import shutil
import zipfile import zipfile
import platform import platform
import subprocess import subprocess
from uuid import uuid4 from uuid import uuid4
from typing import Optional
from glob import glob, iglob from glob import glob, iglob
from argparse import Namespace from argparse import Namespace
from typing import Optional, Any
def validate_inputs(args: Namespace) -> Optional[str]: def validate_inputs(args: Namespace) -> Optional[str]:
@@ -38,15 +39,31 @@ def validate_inputs(args: Namespace) -> Optional[str]:
sys.exit(0) sys.exit(0)
if args.f is not None: if args.f is not None:
# dictionary ensures unique names new: dict[str, str] = {} # dictionary ensures unique names
args.f = {os.path.basename(f): os.path.realpath(f) for f in args.f}
nonexistent = [f for f in args.f.values() if not os.path.exists(f)]
if len(nonexistent) != 0: for f in list(args.f):
print("[!] please ensure the following file(s) exist:") if f[-1] == "/": # yeah this is stupid
for ne in nonexistent: f = f[:-1]
print(f"[?] - {ne}")
sys.exit(1) if not os.path.exists(f):
sys.exit(f"[!] \"{f}\" does not exist")
new[os.path.basename(f)] = os.path.realpath(f)
# i would've modified args.f directly, but it causes type-hinting error :(
args.f = new
if (
args.m is not None
and any(char not in "0123456789." for char in args.m)
):
sys.exit(f"[!] invalid OS version: {args.m}")
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")
def get_app(path: str, tmpdir: str, is_ipa: bool) -> str: def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
@@ -93,11 +110,12 @@ def get_tools_dir() -> tuple[str, str]:
# thank god i dont have to use importlib, # thank god i dont have to use importlib,
# it's the WORST standard library package prior to 3.12 # it's the WORST standard library package prior to 3.12
install_dir = os.path.dirname(__file__) install_dir = os.path.dirname(__file__)
specific_dir = f"{install_dir}/tools/{system}/{mach}"
return ( if not os.path.isdir(specific_dir):
install_dir, sys.exit(f"[!] cyan is not supported on: {system} {mach}")
f"{install_dir}/tools/{system}/{mach}"
) return (install_dir, specific_dir)
def delete_if_exists(path: str, bn: str) -> bool: def delete_if_exists(path: str, bn: str) -> bool:
@@ -140,12 +158,16 @@ def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
for hi in sum(( for hi in sum((
glob(f"{t2}/**/*.dylib", recursive=True), glob(f"{t2}/**/*.dylib", recursive=True),
glob(f"{t2}/**/*.bundle", recursive=True),
glob(f"{t2}/**/*.appex", recursive=True), glob(f"{t2}/**/*.appex", recursive=True),
glob(f"{t2}/**/*.bundle", recursive=True),
glob(f"{t2}/**/*.framework", recursive=True) glob(f"{t2}/**/*.framework", recursive=True)
), []): # type: ignore ), []): # type: ignore
if os.path.islink(hi): if (
continue # symlinks are broken iirc os.path.islink(hi) # symlinks are broken iirc
or hi.count(".bundle") > 1 # prevent sub-bundle detection (rip)
or hi.count(".framework") > 1
):
continue
tweaks[os.path.basename(hi)] = hi tweaks[os.path.basename(hi)] = hi
@@ -170,3 +192,29 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
if weird != 0: if weird != 0:
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:
print("[*] parsing .cyan file..")
with zipfile.ZipFile(args["cyan"]) as zf:
DOT_PATH = f"{tmpdir}/cyan"
os.mkdir(DOT_PATH)
with zf.open("config.json") as f:
config = json.load(f)
if "f" in config:
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
zf.extractall(DOT_PATH, NAMES)
# 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"]
for k, v in config.items():
args[k] = v

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,17 +1,21 @@
# type: ignore
from setuptools import setup from setuptools import setup
setup( setup(
name="cyan", name="cyan",
version="1.0", version="1.1.4",
description="finally, pyzule doesn't suck", description="finally, pyzule doesn't suck",
author="zx", author="zx",
author_email="hi@zxcvbn.fyi", author_email="zx@hrzn.email",
packages=["cyan", "cyan.tbhtypes"], packages=["cyan", "cyan.tbhtypes", "cgen"],
# install_requires=["lief"],
python_requires=">=3.9", python_requires=">=3.9",
include_package_data=True, include_package_data=True,
entry_points={ entry_points={
"console_scripts": ["cyan=cyan.__main__:main"], "console_scripts": [
"cyan=cyan.__main__:main",
"cgen=cgen.__main__:main"
],
} }
) )