mirror of
https://github.com/NohamR/pyzule-rw.git
synced 2026-10-11 02:49:40 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||||
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
build/
|
build/
|
||||||
*.egg-info
|
*.egg-info/
|
||||||
|
|
||||||
|
*.cyan
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
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 !!
|
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!
|
- 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
|
||||||
@@ -14,7 +16,9 @@ you can open an issue to request a feature :D !!
|
|||||||
- 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!)
|
||||||
|
|
||||||
@@ -26,6 +30,8 @@ first, make sure you have [ar](https://command-not-found.com/ar) and [tar](https
|
|||||||
|
|
||||||
also obviously install python, version 3.9 or greater is required
|
also obviously install python, version 3.9 or greater is required
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>linux/WSL/macOS instructions</b></summary>
|
<summary><b>linux/WSL/macOS instructions</b></summary>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -45,8 +51,6 @@ also obviously install python, version 3.9 or greater is required
|
|||||||
</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` !
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.5"
|
"--version", action="version", version="cyan v1.4.4"
|
||||||
)
|
)
|
||||||
|
|
||||||
from cyan import logic
|
from cyan import logic
|
||||||
|
|||||||
@@ -4,24 +4,34 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
|
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Cephei</string>
|
<string>Cephei</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ws.hbang.common</string>
|
<string>ws.hbang.common</string>
|
||||||
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
|
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Cephei</string>
|
<string>Cephei</string>
|
||||||
|
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
|
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>1.0.0</string>
|
||||||
|
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
|
|
||||||
<key>DTPlatformName</key>
|
<key>DTPlatformName</key>
|
||||||
<string>iphoneos</string>
|
<string>iphoneos</string>
|
||||||
|
|
||||||
<key>HBPackageIdentifier</key>
|
<key>HBPackageIdentifier</key>
|
||||||
<string>ws.hbang.common</string>
|
<string>ws.hbang.common</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -4,26 +4,37 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
|
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>CepheiPrefs</string>
|
<string>CepheiPrefs</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>ws.hbang.common.prefs</string>
|
<string>ws.hbang.common.prefs</string>
|
||||||
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
|
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Cephei</string>
|
<string>Cephei</string>
|
||||||
|
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
|
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>1.0.0</string>
|
||||||
|
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
|
|
||||||
<key>DTPlatformName</key>
|
<key>DTPlatformName</key>
|
||||||
<string>iphoneos</string>
|
<string>iphoneos</string>
|
||||||
|
|
||||||
<key>HBPackageIdentifier</key>
|
<key>HBPackageIdentifier</key>
|
||||||
<string>ws.hbang.common</string>
|
<string>ws.hbang.common</string>
|
||||||
|
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>HBDemoRootListController</string>
|
<string>HBDemoRootListController</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Binary file not shown.
@@ -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.
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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,33 +13,46 @@ 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 = {
|
starters = ("\t/Library/", "\t/usr/lib/", "\t@rpath", "\t@executable_path")
|
||||||
# substrate could show up as
|
|
||||||
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
|
||||||
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
|
||||||
|
|
||||||
"CydiaSubstrate.framework": "CydiaSubstrate.framework",
|
# substrate could show up as
|
||||||
"Orion.framework": "Orion.framework",
|
# CydiaSubstrate.framework, libsubstrate.dylib, EVEN CydiaSubstrate.dylib
|
||||||
"Cephei.framework": "Cephei.framework",
|
# AND PROBABLY EVEN MORE !!!! IT'S CRAZY.
|
||||||
"CepheiUI.framework": "CepheiUI.framework",
|
common = {
|
||||||
"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):
|
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.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(
|
||||||
@@ -56,115 +62,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(
|
||||||
@@ -178,39 +80,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}"
|
||||||
@@ -219,9 +111,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],
|
||||||
|
|||||||
168
cyan/tbhtypes/main_executable.py
Normal file
168
cyan/tbhtypes/main_executable.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
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", 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}")
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
120
cyan/tbhutils.py
120
cyan/tbhutils.py
@@ -9,14 +9,19 @@ 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_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"
|
||||||
@@ -62,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:
|
||||||
@@ -81,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):
|
elif not any(name.endswith(".app/Info.plist") for name in names):
|
||||||
sys.exit("[!] no Info.plist, invalid app")
|
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]
|
app = glob(f"{payload}/*.app")[0]
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
sys.exit("[!] couldn't find either Payload or app folder, invalid ipa")
|
sys.exit("[!] couldn't find either Payload or app folder, invalid ipa")
|
||||||
@@ -163,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
|
||||||
):
|
):
|
||||||
@@ -180,41 +208,63 @@ def make_ipa(tmpdir: str, output: str, level: int) -> None:
|
|||||||
os.chdir(tmpdir)
|
os.chdir(tmpdir)
|
||||||
weird = 0
|
weird = 0
|
||||||
|
|
||||||
with zipfile.ZipFile(
|
if HAS_ZIP:
|
||||||
output, "w", zipfile.ZIP_DEFLATED, compresslevel=level
|
try:
|
||||||
) as zf:
|
os.remove(output) # zip command updates zipfiles by default
|
||||||
for f in iglob("Payload/**", recursive=True):
|
except FileNotFoundError:
|
||||||
try:
|
pass
|
||||||
zf.write(f)
|
|
||||||
except ValueError:
|
# don't zip hidden files to fix an installd error sometimes
|
||||||
weird += 1
|
# 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:
|
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:
|
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"
|
|
||||||
os.mkdir(DOT_PATH)
|
|
||||||
|
|
||||||
with zf.open("config.json") as f:
|
with zipfile.ZipFile(cyan) as zf:
|
||||||
config = json.load(f)
|
DOT_PATH = f"{tmpdir}/cyan-{ind}"
|
||||||
|
os.mkdir(DOT_PATH)
|
||||||
|
|
||||||
if "f" in config:
|
with zf.open("config.json") as f:
|
||||||
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
|
config = json.load(f)
|
||||||
zf.extractall(DOT_PATH, NAMES)
|
|
||||||
|
|
||||||
# ensure not None
|
if "f" in config:
|
||||||
args["f"] = args["f"] if args["f"] is not None else {}
|
NAMES = [n for n in zf.namelist() if n.startswith("inject/")]
|
||||||
for e in os.scandir(f"{DOT_PATH}/inject"):
|
zf.extractall(DOT_PATH, NAMES)
|
||||||
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():
|
# ensure not None
|
||||||
args[k] = v
|
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
|
||||||
|
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -4,10 +4,10 @@ from setuptools import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cyan",
|
name="cyan",
|
||||||
version="1.1.5",
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user