12 Commits
v1.2 ... v1.3

Author SHA1 Message Date
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
11 changed files with 101 additions and 88 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

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
__pycache__/ __pycache__/
build/ build/
*.egg-info *.egg-info/

View File

@@ -14,7 +14,7 @@ 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
- add custom entitlements to the main executable - 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!)
@@ -48,8 +48,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

@@ -99,7 +99,7 @@ def main() -> None:
) )
parser.add_argument( parser.add_argument(
"--version", action="version", version="cyan v1.2" "--version", action="version", version="cyan v1.3"
) )
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.

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)
@@ -61,8 +66,8 @@ 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.x is not None: # `validate_inputs()` made it a dict if args.x is not None:
app.executable.merge_entitlements(args.x, tmpdir) 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,6 @@
import os import os
import sys import sys
import subprocess import subprocess
from typing import Optional
try:
import lief
except Exception:
pass
from cyan import tbhutils from cyan import tbhutils
@@ -42,14 +36,7 @@ class Executable:
) )
self.path = path self.path = 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(
@@ -65,17 +52,6 @@ class Executable:
def fakesign(self) -> bool: def fakesign(self) -> bool:
return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0 return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0
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 thin(self) -> bool: def thin(self) -> bool:
return subprocess.run( return subprocess.run(
[self.lipo, "-thin", "arm64", self.path, "-output", self.path], [self.lipo, "-thin", "arm64", self.path, "-output", self.path],
@@ -88,31 +64,6 @@ class Executable:
stderr=subprocess.DEVNULL 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: def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
self.remove_signature() self.remove_signature()

View File

@@ -1,9 +1,13 @@
import os import os
import sys
import shutil import shutil
import plistlib
import subprocess import subprocess
from typing import Any from typing import Optional
from plistlib import dump as pdump
try:
import lief
except Exception:
pass
from cyan import tbhutils from cyan import tbhutils
from .executable import Executable from .executable import Executable
@@ -13,13 +17,20 @@ class MainExecutable(Executable):
super().__init__(path) super().__init__(path)
self.bundle_path = bundle_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: def inject(self, tweaks: dict[str, str], tmpdir: str) -> None:
ENT_PATH = f"{self.bundle_path}/cyan.entitlements" ENT_PATH = f"{self.bundle_path}/cyan.entitlements"
PLUGINS_DIR = f"{self.bundle_path}/PlugIns" PLUGINS_DIR = f"{self.bundle_path}/PlugIns"
FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks" FRAMEWORKS_DIR = f"{self.bundle_path}/Frameworks"
has_entitlements = self.write_entitlements(ENT_PATH) has_entitlements = self.write_entitlements(ENT_PATH)
# iirc, injecting doesnt work (sometimes) if the file isn't signed # iirc, injecting doesnt work (sometimes) if the file is signed
self.remove_signature() self.remove_signature()
if any(t.endswith(".appex") for t in tweaks): if any(t.endswith(".appex") for t in tweaks):
@@ -98,27 +109,50 @@ class MainExecutable(Executable):
self.sign_with_entitlements(ENT_PATH) self.sign_with_entitlements(ENT_PATH)
print("[*] restored entitlements") print("[*] restored entitlements")
def merge_entitlements( def write_entitlements(self, output: str) -> bool:
self, entitlements: dict[str, Any], tmpdir: str with open(output, "wb") as entf:
) -> None: proc = subprocess.run(
ENT_PATH = f"{tmpdir}/new.entitlements" [self.ldid, "-e", self.path],
existing: dict[str, Any] capture_output=True
)
if self.write_entitlements(ENT_PATH): # has entitlements entf.write(proc.stdout)
with open(ENT_PATH, "rb") as f:
existing = plistlib.load(f) return os.path.getsize(output) > 0
def merge_entitlements(self, entitlements: str) -> None:
if self.sign_with_entitlements(entitlements):
print("[*] merged new entitlements")
else: else:
existing = {} print("[!] failed to merge new entitlements, are they valid?")
new = existing | entitlements
with open(ENT_PATH, "wb") as f2:
pdump(new, f2)
self.sign_with_entitlements(ENT_PATH)
print("[*] modified entitlement keys:", ", ".join(entitlements))
def sign_with_entitlements(self, entitlements: str) -> bool: def sign_with_entitlements(self, entitlements: str) -> bool:
return subprocess.run( return subprocess.run(
[self.ldid, f"-S{entitlements}", self.path] [self.ldid, f"-S{entitlements}", "-M", "-Cadhoc", self.path]
).returncode == 0 ).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

@@ -17,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"
@@ -75,7 +76,7 @@ def validate_inputs(args: Namespace) -> Optional[str]:
try: try:
with open(args.x, "rb") as f: with open(args.x, "rb") as f:
args.x = pload(f) pload(f)
except Exception: except Exception:
sys.exit("[!] couldn't parse given entitlements file") sys.exit("[!] couldn't parse given entitlements file")
@@ -208,8 +209,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:

View File

@@ -4,7 +4,7 @@ from setuptools import setup
setup( setup(
name="cyan", name="cyan",
version="1.2", version="1.3",
description="finally, pyzule doesn't suck", description="finally, pyzule doesn't suck",
author="zx", author="zx",
author_email="zx@hrzn.email", author_email="zx@hrzn.email",