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

@@ -47,6 +47,14 @@ def main() -> None:
"-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"
)
parser.add_argument(
"-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
with TemporaryDirectory() as tmpdir, tbhtypes.LeavingCM():
app_path, plist_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
app = tbhtypes.AppBundle(app_path, plist_path)
app_path = tbhutils.get_app(args.i, tmpdir, INPUT_IS_IPA)
app = tbhtypes.AppBundle(app_path)
if app.executable.is_encrypted():
if args.ignore_encrypted:
@@ -38,6 +38,13 @@ def main(parser: ArgumentParser) -> None:
else:
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:
app.executable.inject(args.f, tmpdir)

View File

@@ -1,15 +1,15 @@
import os
import shutil
from glob import glob
from typing import Optional
from typing import Optional, Literal
from .executable import Executable
from .plist import Plist
class AppBundle:
def __init__(self, path: str, plist_path: str):
def __init__(self, path: str):
self.path = path
self.plist = Plist(plist_path)
self.plist = Plist(f"{path}/Info.plist")
self.executable = Executable(
f"{path}/{self.plist['CFBundleExecutable']}",
@@ -18,25 +18,29 @@ class AppBundle:
self.cached_executables: Optional[list[str]] = None
def remove(self, name: str) -> bool:
path = f"{self.path}/{name}"
if not os.path.exists(path):
return False
def remove(self, *names: str) -> bool:
existed = False
try:
shutil.rmtree(path)
except NotADirectoryError:
os.remove(path)
for name in names:
if self.path in name: # i do this in `remove_encrypted_extensions()`
path = name
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:
removed = False
for name in ("Watch", "WatchKit", "com.apple.WatchPlaceholder"):
if self.remove(name):
removed = True
if removed:
if self.remove("Watch", "WatchKit", "com.apple.WatchPlaceholder"):
print("[*] removed watch app")
else:
print("[?] watch app not present")
@@ -48,7 +52,7 @@ class AppBundle:
glob(f"{self.path}/**/*.framework", recursive=True)
), []) # 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
if self.cached_executables is None:
self.cached_executables = self.get_executables()
@@ -76,3 +80,26 @@ class AppBundle:
def thin_all(self) -> None:
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 glob import glob, iglob
from argparse import Namespace
from tempfile import TemporaryDirectory
def validate_inputs(args: Namespace) -> Optional[str]:
@@ -50,7 +49,7 @@ def validate_inputs(args: Namespace) -> Optional[str]:
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"
if is_ipa:
@@ -67,7 +66,6 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]:
ipa.extractall(tmpdir)
app = glob(f"{payload}/*.app")[0]
plist = f"{app}/Info.plist"
except (KeyError, IndexError):
sys.exit("[!] couldn't find either Payload or app folder, invalid ipa")
except zipfile.BadZipFile:
@@ -75,14 +73,14 @@ def get_app(path: str, tmpdir: str, is_ipa: bool) -> tuple[str, str]:
print("[*] extracted ipa")
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")
print("[*] copying app..")
shutil.copytree(path, (app := f"{payload}/{os.path.basename(path)}"))
print("[*] copied app")
return app, plist
return app
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
# no `delete` in `TemporaryDirectory` ?! GREAT !!!
def extract_deb(deb: str, tweaks: dict[str, str], tmpdir: str) -> None:
t2 = f"{tmpdir}/{uuid4()}"
os.mkdir(t2)