mirror of
https://github.com/NohamR/pyzule-rw.git
synced 2026-10-11 02:49:40 +00:00
feat: app extension operations
This commit is contained in:
@@ -17,6 +17,7 @@ more coming soon! i'm trying to copy pyzule in order to deprecate it in favor of
|
|||||||
- remove watch app
|
- remove watch app
|
||||||
- fakesign the output ipa/app
|
- fakesign the output ipa/app
|
||||||
- thin all binaries to arm64, it can LARGELY reduce app size sometimes!
|
- thin all binaries to arm64, it can LARGELY reduce app size sometimes!
|
||||||
|
- remove all app extensions (or just encrypted ones!)
|
||||||
|
|
||||||
## notes
|
## notes
|
||||||
|
|
||||||
@@ -38,9 +39,9 @@ to install or update cyan, just `pip install --force-reinstall git+https://githu
|
|||||||
|
|
||||||
[x] feat: fakesign
|
[x] feat: fakesign
|
||||||
|
|
||||||
[] feat: thin binaries
|
[x] feat: thin binaries
|
||||||
|
|
||||||
[] feat: plugin operations (-q, -e)
|
[x] feat: plugin operations (-q, -e)
|
||||||
|
|
||||||
[] feat: .cyan files (lol rip .pyzule files)
|
[] feat: .cyan files (lol rip .pyzule files)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ def main() -> None:
|
|||||||
"-q", "--thin", action="store_true",
|
"-q", "--thin", action="store_true",
|
||||||
help="thin all binaries to arm64, may largely reduce size"
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--compress", metavar="level", type=int, default=6,
|
"-c", "--compress", metavar="level", type=int, default=6,
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ def main(parser: ArgumentParser) -> None:
|
|||||||
OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False
|
OUTPUT_IS_IPA = True if args.o.endswith(".ipa") else False
|
||||||
|
|
||||||
with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM():
|
with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM():
|
||||||
app_path, plist_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
|
app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
|
||||||
app = tbhtypes.AppBundle(app_path, plist_path)
|
app = tbhtypes.AppBundle(app_path)
|
||||||
|
|
||||||
if app.executable.is_encrypted():
|
if app.executable.is_encrypted():
|
||||||
if args.ignore_encrypted:
|
if args.ignore_encrypted:
|
||||||
@@ -38,6 +38,13 @@ def main(parser: ArgumentParser) -> None:
|
|||||||
else:
|
else:
|
||||||
sys.exit("[!] main binary is encrypted; exiting")
|
sys.exit("[!] main binary is encrypted; exiting")
|
||||||
|
|
||||||
|
# this goes before injection,
|
||||||
|
# since user might inject their own extensions
|
||||||
|
if args.remove_extensions:
|
||||||
|
app.remove_all_extensions()
|
||||||
|
elif args.remove_encrypted:
|
||||||
|
app.remove_encrypted_extensions()
|
||||||
|
|
||||||
if args.f is not None:
|
if args.f is not None:
|
||||||
app.executable.inject(args.f, tmpdir)
|
app.executable.inject(args.f, tmpdir)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
|
|
||||||
from .executable import Executable
|
from .executable import Executable
|
||||||
from .plist import Plist
|
from .plist import Plist
|
||||||
|
|
||||||
class AppBundle:
|
class AppBundle:
|
||||||
def __init__(self, path: str, plist_path: str):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.plist = Plist(plist_path)
|
self.plist = Plist(f"{path}/Info.plist")
|
||||||
|
|
||||||
self.executable = Executable(
|
self.executable = Executable(
|
||||||
f"{path}/{self.plist['CFBundleExecutable']}",
|
f"{path}/{self.plist['CFBundleExecutable']}",
|
||||||
@@ -18,25 +18,29 @@ class AppBundle:
|
|||||||
|
|
||||||
self.cached_executables: Optional[list[str]] = None
|
self.cached_executables: Optional[list[str]] = None
|
||||||
|
|
||||||
def remove(self, name: str) -> bool:
|
def remove(self, *names: str) -> bool:
|
||||||
path = f"{self.path}/{name}"
|
existed = False
|
||||||
if not os.path.exists(path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
for name in names:
|
||||||
shutil.rmtree(path)
|
if self.path in name: # i do this in `remove_encrypted_extensions()`
|
||||||
except NotADirectoryError:
|
path = name
|
||||||
os.remove(path)
|
else:
|
||||||
|
path = f"{self.path}/{name}"
|
||||||
|
|
||||||
return True
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(path)
|
||||||
|
except NotADirectoryError:
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
existed = True
|
||||||
|
|
||||||
|
return existed
|
||||||
|
|
||||||
def remove_watch_apps(self) -> None:
|
def remove_watch_apps(self) -> None:
|
||||||
removed = False
|
if self.remove("Watch", "WatchKit", "com.apple.WatchPlaceholder"):
|
||||||
for name in ("Watch", "WatchKit", "com.apple.WatchPlaceholder"):
|
|
||||||
if self.remove(name):
|
|
||||||
removed = True
|
|
||||||
|
|
||||||
if removed:
|
|
||||||
print("[*] removed watch app")
|
print("[*] removed watch app")
|
||||||
else:
|
else:
|
||||||
print("[?] watch app not present")
|
print("[?] watch app not present")
|
||||||
@@ -48,7 +52,7 @@ class AppBundle:
|
|||||||
glob(f"{self.path}/**/*.framework", recursive=True)
|
glob(f"{self.path}/**/*.framework", recursive=True)
|
||||||
), []) # type: ignore
|
), []) # type: ignore
|
||||||
|
|
||||||
def mass_operate(self, op: str, func: str) -> None:
|
def mass_operate(self, op: str, func: Literal["fakesign", "thin"]) -> None:
|
||||||
# this works since we call this after injecting
|
# this works since we call this after injecting
|
||||||
if self.cached_executables is None:
|
if self.cached_executables is None:
|
||||||
self.cached_executables = self.get_executables()
|
self.cached_executables = self.get_executables()
|
||||||
@@ -76,3 +80,26 @@ class AppBundle:
|
|||||||
def thin_all(self) -> None:
|
def thin_all(self) -> None:
|
||||||
self.mass_operate("thinned", "thin")
|
self.mass_operate("thinned", "thin")
|
||||||
|
|
||||||
|
def remove_all_extensions(self) -> None:
|
||||||
|
if self.remove("Extensions", "PlugIns"):
|
||||||
|
print("[*] removed app extensions")
|
||||||
|
else:
|
||||||
|
print("[?] no app extensions")
|
||||||
|
|
||||||
|
def remove_encrypted_extensions(self) -> None:
|
||||||
|
removed: list[str] = []
|
||||||
|
|
||||||
|
# a singular * is used to not detect watch apps
|
||||||
|
for plugin in glob(
|
||||||
|
f"{self.path}/*/*.appex", recursive=True
|
||||||
|
):
|
||||||
|
bundle = AppBundle(plugin)
|
||||||
|
if bundle.executable.is_encrypted():
|
||||||
|
self.remove(plugin)
|
||||||
|
removed.append(os.path.basename(bundle.executable.path))
|
||||||
|
|
||||||
|
if len(removed) == 0:
|
||||||
|
print("[?] no encrypted plugins")
|
||||||
|
else:
|
||||||
|
print("[*] removed encrypted plugins:", ", ".join(removed))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from uuid import uuid4
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from glob import glob, iglob
|
from glob import glob, iglob
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
|
|
||||||
|
|
||||||
def validate_inputs(args: Namespace) -> Optional[str]:
|
def validate_inputs(args: Namespace) -> Optional[str]:
|
||||||
@@ -50,7 +49,7 @@ def validate_inputs(args: Namespace) -> Optional[str]:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]:
|
def get_app(path: str, tmpdir: str, is_ipa: bool) -> str:
|
||||||
payload = f"{tmpdir}/Payload"
|
payload = f"{tmpdir}/Payload"
|
||||||
|
|
||||||
if is_ipa:
|
if is_ipa:
|
||||||
@@ -67,7 +66,6 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]:
|
|||||||
|
|
||||||
ipa.extractall(tmpdir)
|
ipa.extractall(tmpdir)
|
||||||
app = glob(f"{payload}/*.app")[0]
|
app = glob(f"{payload}/*.app")[0]
|
||||||
plist = f"{app}/Info.plist"
|
|
||||||
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")
|
||||||
except zipfile.BadZipFile:
|
except zipfile.BadZipFile:
|
||||||
@@ -75,14 +73,14 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]:
|
|||||||
|
|
||||||
print("[*] extracted ipa")
|
print("[*] extracted ipa")
|
||||||
else:
|
else:
|
||||||
if not os.path.isfile((plist := f"{path}/Info.plist")):
|
if not os.path.isfile(f"{path}/Info.plist"):
|
||||||
sys.exit("[!] no Info.plist, invalid app")
|
sys.exit("[!] no Info.plist, invalid app")
|
||||||
|
|
||||||
print("[*] copying app..")
|
print("[*] copying app..")
|
||||||
shutil.copytree(path, (app := f"{payload}/{os.path.basename(path)}"))
|
shutil.copytree(path, (app := f"{payload}/{os.path.basename(path)}"))
|
||||||
print("[*] copied app")
|
print("[*] copied app")
|
||||||
|
|
||||||
return app, plist
|
return app
|
||||||
|
|
||||||
|
|
||||||
def get_tools_dir() -> tuple[str, str]:
|
def get_tools_dir() -> tuple[str, str]:
|
||||||
@@ -118,6 +116,7 @@ def delete_if_exists(path: str, bn: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
# damn it, literally EVERY FUCKING python version before 3.12 FUCKING SUCKS
|
# damn it, literally EVERY FUCKING python version before 3.12 FUCKING SUCKS
|
||||||
|
# no `delete` in `TemporaryDirectory` ?! GREAT !!!
|
||||||
def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
|
def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
|
||||||
t2 = f"{tmpdir}/{uuid4()}"
|
t2 = f"{tmpdir}/{uuid4()}"
|
||||||
os.mkdir(t2)
|
os.mkdir(t2)
|
||||||
|
|||||||
Reference in New Issue
Block a user