feat: app extension operations

This commit is contained in:
zx
2024-09-07 10:27:31 -04:00
parent 4a28b6d773
commit c310686840
5 changed files with 70 additions and 28 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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:
existed = False
for name in names:
if self.path in name: # i do this in `remove_encrypted_extensions()`
path = name
else:
path = f"{self.path}/{name}" path = f"{self.path}/{name}"
if not os.path.exists(path): if not os.path.exists(path):
return False continue
try: try:
shutil.rmtree(path) shutil.rmtree(path)
except NotADirectoryError: except NotADirectoryError:
os.remove(path) os.remove(path)
return True 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))

View File

@@ -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)