mirror of
https://github.com/NohamR/pyzule-rw.git
synced 2026-10-11 02:49:40 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740d3716dc | ||
|
|
5ce7a0cb43 | ||
|
|
04a40f2aa6 | ||
|
|
6845281446 | ||
|
|
54da4485a5 | ||
|
|
f7b0169ee9 | ||
|
|
ffbff5c78d | ||
|
|
eba32c22e6 | ||
|
|
a96632f1b0 | ||
|
|
86e45cc186 | ||
|
|
e09351287b | ||
|
|
403e5d65a5 | ||
|
|
354a1d9ac5 | ||
|
|
73d1495eff | ||
|
|
31169d6c65 | ||
|
|
0808fd8c1b | ||
|
|
8c873a84c8 | ||
|
|
7543bb3d8d | ||
|
|
c0b0324a12 | ||
|
|
42517c90fb | ||
|
|
b4e3e6aaf5 | ||
|
|
b60788d167 | ||
|
|
5f37ab3f23 | ||
|
|
4e05892e1d | ||
|
|
54359e0935 | ||
|
|
46752d0a26 | ||
|
|
ce20baae4a | ||
|
|
91494bc3a3 | ||
|
|
edd34f6713 | ||
|
|
c1c0b2294e | ||
|
|
2d16015c78 | ||
|
|
4c979a91e7 | ||
|
|
b4aa6c9b69 | ||
|
|
246f6ace80 | ||
|
|
511cb4bbe3 | ||
|
|
d1c3bc6908 | ||
|
|
8fe0fc2984 | ||
|
|
e315454544 | ||
|
|
9072c91965 | ||
|
|
06a2ea8dad | ||
|
|
55b2ec9d07 | ||
|
|
52b3b11a1a | ||
|
|
4375b7f880 | ||
|
|
8f7db1438a | ||
|
|
5e08afc0ac | ||
|
|
0bd6393e78 | ||
|
|
f5a5d7b5ed | ||
|
|
c3a51e7e03 | ||
|
|
806db413e2 | ||
|
|
927df65b55 | ||
|
|
851eedf629 | ||
|
|
126d17924b | ||
|
|
5ebab86a44 | ||
|
|
c8197a8c47 | ||
|
|
1e9607d9e1 | ||
|
|
30fa189197 | ||
|
|
5c0363b5a9 |
22
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal 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: bug
|
||||
description: report a bug in cyan
|
||||
title: "[bug] "
|
||||
labels: bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: have you searched the existing issues?
|
||||
options:
|
||||
- label: this is a unique issue. i agree that if this isn't a unique issue, i'll be BLOCKED from the cyan repo
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: describe the bug.
|
||||
description: attach videos or screenshots if possible
|
||||
validations:
|
||||
required: true
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal 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: feature request
|
||||
description: request a feature to be added to cyan
|
||||
title: "[feature request] "
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: have you searched the existing issues?
|
||||
options:
|
||||
- label: this is a unique feature request. i agree that if this isn't a unique request, i'll be BLOCKED from the cyan repo
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: describe the feature request.
|
||||
description: what exactly are you requesting? can you provide an example where this would be useful?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal 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
8
.gitignore
vendored
@@ -1,5 +1,11 @@
|
||||
__pycache__/
|
||||
|
||||
build/
|
||||
*.egg-info
|
||||
*.egg-info/
|
||||
|
||||
*.cyan
|
||||
.DS_Store
|
||||
|
||||
*.ipa
|
||||
*.app/
|
||||
|
||||
|
||||
57
README.md
57
README.md
@@ -1,49 +1,64 @@
|
||||
# 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!
|
||||
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
|
||||
|
||||
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 !! also see my [recommended flags](https://github.com/asdfzxcvbn/pyzule-rw/wiki/recommended-flags)
|
||||
|
||||
- generate and use shareable .cyan files to configure IPAs!
|
||||
- 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
|
||||
- copy any unknown file/folder types to app root
|
||||
- change app name, version, bundle id, and minimum os version
|
||||
- remove UISupportedDevices
|
||||
- remove watch app
|
||||
- fakesign the output ipa/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!)
|
||||
|
||||
## 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
|
||||
|
||||
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`
|
||||
the `zip` and `unzip` commands are *optional* dependencies, they may [fix issues when extracting certain IPAs with chinese characters](https://github.com/asdfzxcvbn/pyzule-rw/wiki/file-does-not-exist-(executable)-%3F), etc
|
||||
|
||||
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>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><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>
|
||||
|
||||
## todo
|
||||
<details>
|
||||
<summary><b>jailbroken iOS instructions / automated environment (github workflow, etc)</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>
|
||||
|
||||
[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
|
||||
|
||||
[x] feat: thin binaries
|
||||
|
||||
[x] feat: plugin operations (-q, -e)
|
||||
|
||||
[] feat: .cyan files (lol rip .pyzule files)
|
||||
- [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)
|
||||
- [tyilo](https://github.com/tyilo) for [insert_dylib](https://github.com/tyilo/insert_dylib/) (macOS/iOS)
|
||||
- [LeanVel](https://github.com/LeanVel) for [insert_dylib](https://github.com/LeanVel/insert_dylib) (linux)
|
||||
|
||||
|
||||
160
cgen/__main__.py
Normal file
160
cgen/__main__.py
Normal file
@@ -0,0 +1,160 @@
|
||||
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(
|
||||
"-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",
|
||||
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.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)]
|
||||
|
||||
# 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 "fkxl": # 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 args.l:
|
||||
zf.write(args.l, "merge.plist")
|
||||
|
||||
if args.x:
|
||||
zf.write(args.x, "new.entitlements")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -22,11 +22,44 @@ def main() -> None:
|
||||
help="if unspecified, overwrites input"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
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(
|
||||
"-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",
|
||||
help="remove UISupportedDevices"
|
||||
@@ -71,7 +104,7 @@ def main() -> None:
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--version", action="version", version="cyan v1.0"
|
||||
"--version", action="version", version="cyan v1.4.4"
|
||||
)
|
||||
|
||||
from cyan import logic
|
||||
|
||||
@@ -4,24 +4,34 @@
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Cephei</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ws.hbang.common</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Cephei</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
|
||||
<key>HBPackageIdentifier</key>
|
||||
<string>ws.hbang.common</string>
|
||||
</dict>
|
||||
|
||||
@@ -4,26 +4,37 @@
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>CepheiPrefs</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ws.hbang.common.prefs</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Cephei</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
|
||||
<key>HBPackageIdentifier</key>
|
||||
<string>ws.hbang.common</string>
|
||||
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>HBDemoRootListController</string>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
@@ -12,7 +12,7 @@
|
||||
<string>Orion</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>1.0.2</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
Binary file not shown.
BIN
cyan/extras/zero.requirements
Normal file
BIN
cyan/extras/zero.requirements
Normal file
Binary file not shown.
@@ -13,8 +13,12 @@ def main(parser: ArgumentParser) -> None:
|
||||
|
||||
if args.output is not None:
|
||||
args.o = os.path.normpath(args.output)
|
||||
if not (args.o.endswith(".app") or args.o.endswith(".ipa")):
|
||||
print("[?] output's file extension not specified; will create ipa")
|
||||
if not (
|
||||
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"
|
||||
else:
|
||||
args.o = args.i
|
||||
@@ -25,8 +29,9 @@ def main(parser: ArgumentParser) -> None:
|
||||
if arg_err is not None:
|
||||
parser.error(arg_err)
|
||||
|
||||
INPUT_IS_IPA = True if args.i.endswith(".ipa") else False
|
||||
OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False
|
||||
# mfw when "True if True else False" HAHAHAH
|
||||
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():
|
||||
app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
|
||||
@@ -38,6 +43,10 @@ def main(parser: ArgumentParser) -> None:
|
||||
else:
|
||||
sys.exit("[!] main binary is encrypted; exiting")
|
||||
|
||||
if args.cyan is not None:
|
||||
changing = vars(args)
|
||||
tbhutils.parse_cyans(changing, tmpdir)
|
||||
|
||||
# this goes before injection,
|
||||
# since user might inject their own extensions
|
||||
if args.remove_extensions:
|
||||
@@ -47,6 +56,20 @@ def main(parser: ArgumentParser) -> None:
|
||||
|
||||
if args.f is not None:
|
||||
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.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:
|
||||
app.plist.remove_uisd()
|
||||
@@ -72,6 +95,6 @@ def main(parser: ArgumentParser) -> None:
|
||||
if os.path.isdir(args.o):
|
||||
shutil.rmtree(args.o)
|
||||
|
||||
shutil.move(app_path, args.o)
|
||||
shutil.move(app.path, args.o)
|
||||
print(f"[*] generated app at {args.o}")
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from .app_bundle import AppBundle
|
||||
from .executable import Executable
|
||||
from .leaving_cm import LeavingCM
|
||||
from .main_executable import MainExecutable
|
||||
from .plist import Plist
|
||||
|
||||
__all__ = [
|
||||
"AppBundle",
|
||||
"Executable",
|
||||
"LeavingCM",
|
||||
"MainExecutable",
|
||||
"Plist"
|
||||
]
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import os
|
||||
import shutil
|
||||
from glob import glob
|
||||
from uuid import uuid4
|
||||
from typing import Optional, Literal
|
||||
|
||||
from .executable import Executable
|
||||
from .main_executable import MainExecutable
|
||||
from .plist import Plist
|
||||
|
||||
class AppBundle:
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
self.plist = Plist(f"{path}/Info.plist")
|
||||
self.plist = Plist(f"{path}/Info.plist", path)
|
||||
|
||||
self.executable = Executable(
|
||||
self.executable = MainExecutable(
|
||||
f"{path}/{self.plist['CFBundleExecutable']}",
|
||||
path
|
||||
)
|
||||
@@ -101,3 +103,45 @@ class AppBundle:
|
||||
else:
|
||||
print("[*] removed encrypted plugins:", ", ".join(removed))
|
||||
|
||||
def change_icon(self, path: str, tmpdir: str) -> None:
|
||||
try:
|
||||
from PIL import Image # type: ignore
|
||||
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")
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import lief
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from cyan import tbhutils
|
||||
|
||||
@@ -18,34 +11,50 @@ class Executable:
|
||||
ldid = f"{specific}/ldid"
|
||||
lipo = f"{specific}/lipo"
|
||||
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
|
||||
## 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
|
||||
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
||||
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
||||
common = {
|
||||
# substrate could show up as
|
||||
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
||||
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
||||
|
||||
"CydiaSubstrate.framework": "CydiaSubstrate.framework",
|
||||
"Orion.framework": "Orion.framework",
|
||||
"Cephei.framework": "Cephei.framework",
|
||||
"CepheiUI.framework": "CepheiUI.framework",
|
||||
"CepheiPrefs.framework": "CepheiPrefs.framework"
|
||||
"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, bundle_path: Optional[str] = None):
|
||||
def __init__(self, path: str):
|
||||
if not os.path.isfile(path):
|
||||
sys.exit(f"[!] {path} does not exist (executable)")
|
||||
print(f"[!] {path} does not exist (executable)", file=sys.stderr)
|
||||
sys.exit(
|
||||
"[?] check the wiki for info: "
|
||||
"https://github.com/asdfzxcvbn/pyzule-rw/wiki/"
|
||||
"file-does-not-exist-(executable)-%3F"
|
||||
)
|
||||
|
||||
self.path = path
|
||||
self.bundle_path = bundle_path
|
||||
|
||||
self.bn = os.path.basename(path)
|
||||
self.inj: Optional = None # type: ignore
|
||||
|
||||
if self.specific.endswith("iOS"):
|
||||
self.inj_func = self.ios_inject
|
||||
else:
|
||||
self.inj_func = self.insert_cmd
|
||||
|
||||
def is_encrypted(self) -> bool:
|
||||
proc = subprocess.run(
|
||||
@@ -55,112 +64,11 @@ class Executable:
|
||||
|
||||
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
|
||||
def remove_signature(self) -> None:
|
||||
subprocess.run([self.ldid, "-R", self.path], stderr=subprocess.DEVNULL)
|
||||
|
||||
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.fakesign(False)
|
||||
|
||||
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(["ldid", f"-S{ENT_PATH}", self.path])
|
||||
print("[*] restored entitlements")
|
||||
|
||||
def fakesign(self, keep_entitlements: bool = True) -> bool:
|
||||
cmd = [self.ldid, "-S"]
|
||||
if keep_entitlements:
|
||||
cmd.append("-M")
|
||||
|
||||
subprocess.run(cmd + [self.path])
|
||||
return True
|
||||
def fakesign(self) -> bool:
|
||||
return subprocess.run([self.ldid, "-S", "-M", self.path]).returncode == 0
|
||||
|
||||
def thin(self) -> bool:
|
||||
return subprocess.run(
|
||||
@@ -174,34 +82,29 @@ class Executable:
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
def insert_cmd(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
|
||||
|
||||
self.inj.add(lief.MachO.DylibCommand.weak_lib(cmd)) # type: ignore
|
||||
|
||||
def ios_inject(self, cmd: str) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
f"{self.specific}/insert_dylib",
|
||||
"--weak", "--inplace", "--no-strip-codesig", "--all-yes",
|
||||
cmd, self.path
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
def fix_dependencies(self, tweaks: dict[str, str], need: set[str]) -> None:
|
||||
self.fakesign() # sometimes it doesnt work if we dont do this
|
||||
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}"
|
||||
@@ -210,9 +113,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],
|
||||
|
||||
171
cyan/tbhtypes/main_executable.py
Normal file
171
cyan/tbhtypes/main_executable.py
Normal 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}")
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import sys
|
||||
import plistlib
|
||||
from typing import Any
|
||||
from glob import glob
|
||||
from typing import Optional, Any
|
||||
|
||||
class Plist:
|
||||
def __init__(self, path: str):
|
||||
def __init__(
|
||||
self, path: str, app_path: Optional[str] = None, throw: bool = True
|
||||
):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
self.data: dict[str, Any] = plistlib.load(f)
|
||||
|
||||
self.success = True
|
||||
except Exception:
|
||||
sys.exit(f"[!] couldn't read {path}")
|
||||
if throw:
|
||||
sys.exit(f"[!] couldn't read {path}")
|
||||
|
||||
self.success = False
|
||||
|
||||
self.path = path
|
||||
self.app_path = app_path
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.data.get(key, None)
|
||||
@@ -18,6 +27,9 @@ class Plist:
|
||||
def __setitem__(self, key: str, val: Any) -> None:
|
||||
self.data[key] = val
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.data
|
||||
|
||||
def save(self) -> None:
|
||||
with open(self.path, "wb") as f:
|
||||
plistlib.dump(self.data, f)
|
||||
@@ -25,33 +37,104 @@ class Plist:
|
||||
def remove(self, key: str) -> bool:
|
||||
try:
|
||||
del self.data[key]
|
||||
self.save()
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def change(self, key: str, val: Any) -> bool:
|
||||
def change(self, val: Any, *keys: str) -> bool:
|
||||
try:
|
||||
if self[key] == val:
|
||||
if all(self[key] == val for key in keys):
|
||||
return False
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
self[key] = val
|
||||
for key in keys:
|
||||
self[key] = val
|
||||
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def remove_uisd(self) -> None:
|
||||
if self.remove("UISupportedDevices"):
|
||||
self.save()
|
||||
print("[*] removed UISupportedDevices")
|
||||
else:
|
||||
print("[?] no UISupportedDevices")
|
||||
|
||||
def enable_documents(self) -> None:
|
||||
c1 = self.change("UISupportsDocumentBrowser", True)
|
||||
c2 = self.change("UIFileSharingEnabled", True)
|
||||
c1 = self.change(True, "UISupportsDocumentBrowser")
|
||||
c2 = self.change(True, "UIFileSharingEnabled")
|
||||
|
||||
if c1 or c2:
|
||||
self.save()
|
||||
print("[*] enabled documents support")
|
||||
else:
|
||||
print("[?] documents support was already enabled")
|
||||
|
||||
def change_name(self, name: str) -> None:
|
||||
if self.change(name, "CFBundleName", "CFBundleDisplayName"):
|
||||
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.change(name, "CFBundleName", "CFBundleDisplayName")
|
||||
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(version, "CFBundleVersion", "CFBundleShortVersionString"):
|
||||
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(bundle_id, "CFBundleIdentifier"):
|
||||
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(minimum, "MinimumOSVersion"):
|
||||
print(f"[*] changed minimum version to \"{minimum}\"")
|
||||
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))
|
||||
|
||||
|
||||
154
cyan/tbhutils.py
154
cyan/tbhutils.py
@@ -1,21 +1,27 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
import platform
|
||||
import subprocess
|
||||
from uuid import uuid4
|
||||
from typing import Optional
|
||||
from glob import glob, iglob
|
||||
from argparse import Namespace
|
||||
from typing import Optional, Any
|
||||
from plistlib import load as pload
|
||||
|
||||
HAS_ZIP = shutil.which("zip") is not None
|
||||
HAS_UNZIP = shutil.which("unzip") is not None
|
||||
|
||||
|
||||
def validate_inputs(args: Namespace) -> Optional[str]:
|
||||
if not (
|
||||
args.i.endswith(".ipa")
|
||||
or args.i.endswith(".app")
|
||||
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):
|
||||
return f"{args.i} does not exist"
|
||||
@@ -38,15 +44,46 @@ def validate_inputs(args: Namespace) -> Optional[str]:
|
||||
sys.exit(0)
|
||||
|
||||
if args.f is not None:
|
||||
# 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)]
|
||||
new: dict[str, str] = {} # dictionary ensures unique names
|
||||
|
||||
if len(nonexistent) != 0:
|
||||
print("[!] please ensure the following file(s) exist:")
|
||||
for ne in nonexistent:
|
||||
print(f"[?] - {ne}")
|
||||
sys.exit(1)
|
||||
for f in list(args.f):
|
||||
if f[-1] == "/": # yeah this is stupid
|
||||
f = f[:-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.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):
|
||||
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:
|
||||
@@ -64,7 +101,15 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
|
||||
elif not any(name.endswith(".app/Info.plist") for name in names):
|
||||
sys.exit("[!] no Info.plist, invalid app")
|
||||
|
||||
ipa.extractall(tmpdir)
|
||||
# using unzip fixes extraction errors in ipas with chinese chars, etc
|
||||
if HAS_UNZIP:
|
||||
subprocess.run(
|
||||
["unzip", path, "-d", tmpdir],
|
||||
stdout=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
ipa.extractall(tmpdir)
|
||||
|
||||
app = glob(f"{payload}/*.app")[0]
|
||||
except (KeyError, IndexError):
|
||||
sys.exit("[!] couldn't find either Payload or app folder, invalid ipa")
|
||||
@@ -93,11 +138,12 @@ def get_tools_dir() -> tuple[str, str]:
|
||||
# thank god i dont have to use importlib,
|
||||
# it's the WORST standard library package prior to 3.12
|
||||
install_dir = os.path.dirname(__file__)
|
||||
specific_dir = f"{install_dir}/tools/{system}/{mach}"
|
||||
|
||||
return (
|
||||
install_dir,
|
||||
f"{install_dir}/tools/{system}/{mach}"
|
||||
)
|
||||
if not os.path.isdir(specific_dir):
|
||||
sys.exit(f"[!] cyan is not supported on: {system} {mach}")
|
||||
|
||||
return (install_dir, specific_dir)
|
||||
|
||||
|
||||
def delete_if_exists(path: str, bn: str) -> bool:
|
||||
@@ -140,12 +186,16 @@ def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
|
||||
|
||||
for hi in sum((
|
||||
glob(f"{t2}/**/*.dylib", recursive=True),
|
||||
glob(f"{t2}/**/*.bundle", recursive=True),
|
||||
glob(f"{t2}/**/*.appex", recursive=True),
|
||||
glob(f"{t2}/**/*.bundle", recursive=True),
|
||||
glob(f"{t2}/**/*.framework", recursive=True)
|
||||
), []): # type: ignore
|
||||
if os.path.islink(hi):
|
||||
continue # symlinks are broken iirc
|
||||
if (
|
||||
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
|
||||
):
|
||||
continue
|
||||
|
||||
tweaks[os.path.basename(hi)] = hi
|
||||
|
||||
@@ -158,15 +208,63 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
|
||||
os.chdir(tmpdir)
|
||||
weird = 0
|
||||
|
||||
with zipfile.ZipFile(
|
||||
output, "w", zipfile.ZIP_DEFLATED, compresslevel=level
|
||||
) as zf:
|
||||
for f in iglob("Payload/**", recursive=True):
|
||||
try:
|
||||
zf.write(f)
|
||||
except ValueError:
|
||||
weird += 1
|
||||
if HAS_ZIP:
|
||||
try:
|
||||
os.remove(output) # zip command updates zipfiles by default
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# don't zip hidden files to fix an installd error sometimes
|
||||
# thanks a lot eevee 😭
|
||||
subprocess.run(
|
||||
["zip", f"-{level}", "-r", output, "Payload", "-x", "*/.*"],
|
||||
stdout=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
with zipfile.ZipFile(
|
||||
output, "w", zipfile.ZIP_DEFLATED, compresslevel=level
|
||||
) as zf:
|
||||
for f in iglob("Payload/**", recursive=True):
|
||||
try:
|
||||
zf.write(f)
|
||||
except ValueError:
|
||||
weird += 1
|
||||
|
||||
if weird != 0:
|
||||
print(f"[?] was unable to zip {weird} file(s) due to timestamps")
|
||||
|
||||
|
||||
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 zipfile.ZipFile(cyan) as zf:
|
||||
DOT_PATH = f"{tmpdir}/cyan-{ind}"
|
||||
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"]
|
||||
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
|
||||
|
||||
|
||||
BIN
cyan/tools/Darwin/arm64/insert_dylib
Executable file
BIN
cyan/tools/Darwin/arm64/insert_dylib
Executable file
Binary file not shown.
BIN
cyan/tools/Darwin/x86_64/insert_dylib
Executable file
BIN
cyan/tools/Darwin/x86_64/insert_dylib
Executable file
Binary file not shown.
BIN
cyan/tools/Linux/x86_64/insert_dylib
Executable file
BIN
cyan/tools/Linux/x86_64/insert_dylib
Executable file
Binary file not shown.
14
setup.py
14
setup.py
@@ -1,17 +1,21 @@
|
||||
# type: ignore
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="cyan",
|
||||
version="1.0",
|
||||
version="1.4.4",
|
||||
description="finally, pyzule doesn't suck",
|
||||
author="zx",
|
||||
author_email="hi@zxcvbn.fyi",
|
||||
packages=["cyan", "cyan.tbhtypes"],
|
||||
# install_requires=["lief"],
|
||||
author_email="z@zxcvbn.fyi",
|
||||
packages=["cyan", "cyan.tbhtypes", "cgen"],
|
||||
python_requires=">=3.9",
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
"console_scripts": ["cyan=cyan.__main__:main"],
|
||||
"console_scripts": [
|
||||
"cyan=cyan.__main__:main",
|
||||
"cgen=cgen.__main__:main"
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user