From b8d640132cb14b181363572f9d5c798191fdf20c Mon Sep 17 00:00:00 2001 From: Nandan Kumar Kashyap Date: Sun, 26 Oct 2025 00:30:32 +0530 Subject: [PATCH 1/8] Restructed codebase --- .gitignore | 218 +++++++++++++++++- requirements.txt | 10 +- src/patch_apk/__init__.py | 6 + src/patch_apk/config/__init__.py | 1 + src/patch_apk/config/constants.py | 7 + src/patch_apk/core/__init__.py | 6 + src/patch_apk/core/apk_builder.py | 41 ++++ src/patch_apk/core/apk_tool.py | 142 ++++++++++++ src/patch_apk/main.py | 86 +++++++ src/patch_apk/utils/__init__.py | 2 + src/patch_apk/utils/apk_detect_proguard.py | 15 ++ src/patch_apk/utils/cli_tools.py | 64 +++++ src/patch_apk/utils/copy_split_apks.py | 33 +++ .../patch_apk/utils/data}/patch-apk.keystore | Bin src/patch_apk/utils/dependencies.py | 29 +++ src/patch_apk/utils/disable_apk_split.py | 46 ++++ src/patch_apk/utils/fix_private_resources.py | 19 ++ src/patch_apk/utils/fix_resource_id.py | 109 +++++++++ src/patch_apk/utils/frida_objection.py | 78 +++++++ src/patch_apk/utils/get_apk_paths.py | 35 +++ src/patch_apk/utils/get_target_apk.py | 29 +++ src/patch_apk/utils/raw_re_replace.py | 22 ++ src/patch_apk/utils/remove_duplicate_style.py | 9 + src/patch_apk/utils/verify_package_name.py | 16 ++ 24 files changed, 1017 insertions(+), 6 deletions(-) create mode 100644 src/patch_apk/__init__.py create mode 100644 src/patch_apk/config/__init__.py create mode 100644 src/patch_apk/config/constants.py create mode 100644 src/patch_apk/core/__init__.py create mode 100644 src/patch_apk/core/apk_builder.py create mode 100644 src/patch_apk/core/apk_tool.py create mode 100644 src/patch_apk/main.py create mode 100644 src/patch_apk/utils/__init__.py create mode 100644 src/patch_apk/utils/apk_detect_proguard.py create mode 100644 src/patch_apk/utils/cli_tools.py create mode 100644 src/patch_apk/utils/copy_split_apks.py rename {data => src/patch_apk/utils/data}/patch-apk.keystore (100%) create mode 100644 src/patch_apk/utils/dependencies.py create mode 100644 src/patch_apk/utils/disable_apk_split.py create mode 100644 src/patch_apk/utils/fix_private_resources.py create mode 100644 src/patch_apk/utils/fix_resource_id.py create mode 100644 src/patch_apk/utils/frida_objection.py create mode 100644 src/patch_apk/utils/get_apk_paths.py create mode 100644 src/patch_apk/utils/get_target_apk.py create mode 100644 src/patch_apk/utils/raw_re_replace.py create mode 100644 src/patch_apk/utils/remove_duplicate_style.py create mode 100644 src/patch_apk/utils/verify_package_name.py diff --git a/.gitignore b/.gitignore index f7ae478..e15106e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,216 @@ -patch-apk-dbg.py -.DS_Store +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/requirements.txt b/requirements.txt index 2869b37..9d3863b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -progress -termcolor -objection==1.11.0 -packaging \ No newline at end of file +packaging==25.0 +-e git+https://github.com/Urten/patch-apk.git@399daf302355dcb8f34e15fbfe418a3899141d23#egg=patch_apk +progress==1.6.1 +setuptools==80.9.0 +termcolor==3.1.0 +wheel==0.45.1 diff --git a/src/patch_apk/__init__.py b/src/patch_apk/__init__.py new file mode 100644 index 0000000..456325e --- /dev/null +++ b/src/patch_apk/__init__.py @@ -0,0 +1,6 @@ +""" +patch-apk package. +A tool for patching and modifying Android APK files. +""" + +__version__ = '0.1.0' \ No newline at end of file diff --git a/src/patch_apk/config/__init__.py b/src/patch_apk/config/__init__.py new file mode 100644 index 0000000..fba82fe --- /dev/null +++ b/src/patch_apk/config/__init__.py @@ -0,0 +1 @@ +"""Config package for constants and configurations.""" \ No newline at end of file diff --git a/src/patch_apk/config/constants.py b/src/patch_apk/config/constants.py new file mode 100644 index 0000000..26978d1 --- /dev/null +++ b/src/patch_apk/config/constants.py @@ -0,0 +1,7 @@ +""" +Constants and configuration values for the patch-apk tool. +""" + +# Android related constants + +NULL_DECODED_DRAWABLE_COLOR = "#000000ff" \ No newline at end of file diff --git a/src/patch_apk/core/__init__.py b/src/patch_apk/core/__init__.py new file mode 100644 index 0000000..bc576e6 --- /dev/null +++ b/src/patch_apk/core/__init__.py @@ -0,0 +1,6 @@ +"""Core package for APK processing functionality.""" + +from .apk_tool import APKTool +from .apk_builder import APKBuilder + +__all__ = ["APKTool", "APKBuilder"] \ No newline at end of file diff --git a/src/patch_apk/core/apk_builder.py b/src/patch_apk/core/apk_builder.py new file mode 100644 index 0000000..b3e6a22 --- /dev/null +++ b/src/patch_apk/core/apk_builder.py @@ -0,0 +1,41 @@ +""" +APK building module responsible for rebuilding modified APK files. +""" +import os +import shutil + +# core imports +from .apk_tool import APKTool + +# utility imports + +from patch_apk.utils.cli_tools import verbosePrint, abort, assertSubprocessSuccessfulRun +from patch_apk.utils.fix_private_resources import fixPrivateResources + + +class APKBuilder: + """Handles APK building and rebuilding operations.""" + + @staticmethod + def build(baseapkdir): + # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761) + fixPrivateResources(baseapkdir) + + verbosePrint("[+] Rebuilding APK with apktool.") + result = APKTool.runApkTool(["b", baseapkdir]) + if result["returncode"] != 0: + abort("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") + + + @staticmethod + def signAndZipAlign(baseapkdir, baseapkfilename): + # Zip align the new APK + verbosePrint("[+] Zip aligning new APK.") + assertSubprocessSuccessfulRun(["zipalign", "-f", "4", "-p", os.path.join(baseapkdir, "dist", baseapkfilename), + os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk")]) + shutil.move(os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk"), os.path.join(baseapkdir, "dist", baseapkfilename)) + + # Sign the new APK + verbosePrint("[+] Signing new APK.") + apkpath = os.path.join(baseapkdir, "dist", baseapkfilename) + assertSubprocessSuccessfulRun(["objection", "signapk", apkpath]) \ No newline at end of file diff --git a/src/patch_apk/core/apk_tool.py b/src/patch_apk/core/apk_tool.py new file mode 100644 index 0000000..2bc6005 --- /dev/null +++ b/src/patch_apk/core/apk_tool.py @@ -0,0 +1,142 @@ +''' ApkTool related functions ''' + +import subprocess +import os +from progress.bar import Bar +from packaging.version import parse as parse_version + +# util imports + +from patch_apk.utils.raw_re_replace import rawREReplace +from patch_apk.utils.disable_apk_split import disableApkSplitting +from patch_apk.utils.remove_duplicate_style import hackRemoveDuplicateStyleEntries +from patch_apk.utils.fix_resource_id import fixPublicResourceIDs +from patch_apk.utils.cli_tools import abort, verbosePrint, warningPrint +from patch_apk.utils.apk_detect_proguard import detectProGuard +from patch_apk.utils.copy_split_apks import copySplitApkFiles + + +# core imports + +# from .apk_builder import APKBuilder # removed due to circular import + + +class APKTool: + + ''' + ApkTool class for handling APK files. + + This class provides a way to interface with apktool, a powerful tool for + decompiling and recompiling Android APK files. It also provides helper + functions for common operations when working with APK files. + + Attributes: + None + + Methods: + runApkTool(params): Run apktool with the given parameters. + getAPktoolVersion(): Get the installed version of apktool. + combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only): + Combine multiple split APKs into a single APK. + + Examples: + >>> APKTool.runApkTool(["d", "org.proxydroid.apk"]) + ''' + + + @staticmethod + def runApkTool(params): + exe = "apktool.bat" if os.name == "nt" else "apktool" + # Feed "\r\n" so apktool.bat's `pause` won’t block on Windows. + cp = subprocess.run( + [exe, *params], + input="\r\n", # Should be harmless on linux + text=True, + capture_output=True, + check=False, + ) + # Return a simple, uniform dict + return { + "returncode": cp.returncode, + "stdout": cp.stdout, + "stderr": cp.stderr, + "ok": (cp.returncode == 0), + } + + @staticmethod + def getApktoolVersion(): + commands = [["version"], ["v"], ["-version"], ["-v"]] + for cmd in commands: + try: + result = APKTool.runApkTool(cmd) + + if result["returncode"] != 0: + continue + version_output = result["stdout"].strip().split("\n")[0].strip() + version_str = version_output.split("-")[0].strip() + return parse_version(version_str) + except Exception as e: + continue + raise Exception("Error: Failed to get apktool version.") + + + + @staticmethod + def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only): + + from .apk_builder import APKBuilder + + warningPrint("[!] App bundle/split APK detected, rebuilding as a single APK.") + + # Extract the individual APKs + baseapkdir = os.path.join(tmppath, pkgname + "-base") + baseapkfilename = pkgname + "-base.apk" + splitapkpaths = [] + + bar = Bar('[+] Disassembling split APKs', max=len(localapks)) + verboseOutput = "" + + for apkpath in localapks: + verboseOutput += "\nExtracted: " + apkpath + bar.next() + apkdir = apkpath[:-4] + ret = APKTool.runApkTool(["d", apkpath, "-o", apkdir]) + if ret["returncode"] != 0: + abort("\nError: Failed to run 'apktool d " + apkpath + " -o " + apkdir + "'.\nRun with --debug-output for more information.") + + # Record the destination paths of all but the base APK + if not apkpath.endswith("base.apk"): + splitapkpaths.append(apkdir) + + # Check for ProGuard/AndResGuard - this might b0rk decompile/recompile + if detectProGuard(apkdir): + warningPrint("[!] WARNING: Detected ProGuard/AndResGuard, decompile/recompile may not succeed.\n") + + bar.finish() + + verbosePrint(verboseOutput) + + # Walk the extracted APK directories and copy files and directories to the base APK + print("[+] Rebuilding as a single APK") + copySplitApkFiles(baseapkdir, splitapkpaths) + + # Fix public resource identifiers + fixPublicResourceIDs(baseapkdir, splitapkpaths) + + # Hack: Delete duplicate style resource entries. + if not disableStylesHack: + hackRemoveDuplicateStyleEntries(baseapkdir) + + #Disable APK splitting in the base AndroidManifest.xml file + disableApkSplitting(baseapkdir) + + # Fix apktool bug where ampersands are improperly escaped: https://github.com/iBotPeaches/Apktool/issues/2703 + verbosePrint("[+] Fixing any improperly escaped ampersands.") + rawREReplace(os.path.join(baseapkdir, "res", "values", "strings.xml"), r'(&)([^;])', r'\1;\2') + + # Rebuild the base APK + APKBuilder.build(baseapkdir) + + # Return the new APK path + return os.path.join(baseapkdir, "dist", baseapkfilename) + diff --git a/src/patch_apk/main.py b/src/patch_apk/main.py new file mode 100644 index 0000000..7ac569f --- /dev/null +++ b/src/patch_apk/main.py @@ -0,0 +1,86 @@ +""" +Main entry point for the patch-apk tool. +""" +import os +import shutil +import subprocess +import tempfile + +# core imports + +from patch_apk.core.apk_tool import APKTool + +# utility imports + +from patch_apk.utils.cli_tools import getArgs, warningPrint, assertSubprocessSuccessfulRun +from patch_apk.utils.dependencies import checkDependencies +from patch_apk.utils.frida_objection import fixAPKBeforeObjection +from patch_apk.utils.get_target_apk import getTargetAPK +from patch_apk.utils.get_apk_paths import getAPKPathsForPackage +from patch_apk.utils.verify_package_name import verifyPackageName + +def main(): + # Grab argz + args = getArgs() + + # Check that dependencies are available + checkDependencies(args.extract_only) + + # Warn for unexpected version + apktoolVersion = APKTool.getApktoolVersion() + print(f"Using apktool v{apktoolVersion}") + + # Verify the package name and ensure it's installed (also supports partial package names) + pkgname = verifyPackageName(args.pkgname) + + # Get the APK path(s) from the device + current_user, apkpaths = getAPKPathsForPackage(pkgname) + + # Create a temp directory to work from + with tempfile.TemporaryDirectory() as tmppath: + # Get the APK to patch. Combine app bundles/split APKs into a single APK. + apkfile = getTargetAPK(pkgname, apkpaths, tmppath, args.disable_styles_hack, args.extract_only) + + # Save the APK if requested + if args.save_apk is not None or args.extract_only: + targetName = args.save_apk if args.save_apk is not None else pkgname + ".apk" # type: ignore + print("[+] Saving a copy of the APK to " + targetName) + shutil.copy(apkfile, targetName) + + if args.extract_only: + os.remove(apkfile) + return + + # Before patching with objection, add INTERNET permission if not already present, and set extractNativeLibs to true + fixAPKBeforeObjection(apkfile, not args.no_enable_user_certs) + + # Patch the target APK with objection + print("[+] Patching " + apkfile.split(os.sep)[-1] + " with objection.") + warningPrint("[!] The application will be patched with Frida 16.7.19. See https://github.com/sensepost/objection/issues/737 for more information.") + if subprocess.run(["objection", "patchapk", "-V", "16.7.19", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], capture_output=True).returncode != 0: + print("[+] Objection patching failed, trying alternative approach") + warningPrint("[!] If you get an error, the application might not have a launchable activity") + + # Try without --skip-resources, since objection potentially wasn't able to identify the starting activity + # There could have been another reason for the failure, but it's a sensible fallback + # Another reason could be a missing INTERNET permission + assertSubprocessSuccessfulRun(["objection", "patchapk","-V", "16.7.19", "--ignore-nativelibs", "-s", apkfile]) + + os.remove(apkfile) + shutil.move(apkfile[:-4] + ".objection.apk", apkfile) + + # Uninstall the original package from the device + print(f"[+] Uninstalling the original package from the device. (user: {current_user})") + assertSubprocessSuccessfulRun(["adb", "uninstall", "--user", current_user, pkgname]) + + # Install the patched APK + print(f"[+] Installing the patched APK to the device. (user: {current_user})") + assertSubprocessSuccessfulRun(["adb", "install", "--user", current_user, apkfile]) + + + # Done + print("[+] Done") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/patch_apk/utils/__init__.py b/src/patch_apk/utils/__init__.py new file mode 100644 index 0000000..25cbd79 --- /dev/null +++ b/src/patch_apk/utils/__init__.py @@ -0,0 +1,2 @@ +"""Utils package for helper functions and utilities.""" + diff --git a/src/patch_apk/utils/apk_detect_proguard.py b/src/patch_apk/utils/apk_detect_proguard.py new file mode 100644 index 0000000..1c208c4 --- /dev/null +++ b/src/patch_apk/utils/apk_detect_proguard.py @@ -0,0 +1,15 @@ +import os + +#################### +# Attempt to detect ProGuard/AndResGuard. +#################### +def detectProGuard(extractedPath): + if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "proguard")): + return True + if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF")): + fh = open(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF")) + d = fh.read() + fh.close() + if "proguard" in d.lower(): + return True + return False \ No newline at end of file diff --git a/src/patch_apk/utils/cli_tools.py b/src/patch_apk/utils/cli_tools.py new file mode 100644 index 0000000..4ef6598 --- /dev/null +++ b/src/patch_apk/utils/cli_tools.py @@ -0,0 +1,64 @@ +""" +Command-line interface argument handling. +""" + +import argparse +import sys +from termcolor import colored +import subprocess + +def getArgs(): + # Only parse args once + if not hasattr(getArgs, "parsed_args"): + # Parse the command line + parser = argparse.ArgumentParser( + description="patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs." + ) + parser.add_argument("--no-enable-user-certs", help="Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.", action="store_true") + parser.add_argument("--save-apk", help="Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.") + parser.add_argument("--extract-only", help="Disable including objection and pushing modified APK to device.", action="store_true") + parser.add_argument("--disable-styles-hack", help="Disable the styles hack that removes duplicate entries from res/values/styles.xml.", action="store_true") + parser.add_argument("--debug-output", help="Enable debug output.", action="store_true") + parser.add_argument("-v", "--verbose", help="Enable verbose output.", action="store_true") + parser.add_argument("pkgname", help="The name, or partial name, of the package to patch (e.g. com.foo.bar).") + + # Store the parsed args + getArgs.parsed_args = parser.parse_args() + + # Return the parsed command line args + return getArgs.parsed_args + +def abort(msg): + print(colored(msg, "red")) + sys.exit(1) + + +def verbosePrint(msg): + if getArgs().verbose: + for line in msg.split("\n"): + print(colored(" " + line, "light_grey")) + + +def dbgPrint(msg): + if getArgs().debug_output: + print(msg) + +#################### +# Warning print +#################### +def warningPrint(msg): + print(colored(msg, "yellow")) + + + +def getStdout(): + if getArgs().debug_output: + return None + else: + return subprocess.DEVNULL + + + +def assertSubprocessSuccessfulRun(args): + if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0: + abort(f"Error: Failed to run {' '.join(args)}.\nRun with --debug-output for more information.") \ No newline at end of file diff --git a/src/patch_apk/utils/copy_split_apks.py b/src/patch_apk/utils/copy_split_apks.py new file mode 100644 index 0000000..bb8768e --- /dev/null +++ b/src/patch_apk/utils/copy_split_apks.py @@ -0,0 +1,33 @@ +import os +from patch_apk.utils.cli_tools import dbgPrint +import shutil + +def copySplitApkFiles(baseapkdir, splitapkpaths): + for apkdir in splitapkpaths: + for (root, dirs, files) in os.walk(apkdir): + # Skip the original files directory + if not root.startswith(os.path.join(apkdir, "original")): + # Create any missing directories + for d in dirs: + # Translate directory path to base APK path and create the directory if it doesn't exist + p = baseapkdir + os.path.join(root, d)[len(apkdir):] + if not os.path.exists(p): + dbgPrint("[+] Creating directory in base APK: " + p[len(baseapkdir):]) + os.mkdir(p) + + # Copy files into the base APK + for f in files: + # Skip the AndroidManifest.xml and apktool.yml in the APK root directory + if apkdir == root and (f == "AndroidManifest.xml" or f == "apktool.yml"): + continue + + # Translate path to base APK + p = baseapkdir + os.path.join(root, f)[len(apkdir):] + + # Copy files into the base APK, except for XML files in the res directory + if f.lower().endswith(".xml") and p.startswith(os.path.join(baseapkdir, "res")): + continue + dbgPrint("[+] Moving file to base APK: " + p[len(baseapkdir):]) + shutil.move(os.path.join(root, f), p) + + diff --git a/data/patch-apk.keystore b/src/patch_apk/utils/data/patch-apk.keystore similarity index 100% rename from data/patch-apk.keystore rename to src/patch_apk/utils/data/patch-apk.keystore diff --git a/src/patch_apk/utils/dependencies.py b/src/patch_apk/utils/dependencies.py new file mode 100644 index 0000000..1d19db3 --- /dev/null +++ b/src/patch_apk/utils/dependencies.py @@ -0,0 +1,29 @@ +import subprocess +import os +import shutil +from patch_apk.utils.cli_tools import abort + +def checkDependencies(extract_only): + deps = ["adb", "apktool", "aapt"] + + if not extract_only: + deps += ["objection", "zipalign", "apksigner"] + + missing = [] + for dep in deps: + if shutil.which(dep) is None: + missing.append(dep) + if len(missing) > 0: + abort("Error, missing dependencies, ensure the following commands are available on the PATH: " + (", ".join(missing))) + + # Verify that an Android device is connected + proc = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE) + if proc.returncode != 0: + abort("Error: Failed to run 'adb devices'.") + deviceOut = proc.stdout.decode("utf-8") + if len(deviceOut.strip().split(os.linesep)) == 1: + abort("Error, no Android device connected (\"adb devices\"), connect a device first.") + + # Check that the included keystore exists + if not os.path.exists(os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore"))): + abort("Error, the keystore was not found at " + os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) + ", please clone the repository or get the keystore file and place it at this location.") \ No newline at end of file diff --git a/src/patch_apk/utils/disable_apk_split.py b/src/patch_apk/utils/disable_apk_split.py new file mode 100644 index 0000000..7182e60 --- /dev/null +++ b/src/patch_apk/utils/disable_apk_split.py @@ -0,0 +1,46 @@ +from patch_apk.utils.cli_tools import verbosePrint +import os +import xml.etree.ElementTree + +def disableApkSplitting(baseapkdir): + verbosePrint("[+] Disabling APK splitting in AndroidManifest.xml of base APK.") + + # Load AndroidManifest.xml + tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "AndroidManifest.xml")) + + # Register the namespaces and get the prefix for the "android" namespace + namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])]) # pyright: ignore[reportArgumentType] + for ns in namespaces: + xml.etree.ElementTree.register_namespace(ns, namespaces[ns]) + ns = "{" + namespaces["android"] + "}" + + # Disable APK splitting + appEl = None + elsToRemove = [] + for el in tree.iter(): + if el.tag == "application": + appEl = el + if ns + "isSplitRequired" in el.attrib: + del el.attrib[ns + "isSplitRequired"] + if ns + "extractNativeLibs" in el.attrib: + el.attrib[ns + "extractNativeLibs"] = "true" + elif appEl is not None and el.tag == "meta-data": + if ns + "name" in el.attrib: + if el.attrib[ns + "name"] == "com.android.vending.splits.required": + elsToRemove.append(el) + elif el.attrib[ns + "name"] == "com.android.vending.splits": + elsToRemove.append(el) + for el in elsToRemove: + appEl.remove(el) + + # Clean up tag + root = tree.getroot() + if ns + "isSplitRequired" in root.attrib: + del root.attrib[ns + "isSplitRequired"] + if ns + "requiredSplitTypes" in root.attrib: + del root.attrib[ns + "requiredSplitTypes"] + if ns + "splitTypes" in root.attrib: + del root.attrib[ns + "splitTypes"] + + # Save the updated AndroidManifest.xml + tree.write(os.path.join(baseapkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True) \ No newline at end of file diff --git a/src/patch_apk/utils/fix_private_resources.py b/src/patch_apk/utils/fix_private_resources.py new file mode 100644 index 0000000..fb7d4cb --- /dev/null +++ b/src/patch_apk/utils/fix_private_resources.py @@ -0,0 +1,19 @@ +import os +from patch_apk.utils.cli_tools import verbosePrint +from patch_apk.utils.raw_re_replace import rawREReplace + + +#################### +# Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761) +#################### +def fixPrivateResources(baseapkdir): + + verbosePrint("[+] Forcing all private resources to be public") + updated = 0 + for (root, dirs, files) in os.walk(os.path.join(baseapkdir, "res")): + for f in files: + if f.lower().endswith(".xml"): + rawREReplace(os.path.join(root, f), '@android', '@*android') + updated += 1 + if updated > 0: + verbosePrint("[+] Updated " + str(updated) + " private resources before building APK.") \ No newline at end of file diff --git a/src/patch_apk/utils/fix_resource_id.py b/src/patch_apk/utils/fix_resource_id.py new file mode 100644 index 0000000..11965b5 --- /dev/null +++ b/src/patch_apk/utils/fix_resource_id.py @@ -0,0 +1,109 @@ +import os +import xml.etree.ElementTree +from patch_apk.utils.cli_tools import verbosePrint, dbgPrint +from patch_apk.config.constants import NULL_DECODED_DRAWABLE_COLOR + + +def fixPublicResourceIDs(baseapkdir, splitapkpaths): + # Bail if the base APK does not have a public.xml + if not os.path.exists(os.path.join(baseapkdir, "res", "values", "public.xml")): + return + verbosePrint("[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.") + + # Mappings of resource IDs and names + idToDummyName = {} + dummyNameToRealName = {} + + # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to. + # Load these into the lookup tables ready to resolve the real resource names from + # the split APKs in step 2 below. + baseXmlTree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "res", "values", "public.xml")) + for el in baseXmlTree.getroot(): + if "name" in el.attrib and "id" in el.attrib: + if el.attrib["name"].startswith("APKTOOL_DUMMY_") and el.attrib["name"] not in idToDummyName: + idToDummyName[el.attrib["id"]] = el.attrib["name"] + dummyNameToRealName[el.attrib["name"]] = None + verbosePrint("[+] Resolving " + str(len(idToDummyName)) + " resource identifiers.") + + # Step 2) Parse the public.xml file from each split APK in search of resource IDs matching + # those loaded during step 1. Each match gives the true resource name allowing us to + # replace all APKTOOL_DUMMY_XXX resource names with the true resource names back in + # the base APK. + found = 0 + for splitdir in splitapkpaths: + if os.path.exists(os.path.join(splitdir, "res", "values", "public.xml")): + tree = xml.etree.ElementTree.parse(os.path.join(splitdir, "res", "values", "public.xml")) + for el in tree.getroot(): + if "name" in el.attrib and "id" in el.attrib: + if el.attrib["id"] in idToDummyName: + dummyNameToRealName[idToDummyName[el.attrib["id"]]] = el.attrib["name"] + found += 1 + verbosePrint("[+] Located " + str(found) + " true resource names.") + + # Step 3) Update the base APK to replace all APKTOOL_DUMMY_XXX resource names with the true + # resource name. + updated = 0 + for el in baseXmlTree.getroot(): + if "name" in el.attrib and "id" in el.attrib: + if el.attrib["name"] in dummyNameToRealName and dummyNameToRealName[el.attrib["name"]] is not None: + el.attrib["name"] = dummyNameToRealName[el.attrib["name"]] + updated += 1 + baseXmlTree.write(os.path.join(baseapkdir, "res", "values", "public.xml"), encoding="utf-8", xml_declaration=True) + verbosePrint("[+] Updated " + str(updated) + " dummy resource names with true names in the base APK.") + + # Step 4) Find all references to APKTOOL_DUMMY_XXX resources within other XML resource files + # in the base APK and update them to refer to the true resource name. + updated = 0 + for (root, dirs, files) in os.walk(os.path.join(baseapkdir, "res")): + for f in files: + if f.lower().endswith(".xml"): + try: + # Load the XML + xmlPath = os.path.join(root, f) + dbgPrint("[~] Parsing " + xmlPath) + tree = xml.etree.ElementTree.parse(xmlPath) + + # Register the namespaces and get the prefix for the "android" namespace + namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])]) + for ns in namespaces: + xml.etree.ElementTree.register_namespace(ns, namespaces[ns]) + ns = "{" + namespaces["android"] + "}" # type: ignore + + # Update references to APKTOOL_DUMMY_XXX resources + changed = False + for el in tree.iter(): + # Check for references to APKTOOL_DUMMY_XXX resources in attributes of this element + for attr in el.attrib: + val = el.attrib[attr] + if val.startswith("@") and "/" in val and val.split("/")[1].startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val.split("/")[1]] is not None: + el.attrib[attr] = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]] + updated += 1 + changed = True + elif val.startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val] is not None: + el.attrib[attr] = dummyNameToRealName[val] + updated += 1 + changed = True + + if changed: + dbgPrint("[~] Patching dummy apktool attribute \"" + attr + "\" value \"" + val + "\"" + (" -> \"" + el.attrib[attr] + "\"" if val != el.attrib[attr] else "") + " (" + str(updated) + ")") + + # Fix for untracked bug where drawables are decoded without drawable values (@null) + if f == "drawables.xml" and attr == "name" and el.text is None: + dbgPrint("[~] Patching null decoded drawable \"" + el.attrib[attr] + "\" (" + str(updated) + ")") + el.text = NULL_DECODED_DRAWABLE_COLOR + + # Check for references to APKTOOL_DUMMY_XXX resources in the element text + val = el.text + if val is not None and val.startswith("@") and "/" in val and val.split("/")[1].startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val.split("/")[1]] is not None: + el.text = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]] + updated += 1 + changed = True + dbgPrint("[~] Patching dummy apktool element \"" + el.get('name', el.tag) + "\" value \"" + val + (" -> \"" + el.text + "\"" if val != el.text else "") + str(updated) + ")") + + # Save the file if it was updated + if changed: + dbgPrint("[+] Writing patched " + f) + tree.write(os.path.join(root, f), encoding="utf-8", xml_declaration=True) + except xml.etree.ElementTree.ParseError: + print("[-] XML parse error in " + os.path.join(root, f) + ", skipping.") + verbosePrint("[+] Updated " + str(updated) + " references to dummy resource names in the base APK.") \ No newline at end of file diff --git a/src/patch_apk/utils/frida_objection.py b/src/patch_apk/utils/frida_objection.py new file mode 100644 index 0000000..9584361 --- /dev/null +++ b/src/patch_apk/utils/frida_objection.py @@ -0,0 +1,78 @@ +import os +import tempfile +import shutil +import xml.etree.ElementTree + +# core imports + +from patch_apk.core.apk_tool import APKTool + +# utility imports + +from patch_apk.utils.cli_tools import abort + +def fixAPKBeforeObjection(apkfile, fix_network_security_config): + print("[+] Prepping AndroidManifest.xml") + with tempfile.TemporaryDirectory() as tmppath: + apkdir = os.path.join(tmppath, "apk") + ret = APKTool.runApkTool(["d", apkfile, "-o", apkdir]) + if ret["returncode"] != 0: + abort("Error: Failed to run 'apktool d " + apkfile + " -o " + apkdir + "'.\nRun with --debug-output for more information.") + + # Load AndroidManifest.xml + manifestPath = os.path.join(apkdir, "AndroidManifest.xml") + tree = xml.etree.ElementTree.parse(manifestPath) + + # Register the namespaces and get the prefix for the "android" namespace + namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(manifestPath, events=["start-ns"])]) + for ns in namespaces: + xml.etree.ElementTree.register_namespace(ns, namespaces[ns]) + ns = "{" + namespaces["android"] + "}" + + # Ensure INTERNET permission is present + hasInternetPermission = False + for el in tree.getroot(): + if el.tag == "uses-permission" and ns + "name" in el.attrib: + if el.attrib[ns + "name"] == "android.permission.INTERNET": + hasInternetPermission = True + break + if not hasInternetPermission: + print("[+] Adding android.permission.INTERNET to AndroidManifest.xml") + usesPermissionEl = xml.etree.ElementTree.Element("uses-permission") + usesPermissionEl.attrib[ns + "name"] = "android.permission.INTERNET" + tree.getroot().insert(0, usesPermissionEl) + + # Set extractNativeLibs to true + appEl = tree.find(".//application") + if appEl is not None: + print("[+] \tSetting extractNativeLibs to true") + appEl.attrib[ns + "extractNativeLibs"] = "true" + + + if fix_network_security_config: + print("[+] \tEnabling support for user-installed CA certificates.") + + # Add networkSecurityConfig + for el in tree.findall("application"): + el.attrib[ns + "networkSecurityConfig"] = "@xml/network_security_config" + + # Create a network security config file + fh = open(os.path.join(apkdir, "res", "xml", "network_security_config.xml"), "wb") + fh.write("".encode("utf-8")) + fh.close() + + # Save the updated AndroidManifest.xml + tree.write(manifestPath, encoding="utf-8", xml_declaration=True) + + # Rebuild apk file + result = APKTool.runApkTool(["b", apkdir]) + if result["returncode"] != 0: + abort("Error: Failed to run 'apktool b " + apkdir + "'.\nRun with --debug-output for more information.") + + + # Move rebuilt APK back to original location + rebuilt_apk = os.path.join(apkdir, "dist", os.path.basename(apkfile)) + if os.path.exists(rebuilt_apk): + shutil.move(rebuilt_apk, apkfile) + else: + abort("Error: Rebuilt APK not found.") \ No newline at end of file diff --git a/src/patch_apk/utils/get_apk_paths.py b/src/patch_apk/utils/get_apk_paths.py new file mode 100644 index 0000000..b04b451 --- /dev/null +++ b/src/patch_apk/utils/get_apk_paths.py @@ -0,0 +1,35 @@ +import subprocess +import os +import re +from patch_apk.utils.cli_tools import abort, verbosePrint, warningPrint + +def getAPKPathsForPackage(pkgname, current_user = "0", users_to_try = None): + print(f"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}") + paths = [] + proc = subprocess.run(["adb", "shell", "pm", "path", "--user", current_user, pkgname], stdout=subprocess.PIPE) + if proc.returncode != 0: + if not users_to_try: + proc = subprocess.run(["adb", "shell", "pm", "list", "users"], stdout=subprocess.PIPE) + out = proc.stdout.decode("utf-8") + + pattern = r'UserInfo{(\d+):' + users_to_try = re.findall(pattern, out) + + if current_user in users_to_try: + users_to_try.remove(current_user) + + if len(users_to_try) > 0: + warningPrint(f"[!] Package not found for user {current_user}, trying next user") + return getAPKPathsForPackage(pkgname, users_to_try[0], users_to_try) + else: + abort("Error: Failed to run 'adb shell pm path " + pkgname + "'.") + + out = proc.stdout.decode("utf-8") + + for line in out.split(os.linesep): + if line.startswith("package:"): + line = line[8:].strip() + verbosePrint("[+] APK path: " + line) + paths.append(line) + + return current_user, paths \ No newline at end of file diff --git a/src/patch_apk/utils/get_target_apk.py b/src/patch_apk/utils/get_target_apk.py new file mode 100644 index 0000000..99df08c --- /dev/null +++ b/src/patch_apk/utils/get_target_apk.py @@ -0,0 +1,29 @@ +import os +from patch_apk.utils.cli_tools import verbosePrint, assertSubprocessSuccessfulRun +from progress.bar import Bar +from patch_apk.core.apk_tool import APKTool + +def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only): + # Pull the APKs from the device + + bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths)) + verboseOutput = "" + + localapks = [] + for remotepath in apkpaths: + baseapkname = remotepath.split('/')[-1] + localapks.append(os.path.join(tmppath, pkgname + "-" + baseapkname)) + verboseOutput += f"[+] Pulled: {pkgname}-{baseapkname}" + bar.next() + # assertSubprocessSuccessfulRun(["adb", "pull", remotepath, localapks[-1]]) + assertSubprocessSuccessfulRun(["adb", "pull", remotepath, localapks[-1]] ) + + bar.finish() + verbosePrint(verboseOutput.rstrip()) + + # Return the target APK path + if len(localapks) == 1: + return localapks[0] + else: + # Combine split APKs + return APKTool.combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only) \ No newline at end of file diff --git a/src/patch_apk/utils/raw_re_replace.py b/src/patch_apk/utils/raw_re_replace.py new file mode 100644 index 0000000..55c901e --- /dev/null +++ b/src/patch_apk/utils/raw_re_replace.py @@ -0,0 +1,22 @@ +""" +File operation utilities. +""" +from patch_apk.utils.cli_tools import abort, dbgPrint +import os +import re +import xml.etree.ElementTree +from patch_apk.utils.cli_tools import verbosePrint + + +def rawREReplace(path, pattern, replacement): + if os.path.exists(path): + contents = "" + with open(path, 'r') as file: + contents = file.read() + newContents = re.sub(pattern, replacement, contents) + if (contents != newContents): + dbgPrint("[~] Patching " + path) + with open(path, 'w') as file: + file.write(newContents) + else: + abort("\nError: Failed to find file at " + path + " for pattern replacement") \ No newline at end of file diff --git a/src/patch_apk/utils/remove_duplicate_style.py b/src/patch_apk/utils/remove_duplicate_style.py new file mode 100644 index 0000000..6787160 --- /dev/null +++ b/src/patch_apk/utils/remove_duplicate_style.py @@ -0,0 +1,9 @@ +from patch_apk.utils.cli_tools import abort, getStdout, verbosePrint +from patch_apk.utils.raw_re_replace import rawREReplace +import os + + +def hackRemoveDuplicateStyleEntries(baseapkdir): + # Bail if there is no styles.xml + if not os.path.exists(os.path.join(baseapkdir, "res", "values", "styles.xml")): + return \ No newline at end of file diff --git a/src/patch_apk/utils/verify_package_name.py b/src/patch_apk/utils/verify_package_name.py new file mode 100644 index 0000000..73c7517 --- /dev/null +++ b/src/patch_apk/utils/verify_package_name.py @@ -0,0 +1,16 @@ +import subprocess +from patch_apk.utils.cli_tools import abort +import os + +def verifyPackageName(pkgname): + # Get a list of installed packages matching the given name + packages = [] + proc = subprocess.run(["adb", "shell", "pm", "list", "packages"], stdout=subprocess.PIPE) + if proc.returncode != 0: + abort("Error: Failed to run 'adb shell pm list packages'.") + out = proc.stdout.decode("utf-8") + for line in out.split(os.linesep): + if line.startswith("package:"): + line = line[8:].strip() + if pkgname.lower() in line.lower(): + packages.append(line) \ No newline at end of file From 455361a0e30dfd3df8d96ede0b7379e859120f01 Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 11:44:16 +0530 Subject: [PATCH 2/8] minor fixes --- .vscode/PythonImportHelper-v2-Completion.json | 1632 +++++++++++++++++ README.md | 33 +- README_PROJECT_LAYOUT.md | 50 + data/patch-apk.keystore | Bin 0 -> 2533 bytes requirements.txt | Bin 183 -> 376 bytes setup.py | 53 + .../PythonImportHelper-v2-Completion.json | 681 +++++++ src/patch_apk/utils/verify_package_name.py | 24 +- 8 files changed, 2466 insertions(+), 7 deletions(-) create mode 100644 .vscode/PythonImportHelper-v2-Completion.json create mode 100644 README_PROJECT_LAYOUT.md create mode 100644 data/patch-apk.keystore create mode 100644 setup.py create mode 100644 src/.vscode/PythonImportHelper-v2-Completion.json diff --git a/.vscode/PythonImportHelper-v2-Completion.json b/.vscode/PythonImportHelper-v2-Completion.json new file mode 100644 index 0000000..b1da922 --- /dev/null +++ b/.vscode/PythonImportHelper-v2-Completion.json @@ -0,0 +1,1632 @@ +[ + { + "label": "os", + "kind": 6, + "isExtraImport": true, + "importPath": "os", + "description": "os", + "detail": "os", + "documentation": {} + }, + { + "label": "shutil", + "kind": 6, + "isExtraImport": true, + "importPath": "shutil", + "description": "shutil", + "detail": "shutil", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getArgs", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getArgs", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "importPath": "patch_apk.utils.fix_private_resources", + "description": "patch_apk.utils.fix_private_resources", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "importPath": "patch_apk.utils.fix_private_resources", + "description": "patch_apk.utils.fix_private_resources", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "subprocess", + "kind": 6, + "isExtraImport": true, + "importPath": "subprocess", + "description": "subprocess", + "detail": "subprocess", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "parse", + "importPath": "packaging.version", + "description": "packaging.version", + "isExtraImport": true, + "detail": "packaging.version", + "documentation": {} + }, + { + "label": "parse", + "importPath": "packaging.version", + "description": "packaging.version", + "isExtraImport": true, + "detail": "packaging.version", + "documentation": {} + }, + { + "label": "parse", + "importPath": "packaging.version", + "description": "packaging.version", + "isExtraImport": true, + "detail": "packaging.version", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "importPath": "patch_apk.utils.disable_apk_split", + "description": "patch_apk.utils.disable_apk_split", + "isExtraImport": true, + "detail": "patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "importPath": "patch_apk.utils.disable_apk_split", + "description": "patch_apk.utils.disable_apk_split", + "isExtraImport": true, + "detail": "patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "importPath": "patch_apk.utils.remove_duplicate_style", + "description": "patch_apk.utils.remove_duplicate_style", + "isExtraImport": true, + "detail": "patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "importPath": "patch_apk.utils.remove_duplicate_style", + "description": "patch_apk.utils.remove_duplicate_style", + "isExtraImport": true, + "detail": "patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "importPath": "patch_apk.utils.fix_resource_id", + "description": "patch_apk.utils.fix_resource_id", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "importPath": "patch_apk.utils.fix_resource_id", + "description": "patch_apk.utils.fix_resource_id", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "detectProGuard", + "importPath": "patch_apk.utils.apk_detect_proguard", + "description": "patch_apk.utils.apk_detect_proguard", + "isExtraImport": true, + "detail": "patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "detectProGuard", + "importPath": "patch_apk.utils.apk_detect_proguard", + "description": "patch_apk.utils.apk_detect_proguard", + "isExtraImport": true, + "detail": "patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "importPath": "patch_apk.utils.copy_split_apks", + "description": "patch_apk.utils.copy_split_apks", + "isExtraImport": true, + "detail": "patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "importPath": "patch_apk.utils.copy_split_apks", + "description": "patch_apk.utils.copy_split_apks", + "isExtraImport": true, + "detail": "patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "argparse", + "kind": 6, + "isExtraImport": true, + "importPath": "argparse", + "description": "argparse", + "detail": "argparse", + "documentation": {} + }, + { + "label": "sys", + "kind": 6, + "isExtraImport": true, + "importPath": "sys", + "description": "sys", + "detail": "sys", + "documentation": {} + }, + { + "label": "colored", + "importPath": "termcolor", + "description": "termcolor", + "isExtraImport": true, + "detail": "termcolor", + "documentation": {} + }, + { + "label": "colored", + "importPath": "termcolor", + "description": "termcolor", + "isExtraImport": true, + "detail": "termcolor", + "documentation": {} + }, + { + "label": "colored", + "importPath": "termcolor", + "description": "termcolor", + "isExtraImport": true, + "detail": "termcolor", + "documentation": {} + }, + { + "label": "xml.etree.ElementTree", + "kind": 6, + "isExtraImport": true, + "importPath": "xml.etree.ElementTree", + "description": "xml.etree.ElementTree", + "detail": "xml.etree.ElementTree", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "importPath": "patch_apk.config.constants", + "description": "patch_apk.config.constants", + "isExtraImport": true, + "detail": "patch_apk.config.constants", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "importPath": "patch_apk.config.constants", + "description": "patch_apk.config.constants", + "isExtraImport": true, + "detail": "patch_apk.config.constants", + "documentation": {} + }, + { + "label": "tempfile", + "kind": 6, + "isExtraImport": true, + "importPath": "tempfile", + "description": "tempfile", + "detail": "tempfile", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "re", + "kind": 6, + "isExtraImport": true, + "importPath": "re", + "description": "re", + "detail": "re", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "core.apk_tool", + "description": "core.apk_tool", + "isExtraImport": true, + "detail": "core.apk_tool", + "documentation": {} + }, + { + "label": "checkDependencies", + "importPath": "patch_apk.utils.dependencies", + "description": "patch_apk.utils.dependencies", + "isExtraImport": true, + "detail": "patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "checkDependencies", + "importPath": "patch_apk.utils.dependencies", + "description": "patch_apk.utils.dependencies", + "isExtraImport": true, + "detail": "patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "importPath": "patch_apk.utils.frida_objection", + "description": "patch_apk.utils.frida_objection", + "isExtraImport": true, + "detail": "patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "importPath": "patch_apk.utils.frida_objection", + "description": "patch_apk.utils.frida_objection", + "isExtraImport": true, + "detail": "patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "getTargetAPK", + "importPath": "patch_apk.utils.get_target_apk", + "description": "patch_apk.utils.get_target_apk", + "isExtraImport": true, + "detail": "patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "getTargetAPK", + "importPath": "patch_apk.utils.get_target_apk", + "description": "patch_apk.utils.get_target_apk", + "isExtraImport": true, + "detail": "patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "importPath": "patch_apk.utils.get_apk_paths", + "description": "patch_apk.utils.get_apk_paths", + "isExtraImport": true, + "detail": "patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "importPath": "patch_apk.utils.get_apk_paths", + "description": "patch_apk.utils.get_apk_paths", + "isExtraImport": true, + "detail": "patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "verifyPackageName", + "importPath": "patch_apk.utils.verify_package_name", + "description": "patch_apk.utils.verify_package_name", + "isExtraImport": true, + "detail": "patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "verifyPackageName", + "importPath": "patch_apk.utils.verify_package_name", + "description": "patch_apk.utils.verify_package_name", + "isExtraImport": true, + "detail": "patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "Path", + "importPath": "pathlib", + "description": "pathlib", + "isExtraImport": true, + "detail": "pathlib", + "documentation": {} + }, + { + "label": "setup", + "importPath": "setuptools", + "description": "setuptools", + "isExtraImport": true, + "detail": "setuptools", + "documentation": {} + }, + { + "label": "find_packages", + "importPath": "setuptools", + "description": "setuptools", + "isExtraImport": true, + "detail": "setuptools", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "kind": 5, + "importPath": "build.lib.patch_apk.config.constants", + "description": "build.lib.patch_apk.config.constants", + "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", + "detail": "build.lib.patch_apk.config.constants", + "documentation": {} + }, + { + "label": "APKBuilder", + "kind": 6, + "importPath": "build.lib.patch_apk.core.apk_builder", + "description": "build.lib.patch_apk.core.apk_builder", + "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", + "detail": "build.lib.patch_apk.core.apk_builder", + "documentation": {} + }, + { + "label": "APKTool", + "kind": 6, + "importPath": "build.lib.patch_apk.core.apk_tool", + "description": "build.lib.patch_apk.core.apk_tool", + "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", + "detail": "build.lib.patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "detectProGuard", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.apk_detect_proguard", + "description": "build.lib.patch_apk.utils.apk_detect_proguard", + "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", + "detail": "build.lib.patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "getArgs", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.cli_tools", + "description": "build.lib.patch_apk.utils.cli_tools", + "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "build.lib.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.copy_split_apks", + "description": "build.lib.patch_apk.utils.copy_split_apks", + "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", + "detail": "build.lib.patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "checkDependencies", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.dependencies", + "description": "build.lib.patch_apk.utils.dependencies", + "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", + "detail": "build.lib.patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.disable_apk_split", + "description": "build.lib.patch_apk.utils.disable_apk_split", + "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", + "detail": "build.lib.patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.fix_private_resources", + "description": "build.lib.patch_apk.utils.fix_private_resources", + "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", + "detail": "build.lib.patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.fix_resource_id", + "description": "build.lib.patch_apk.utils.fix_resource_id", + "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", + "detail": "build.lib.patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.frida_objection", + "description": "build.lib.patch_apk.utils.frida_objection", + "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "detail": "build.lib.patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.get_apk_paths", + "description": "build.lib.patch_apk.utils.get_apk_paths", + "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", + "detail": "build.lib.patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "getTargetAPK", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.get_target_apk", + "description": "build.lib.patch_apk.utils.get_target_apk", + "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", + "detail": "build.lib.patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "rawREReplace", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.raw_re_replace", + "description": "build.lib.patch_apk.utils.raw_re_replace", + "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", + "detail": "build.lib.patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.remove_duplicate_style", + "description": "build.lib.patch_apk.utils.remove_duplicate_style", + "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", + "detail": "build.lib.patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "verifyPackageName", + "kind": 2, + "importPath": "build.lib.patch_apk.utils.verify_package_name", + "description": "build.lib.patch_apk.utils.verify_package_name", + "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", + "detail": "build.lib.patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "main", + "kind": 2, + "importPath": "build.lib.patch_apk.main", + "description": "build.lib.patch_apk.main", + "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", + "detail": "build.lib.patch_apk.main", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "kind": 5, + "importPath": "src.patch_apk.config.constants", + "description": "src.patch_apk.config.constants", + "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", + "detail": "src.patch_apk.config.constants", + "documentation": {} + }, + { + "label": "APKBuilder", + "kind": 6, + "importPath": "src.patch_apk.core.apk_builder", + "description": "src.patch_apk.core.apk_builder", + "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", + "detail": "src.patch_apk.core.apk_builder", + "documentation": {} + }, + { + "label": "APKTool", + "kind": 6, + "importPath": "src.patch_apk.core.apk_tool", + "description": "src.patch_apk.core.apk_tool", + "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", + "detail": "src.patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "detectProGuard", + "kind": 2, + "importPath": "src.patch_apk.utils.apk_detect_proguard", + "description": "src.patch_apk.utils.apk_detect_proguard", + "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", + "detail": "src.patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "getArgs", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "kind": 2, + "importPath": "src.patch_apk.utils.cli_tools", + "description": "src.patch_apk.utils.cli_tools", + "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "src.patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "kind": 2, + "importPath": "src.patch_apk.utils.copy_split_apks", + "description": "src.patch_apk.utils.copy_split_apks", + "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", + "detail": "src.patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "checkDependencies", + "kind": 2, + "importPath": "src.patch_apk.utils.dependencies", + "description": "src.patch_apk.utils.dependencies", + "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", + "detail": "src.patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "kind": 2, + "importPath": "src.patch_apk.utils.disable_apk_split", + "description": "src.patch_apk.utils.disable_apk_split", + "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", + "detail": "src.patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "kind": 2, + "importPath": "src.patch_apk.utils.fix_private_resources", + "description": "src.patch_apk.utils.fix_private_resources", + "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", + "detail": "src.patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "kind": 2, + "importPath": "src.patch_apk.utils.fix_resource_id", + "description": "src.patch_apk.utils.fix_resource_id", + "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", + "detail": "src.patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "kind": 2, + "importPath": "src.patch_apk.utils.frida_objection", + "description": "src.patch_apk.utils.frida_objection", + "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "detail": "src.patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "kind": 2, + "importPath": "src.patch_apk.utils.get_apk_paths", + "description": "src.patch_apk.utils.get_apk_paths", + "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", + "detail": "src.patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "getTargetAPK", + "kind": 2, + "importPath": "src.patch_apk.utils.get_target_apk", + "description": "src.patch_apk.utils.get_target_apk", + "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", + "detail": "src.patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "rawREReplace", + "kind": 2, + "importPath": "src.patch_apk.utils.raw_re_replace", + "description": "src.patch_apk.utils.raw_re_replace", + "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", + "detail": "src.patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "kind": 2, + "importPath": "src.patch_apk.utils.remove_duplicate_style", + "description": "src.patch_apk.utils.remove_duplicate_style", + "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", + "detail": "src.patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "verifyPackageName", + "kind": 2, + "importPath": "src.patch_apk.utils.verify_package_name", + "description": "src.patch_apk.utils.verify_package_name", + "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", + "detail": "src.patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "main", + "kind": 2, + "importPath": "src.patch_apk.main", + "description": "src.patch_apk.main", + "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", + "detail": "src.patch_apk.main", + "documentation": {} + }, + { + "label": "main", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")\ndef fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "checkDependencies", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "getArgs", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "dbgPrint", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\n####################\n# Abort will print given error message and exit the app", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "warningPrint", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\n####################\n# Abort will print given error message and exit the app\n####################\ndef abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\n####################\n# Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "abort", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\n####################\n# Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.\n####################\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "getStdout", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\n####################\n# Get apktool version\n####################\ndef getApktoolVersion():\n commands = [[\"version\"], [\"v\"], [\"-version\"], [\"-v\"]] ", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "getApktoolVersion", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def getApktoolVersion():\n commands = [[\"version\"], [\"v\"], [\"-version\"], [\"-v\"]] \n for cmd in commands:\n try:\n result = runApkTool(cmd)\n if result[\"returncode\"] != 0:\n continue\n version_output = result[\"stdout\"].strip().split(\"\\n\")[0].strip()\n version_str = version_output.split(\"-\")[0].strip()\n return parse_version(version_str)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "runApkTool", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def runApkTool(params):\n exe = \"apktool.bat\" if os.name == \"nt\" else \"apktool\"\n # Feed \"\\r\\n\" so apktool.bat's `pause` won’t block on Windows.\n cp = subprocess.run(\n [exe, *params],\n input=\"\\r\\n\", # Should be harmless on linux\n text=True,\n capture_output=True,\n check=False,\n )", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "build", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")\n####################\n# Sign the APK with apksigner and zip align\n# Fixes https://github.com/NickstaDB/patch-apk/issues/31 by no longer using jarsigner (V1 APK signatures)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "signAndZipAlign", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def signAndZipAlign(baseapkdir, baseapkfilename):\n # Zip align the new APK\n verbosePrint(\"[+] Zip aligning new APK.\")\n assertSubprocessSuccessfulRun([\"zipalign\", \"-f\", \"4\", \"-p\", os.path.join(baseapkdir, \"dist\", baseapkfilename),\n os.path.join(baseapkdir, \"dist\", baseapkfilename[:-4] + \"-aligned.apk\")])\n shutil.move(os.path.join(baseapkdir, \"dist\", baseapkfilename[:-4] + \"-aligned.apk\"), os.path.join(baseapkdir, \"dist\", baseapkfilename))\n # Sign the new APK\n verbosePrint(\"[+] Signing new APK.\")\n apkpath = os.path.join(baseapkdir, \"dist\", baseapkfilename)\n assertSubprocessSuccessfulRun([\"objection\", \"signapk\", apkpath])", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "verifyPackageName", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "getTargetAPK", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "verbosePrint", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\n####################\n# Combine app bundles/split APKs into a single APK for patching.\n####################\ndef combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only):\n warningPrint(\"[!] App bundle/split APK detected, rebuilding as a single APK.\")\n # Extract the individual APKs", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "combineSplitAPKs", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only):\n warningPrint(\"[!] App bundle/split APK detected, rebuilding as a single APK.\")\n # Extract the individual APKs\n baseapkdir = os.path.join(tmppath, pkgname + \"-base\")\n baseapkfilename = pkgname + \"-base.apk\"\n splitapkpaths = []\n bar = Bar('[+] Disassembling split APKs', max=len(localapks))\n verboseOutput = \"\"\n for apkpath in localapks:\n verboseOutput += \"\\nExtracted: \" + apkpath", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "detectProGuard", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return\n # Duplicates\n dupes = []\n # Parse styles.xml and find all elements with duplicate names\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\"))\n for styleEl in tree.getroot().findall(\"style\"):\n itemNames = []", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])])\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "rawREReplace", + "kind": 2, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "kind": 5, + "importPath": "patch-apk", + "description": "patch-apk", + "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"\n####################\n# Main()\n####################\ndef main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version", + "detail": "patch-apk", + "documentation": {} + }, + { + "label": "read_requirements", + "kind": 2, + "importPath": "setup", + "description": "setup", + "peekOfCode": "def read_requirements(req_file: Path):\n if not req_file.exists():\n return []\n lines = req_file.read_text(encoding=\"utf-8\").splitlines()\n reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith(\"#\")]\n return reqs\ndef get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"\n text = pkg_init.read_text(encoding=\"utf-8\")", + "detail": "setup", + "documentation": {} + }, + { + "label": "get_version", + "kind": 2, + "importPath": "setup", + "description": "setup", + "peekOfCode": "def get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"\n text = pkg_init.read_text(encoding=\"utf-8\")\n m = re.search(r\"__version__\\s*=\\s*['\\\"]([^'\\\"]+)['\\\"]\", text)\n return m.group(1) if m else \"0.0.0\"\nlong_description = \"\"\nreadme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")", + "detail": "setup", + "documentation": {} + }, + { + "label": "HERE", + "kind": 5, + "importPath": "setup", + "description": "setup", + "peekOfCode": "HERE = Path(__file__).parent\ndef read_requirements(req_file: Path):\n if not req_file.exists():\n return []\n lines = req_file.read_text(encoding=\"utf-8\").splitlines()\n reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith(\"#\")]\n return reqs\ndef get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"", + "detail": "setup", + "documentation": {} + }, + { + "label": "long_description", + "kind": 5, + "importPath": "setup", + "description": "setup", + "peekOfCode": "long_description = \"\"\nreadme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")\ninstall_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,", + "detail": "setup", + "documentation": {} + }, + { + "label": "readme_file", + "kind": 5, + "importPath": "setup", + "description": "setup", + "peekOfCode": "readme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")\ninstall_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,\n long_description_content_type=\"text/markdown\",", + "detail": "setup", + "documentation": {} + }, + { + "label": "install_requires", + "kind": 5, + "importPath": "setup", + "description": "setup", + "peekOfCode": "install_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,\n long_description_content_type=\"text/markdown\",\n packages=find_packages(where=\"src\"),\n package_dir={\"\": \"src\"},\n include_package_data=True,", + "detail": "setup", + "documentation": {} + } +] \ No newline at end of file diff --git a/README.md b/README.md index 7029e4d..f5cdb49 100644 --- a/README.md +++ b/README.md @@ -37,16 +37,39 @@ An APK patcher, for use with [objection](https://github.com/sensepost/objection) * **29th March 2020:** Added `--save-apk` parameter to save a copy of the unpatched single APK for use with other tools. * **27th March 2020:** Initial release supporting split APKs and the `--no-enable-user-certs` flag. -## Usage ## -Install the target Android application on your device and connect it to your computer/VM so that `adb devices` can see it, then run: +## Installation & Usage ## + +Installation (development) + +Install the project in editable mode (recommended during development): + +``` +python -m pip install -e . +``` + +Install for production / normal use: + +``` +python -m pip install . +``` + +After installation the CLI entry point `patch-apk` will be available. Alternatively you can run directly from the source tree: + +``` +python -m patch_apk.main {package-name} +``` + +Usage + +Ensure your Android device is connected and visible to adb (`adb devices`). Then run: ``` -python3 patch-apk.py {package-name} +patch-apk {package-name} ``` -The package-name parameter can be the fully-qualified package name of the Android app, such as `com.google.android.youtube`, or a partial package name, such as `tube`. +The `{package-name}` parameter can be a fully-qualified package name such as `com.google.android.youtube`, or a partial package name (e.g. `tube`) — the tool will attempt to resolve partial names and ask for confirmation if multiple matches are found. -Along with injecting an instrumentation gadget, the script also automatically enables support for user-installed CA certificates by injecting a network security configuration file into the APK. To disable this functionality, pass the `--no-enable-user-certs` parameter on the command line. +By default the tool will inject the Frida gadget and enable support for user-installed CA certificates by modifying the app's network security config. To disable the network cert modification, pass `--no-enable-user-certs` on the command line. ### Examples ### **Basic usage:** Simply install the target Android app on your device, make sure `adb devices` can see your device, then pass the package name to `patch-apk.py`. diff --git a/README_PROJECT_LAYOUT.md b/README_PROJECT_LAYOUT.md new file mode 100644 index 0000000..d6336a6 --- /dev/null +++ b/README_PROJECT_LAYOUT.md @@ -0,0 +1,50 @@ +## Project layout + +The primary source package lives under `src/patch_apk/`. The following lists the source tree with files inside the `config`, `core`, and `utils` subfolders. + +``` +src/ +├─ .vscode/ +│ └─ PythonImportHelper-v2-Completion.json +├─ patch_apk/ +│ ├─ __init__.py +│ ├─ main.py +│ ├─ config/ +│ │ ├─ __init__.py +│ │ └─ constants.py +│ ├─ core/ +│ │ ├─ __init__.py +│ │ ├─ apk_builder.py +│ │ └─ apk_tool.py +│ └─ utils/ +│ ├─ __init__.py +│ ├─ apk_detect_proguard.py +│ ├─ cli_tools.py +│ ├─ copy_split_apks.py +│ ├─ dependencies.py +│ ├─ disable_apk_split.py +│ ├─ fix_private_resources.py +│ ├─ fix_resource_id.py +│ ├─ frida_objection.py +│ ├─ get_apk_paths.py +│ ├─ get_target_apk.py +│ ├─ raw_re_replace.py +│ ├─ remove_duplicate_style.py +│ ├─ verify_package_name.py +│ └─ data/ +│ └─ patch-apk.keystore +└─ patch_apk.egg-info/ + ├─ dependency_links.txt + ├─ entry_points.txt + ├─ PKG-INFO + ├─ requires.txt + ├─ SOURCES.txt + └─ top_level.txt +``` + +Notes: + +- Files inside `src/patch_apk/core/` are implemented in a class-based style (for example `APKTool`, `APKBuilder`, etc.). +- Files inside `src/patch_apk/utils/` are implemented as modules of helper functions (function-based utilities used across the project). + +The package version is defined in `src/patch_apk/__init__.py` (`__version__`). diff --git a/data/patch-apk.keystore b/data/patch-apk.keystore new file mode 100644 index 0000000000000000000000000000000000000000..e412b8bae53438d7cfeadcc00cb0833b3dd22a28 GIT binary patch literal 2533 zcmY+EcQ_l08pb1wSfz~`MQtKfY`%E!STf^o2f z3p);lgM|Def_Q>)5Z4Ra@nY+{0&0ndoFxLHIl}NL2FEas@MT zBq3obYX(j}v1QkjR5v2*NA`ec$hl7^8wpc%jTuAx^d5;%qon@4xZ!1lFj54UXcumo zRarX?NGii;rPqzjCRZ`MbsPRz!SS`?w?v^)t)&)QytL>(#2_m>!`-1M+d;8EofFiz zD}5FX8}EjaF)mjD)py)Fe`jL7myKVc-UU25g@1;*q{S8doI38zPSk zHY};5o-iyG%!s8wR+*|FscjGIzq@kZGS9cNmeae*h2?I-8#!F^3xQ-$hGOUSD*nxW zRCR%nDu2q#aDjoEL%by+%7QELLoo7y(7@*XmMnB{m0EU<`;^WuByAP*of=cPP={F> zYGs%+X)GvC5j*tVU~ZTiBp5Vz%gILeEykH;Pew}ddhn52nnIKDZ82UDfz2E{I;U=@ zw`uBRk>%{Pczs?^ZlyKQiYk2zm$_5G>oX*tx3ps+kni4>m+?}hTt6c=TjR1#DSb+0 z1%_FSKxXO-4MOkKOQoM;1KH6D^)ua~^?`IbIb^EQb;+G>5sir>E!=mvKd()#J7UXu z9DN@YQ5^(2o_nnh4V&;Vjxg0KYH9C0KWYAGjwp*>$}LvjRr4n%%%Y@wy`}mj9%!UYUS~9VV>5&ZQTi=jrvejS|SW~-i#gLhVHwg}O$m0PQ`;ng7IEX=yFl8a>~voBvyfU+Fw{+InE8 z1Ey_&)}V*kR%>cJ{eC8)yiYr_5c6Ipk( zNa!`AWk=}A@()k6S9psY4bAjQ<7pVSv)nO3l?)S-R7G^{6%pgJS5?WkH;zi_d0%%K zbpfi(Z_rqv8IzrJMH)$wugqC^zPt83Kw7Gmy=pBxugH$Zru0X%GH911%Bkd!) zwrzNv)l^e;u`B*roEzwzZ%)-m96sT-R#ZQri=RtNty`mwUI&!Re9T$Bb>9j3YjJh5 z^aJA#Fc0yrqP23bJO4ROTYnVzlk|BxgA)4ghT1{aEkb~4|G#J&HEGPdF#MvD#uwoG zi=r^NBquiqz#rfQ2m-hOJOSbtn?C@3p=f{`AQZ*>ca=pF4!&&Zg>mDNlvR+Gl9NTr zDacF7U!WBH{}MGc9|t-_;XwO9D!_%1|4yj>!CUBmcw5y_!K!AAchjw00v=?-Py;|u z#sBg)1_!FiuP&K&$x@qaol`S0>!t;r6O84}_qWW(R1c(tt2ZYO>45Tx()6KQA_ll?e4J);O;yo0N?wutstIw#&6I=Ynk z`CSKo9(^f2$@H@O$D~cifzCsu_EGK{<5J`s0HjWaJ(Rz|m^pq5D$;w%82@2g*EmctlA-orrk<< zBsoT#4yo4T~Z^vV% z{_LBeG25dnMWM}Bl&E~9js^W&t)^Qyu~ofifGhCl)fIcga~9!Cuz@&#KkLe-PMsqb zLa@YQ^h1vp`|v8&DbyT+xr(ZMt4gIa&NcBj6kMl)awEPA`h6#$ybhTjUgJg{9Lcjp zlu60hLVT{MYzZaWmShg3z-!>uL_hA(V?7C+kf)j1@VEh5Ftc)6 zd{ihwaKzF1OI)ukShlvBD@6TQedQ={ox0ZLv<>srG9d@^0PX%=)gh_@80OI|~n4 zzL16%%eT?JGcuu^b7X%ZgF?iq-R8{}N+I7aTL);w+_lBR>T8{@Y|GOCrazj-3@TU8 zNZG2T;naF+K5HF`mP#mHhYYbBP3-q+&ch}HTy;ZcT8EsAQR?3FGJP2|0gEV~VwR5! zf+|t##YudUJc!2q7D3(((#Rh&07@&F=DXmU`R70+BPK8CC@gnk@Pz4U5S`Mu5%Be+ z_lp}>S{&I;bs`jeac#<+0%L4%$npIYyELQR=!ZWCZ9u2 zoK8gJlbf$~Qvn*b>ArQHCid{p%Kn5^hjlL7v8=Jc0O6rLL4jUb6KI(z$yuT?j@g}~ zql4JEsOWCu%;kfColT)sP`oG@6oO;~0XZN5FkG*3FaN#G3${AUn~CED-e(!vl{R4b dn4Pp7HXYT#U1ClPrN+=z$8|{oK~xX&{|zOQrfvWL literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 9d3863bde5ee74e8dae3e2a4b128ecacfc40b6cc..cb7a9c1d16d52235697dab029c081ef1af7aa123 100644 GIT binary patch literal 376 zcmYk2-3o$06oltG=pBMCLv72yf}oe^I?A*n%SDaezWR1Ch+z-woH=K9{JgCWDs-cs zb=8Bq0ShGxHP=`p1u8Z1-Ci?Y)B~$dYem3K^{`JJnJCog4!cteTe2@gd*EK63AkCj z!eMI5Os&AKOW_k`^eU}TCulNmuDjw?a24G-{1neK*!;Qf2UnNH z@~(z{i>?DMtKmH<$N01zot`w7+JJgcCF6b1MBib9tqKB5*aT)^dGN+IKu>;?hoErQt!dZ7z;Be@MTc!kEQ+!6u>9;%4oIM6UeoQF3jCHik>S zOyY_mz+ZiLb;u=!g0%v0awmMK*v{Ni+B(sUnBLK_>@IDo00_c6_(sGHfDx&>@dHz% BIeY*B diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8cabca4 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +from pathlib import Path +from setuptools import setup, find_packages +import re + + +HERE = Path(__file__).parent + + +def read_requirements(req_file: Path): + if not req_file.exists(): + return [] + lines = req_file.read_text(encoding="utf-8").splitlines() + reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith("#")] + return reqs + + +def get_version(pkg_init: Path): + if not pkg_init.exists(): + return "0.0.0" + text = pkg_init.read_text(encoding="utf-8") + m = re.search(r"__version__\s*=\s*['\"]([^'\"]+)['\"]", text) + return m.group(1) if m else "0.0.0" + + +long_description = "" +readme_file = HERE / "README.md" +if readme_file.exists(): + long_description = readme_file.read_text(encoding="utf-8") + +install_requires = read_requirements(HERE / "requirements.txt") + +setup( + name="patch-apk", + version=get_version(HERE / "src" / "patch_apk" / "__init__.py"), + description="App Bundle / Split APK aware patcher for objection", + long_description=long_description, + long_description_content_type="text/markdown", + packages=find_packages(where="src"), + package_dir={"": "src"}, + include_package_data=True, + install_requires=install_requires, + entry_points={ + "console_scripts": [ + "patch-apk=patch_apk.main:main", + ] + }, + python_requires=">=3.8", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], +) diff --git a/src/.vscode/PythonImportHelper-v2-Completion.json b/src/.vscode/PythonImportHelper-v2-Completion.json new file mode 100644 index 0000000..f25bd00 --- /dev/null +++ b/src/.vscode/PythonImportHelper-v2-Completion.json @@ -0,0 +1,681 @@ +[ + { + "label": "os", + "kind": 6, + "isExtraImport": true, + "importPath": "os", + "description": "os", + "detail": "os", + "documentation": {} + }, + { + "label": "shutil", + "kind": 6, + "isExtraImport": true, + "importPath": "shutil", + "description": "shutil", + "detail": "shutil", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getArgs", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "importPath": "patch_apk.utils.fix_private_resources", + "description": "patch_apk.utils.fix_private_resources", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "subprocess", + "kind": 6, + "isExtraImport": true, + "importPath": "subprocess", + "description": "subprocess", + "detail": "subprocess", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "Bar", + "importPath": "progress.bar", + "description": "progress.bar", + "isExtraImport": true, + "detail": "progress.bar", + "documentation": {} + }, + { + "label": "parse", + "importPath": "packaging.version", + "description": "packaging.version", + "isExtraImport": true, + "detail": "packaging.version", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "rawREReplace", + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "isExtraImport": true, + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "importPath": "patch_apk.utils.disable_apk_split", + "description": "patch_apk.utils.disable_apk_split", + "isExtraImport": true, + "detail": "patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "importPath": "patch_apk.utils.remove_duplicate_style", + "description": "patch_apk.utils.remove_duplicate_style", + "isExtraImport": true, + "detail": "patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "importPath": "patch_apk.utils.fix_resource_id", + "description": "patch_apk.utils.fix_resource_id", + "isExtraImport": true, + "detail": "patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "detectProGuard", + "importPath": "patch_apk.utils.apk_detect_proguard", + "description": "patch_apk.utils.apk_detect_proguard", + "isExtraImport": true, + "detail": "patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "importPath": "patch_apk.utils.copy_split_apks", + "description": "patch_apk.utils.copy_split_apks", + "isExtraImport": true, + "detail": "patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "argparse", + "kind": 6, + "isExtraImport": true, + "importPath": "argparse", + "description": "argparse", + "detail": "argparse", + "documentation": {} + }, + { + "label": "sys", + "kind": 6, + "isExtraImport": true, + "importPath": "sys", + "description": "sys", + "detail": "sys", + "documentation": {} + }, + { + "label": "colored", + "importPath": "termcolor", + "description": "termcolor", + "isExtraImport": true, + "detail": "termcolor", + "documentation": {} + }, + { + "label": "xml.etree.ElementTree", + "kind": 6, + "isExtraImport": true, + "importPath": "xml.etree.ElementTree", + "description": "xml.etree.ElementTree", + "detail": "xml.etree.ElementTree", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "importPath": "patch_apk.config.constants", + "description": "patch_apk.config.constants", + "isExtraImport": true, + "detail": "patch_apk.config.constants", + "documentation": {} + }, + { + "label": "tempfile", + "kind": 6, + "isExtraImport": true, + "importPath": "tempfile", + "description": "tempfile", + "detail": "tempfile", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "APKTool", + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "isExtraImport": true, + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "re", + "kind": 6, + "isExtraImport": true, + "importPath": "re", + "description": "re", + "detail": "re", + "documentation": {} + }, + { + "label": "checkDependencies", + "importPath": "patch_apk.utils.dependencies", + "description": "patch_apk.utils.dependencies", + "isExtraImport": true, + "detail": "patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "importPath": "patch_apk.utils.frida_objection", + "description": "patch_apk.utils.frida_objection", + "isExtraImport": true, + "detail": "patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "getTargetAPK", + "importPath": "patch_apk.utils.get_target_apk", + "description": "patch_apk.utils.get_target_apk", + "isExtraImport": true, + "detail": "patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "importPath": "patch_apk.utils.get_apk_paths", + "description": "patch_apk.utils.get_apk_paths", + "isExtraImport": true, + "detail": "patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "verifyPackageName", + "importPath": "patch_apk.utils.verify_package_name", + "description": "patch_apk.utils.verify_package_name", + "isExtraImport": true, + "detail": "patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "NULL_DECODED_DRAWABLE_COLOR", + "kind": 5, + "importPath": "patch_apk.config.constants", + "description": "patch_apk.config.constants", + "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", + "detail": "patch_apk.config.constants", + "documentation": {} + }, + { + "label": "APKBuilder", + "kind": 6, + "importPath": "patch_apk.core.apk_builder", + "description": "patch_apk.core.apk_builder", + "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", + "detail": "patch_apk.core.apk_builder", + "documentation": {} + }, + { + "label": "APKTool", + "kind": 6, + "importPath": "patch_apk.core.apk_tool", + "description": "patch_apk.core.apk_tool", + "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", + "detail": "patch_apk.core.apk_tool", + "documentation": {} + }, + { + "label": "detectProGuard", + "kind": 2, + "importPath": "patch_apk.utils.apk_detect_proguard", + "description": "patch_apk.utils.apk_detect_proguard", + "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", + "detail": "patch_apk.utils.apk_detect_proguard", + "documentation": {} + }, + { + "label": "getArgs", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "abort", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "verbosePrint", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "dbgPrint", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "getStdout", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "assertSubprocessSuccessfulRun", + "kind": 2, + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "copySplitApkFiles", + "kind": 2, + "importPath": "patch_apk.utils.copy_split_apks", + "description": "patch_apk.utils.copy_split_apks", + "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", + "detail": "patch_apk.utils.copy_split_apks", + "documentation": {} + }, + { + "label": "checkDependencies", + "kind": 2, + "importPath": "patch_apk.utils.dependencies", + "description": "patch_apk.utils.dependencies", + "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", + "detail": "patch_apk.utils.dependencies", + "documentation": {} + }, + { + "label": "disableApkSplitting", + "kind": 2, + "importPath": "patch_apk.utils.disable_apk_split", + "description": "patch_apk.utils.disable_apk_split", + "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", + "detail": "patch_apk.utils.disable_apk_split", + "documentation": {} + }, + { + "label": "fixPrivateResources", + "kind": 2, + "importPath": "patch_apk.utils.fix_private_resources", + "description": "patch_apk.utils.fix_private_resources", + "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", + "detail": "patch_apk.utils.fix_private_resources", + "documentation": {} + }, + { + "label": "fixPublicResourceIDs", + "kind": 2, + "importPath": "patch_apk.utils.fix_resource_id", + "description": "patch_apk.utils.fix_resource_id", + "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", + "detail": "patch_apk.utils.fix_resource_id", + "documentation": {} + }, + { + "label": "fixAPKBeforeObjection", + "kind": 2, + "importPath": "patch_apk.utils.frida_objection", + "description": "patch_apk.utils.frida_objection", + "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "detail": "patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "getAPKPathsForPackage", + "kind": 2, + "importPath": "patch_apk.utils.get_apk_paths", + "description": "patch_apk.utils.get_apk_paths", + "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", + "detail": "patch_apk.utils.get_apk_paths", + "documentation": {} + }, + { + "label": "getTargetAPK", + "kind": 2, + "importPath": "patch_apk.utils.get_target_apk", + "description": "patch_apk.utils.get_target_apk", + "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", + "detail": "patch_apk.utils.get_target_apk", + "documentation": {} + }, + { + "label": "rawREReplace", + "kind": 2, + "importPath": "patch_apk.utils.raw_re_replace", + "description": "patch_apk.utils.raw_re_replace", + "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", + "detail": "patch_apk.utils.raw_re_replace", + "documentation": {} + }, + { + "label": "hackRemoveDuplicateStyleEntries", + "kind": 2, + "importPath": "patch_apk.utils.remove_duplicate_style", + "description": "patch_apk.utils.remove_duplicate_style", + "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", + "detail": "patch_apk.utils.remove_duplicate_style", + "documentation": {} + }, + { + "label": "verifyPackageName", + "kind": 2, + "importPath": "patch_apk.utils.verify_package_name", + "description": "patch_apk.utils.verify_package_name", + "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", + "detail": "patch_apk.utils.verify_package_name", + "documentation": {} + }, + { + "label": "main", + "kind": 2, + "importPath": "patch_apk.main", + "description": "patch_apk.main", + "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", + "detail": "patch_apk.main", + "documentation": {} + } +] \ No newline at end of file diff --git a/src/patch_apk/utils/verify_package_name.py b/src/patch_apk/utils/verify_package_name.py index 73c7517..567679a 100644 --- a/src/patch_apk/utils/verify_package_name.py +++ b/src/patch_apk/utils/verify_package_name.py @@ -1,5 +1,5 @@ import subprocess -from patch_apk.utils.cli_tools import abort +from patch_apk.utils.cli_tools import abort, warningPrint import os def verifyPackageName(pkgname): @@ -13,4 +13,24 @@ def verifyPackageName(pkgname): if line.startswith("package:"): line = line[8:].strip() if pkgname.lower() in line.lower(): - packages.append(line) \ No newline at end of file + packages.append(line) + + # Bail out if no matching packages were found + if len(packages) == 0: + abort("Error, no packages found on the device matching the search term '" + pkgname + "'.\nRun 'adb shell pm list packages' to verify installed package names.") + + # Return the target package name, offering a choice to the user if necessary + if len(packages) == 1: + return packages[0] + else: + warningPrint("[!] Multiple matching packages installed, select the package to patch.") + choice = -1 + while choice == -1: + for i in range(len(packages)): + print("[" + str(i + 1) + "] " + packages[i]) + choice = input("\nChoice: ") + if not choice.isnumeric() or int(choice) < 1 or int(choice) > len(packages): + print("\nInvalid choice.\n") + choice = -1 + print("") + return packages[int(choice) - 1] \ No newline at end of file From 8f1528ea2b9880e8ca687f73b04d7c7ff3d15dad Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 16:04:02 +0530 Subject: [PATCH 3/8] TODO: Fix objection patching, added --only-main-classes flag for app building due to building errors --- .vscode/PythonImportHelper-v2-Completion.json | 61 +++++++++++++++- src/patch_apk/main.py | 15 +--- src/patch_apk/utils/frida_objection.py | 29 ++++++-- src/patch_apk/utils/remove_duplicate_class.py | 69 +++++++++++++++++++ 4 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 src/patch_apk/utils/remove_duplicate_class.py diff --git a/.vscode/PythonImportHelper-v2-Completion.json b/.vscode/PythonImportHelper-v2-Completion.json index b1da922..6ee998a 100644 --- a/.vscode/PythonImportHelper-v2-Completion.json +++ b/.vscode/PythonImportHelper-v2-Completion.json @@ -345,6 +345,22 @@ "detail": "patch_apk.utils.cli_tools", "documentation": {} }, + { + "label": "assertSubprocessSuccessfulRun", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, + { + "label": "warningPrint", + "importPath": "patch_apk.utils.cli_tools", + "description": "patch_apk.utils.cli_tools", + "isExtraImport": true, + "detail": "patch_apk.utils.cli_tools", + "documentation": {} + }, { "label": "abort", "importPath": "patch_apk.utils.cli_tools", @@ -855,6 +871,14 @@ "detail": "patch_apk.utils.frida_objection", "documentation": {} }, + { + "label": "patchingWithObjection", + "importPath": "patch_apk.utils.frida_objection", + "description": "patch_apk.utils.frida_objection", + "isExtraImport": true, + "detail": "patch_apk.utils.frida_objection", + "documentation": {} + }, { "label": "getTargetAPK", "importPath": "patch_apk.utils.get_target_apk", @@ -903,6 +927,23 @@ "detail": "patch_apk.utils.verify_package_name", "documentation": {} }, + { + "label": "patch_apk.utils.remove_duplicate_class", + "kind": 6, + "isExtraImport": true, + "importPath": "patch_apk.utils.remove_duplicate_class", + "description": "patch_apk.utils.remove_duplicate_class", + "detail": "patch_apk.utils.remove_duplicate_class", + "documentation": {} + }, + { + "label": "remove_duplicate_classes", + "importPath": "patch_apk.utils.remove_duplicate_class", + "description": "patch_apk.utils.remove_duplicate_class", + "isExtraImport": true, + "detail": "patch_apk.utils.remove_duplicate_class", + "documentation": {} + }, { "label": "Path", "importPath": "pathlib", @@ -1283,7 +1324,16 @@ "kind": 2, "importPath": "src.patch_apk.utils.frida_objection", "description": "src.patch_apk.utils.frida_objection", - "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", \"--only-main-classes\", apkfile, \"-o\", apkdir,])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", + "detail": "src.patch_apk.utils.frida_objection", + "documentation": {} + }, + { + "label": "patchingWithObjection", + "kind": 2, + "importPath": "src.patch_apk.utils.frida_objection", + "description": "src.patch_apk.utils.frida_objection", + "peekOfCode": "def patchingWithObjection(apkfile):\n # Patch the target APK with objection\n print(\"[+] Patching \" + apkfile.split(os.sep)[-1] + \" with objection.\")\n warningPrint(\"[!] The application will be patched with Frida 16.7.19. See https://github.com/sensepost/objection/issues/737 for more information.\")\n if subprocess.run([\"objection\", \"patchapk\", \"-V\", \"16.7.19\", \"--skip-resources\", \"--ignore-nativelibs\", \"-s\", apkfile], capture_output=True).returncode != 0:\n print(\"[+] Objection patching failed, trying alternative approach\")\n warningPrint(\"[!] If you get an error, the application might not have a launchable activity\")\n # Try without --skip-resources, since objection potentially wasn't able to identify the starting activity\n # There could have been another reason for the failure, but it's a sensible fallback\n # Another reason could be a missing INTERNET permission", "detail": "src.patch_apk.utils.frida_objection", "documentation": {} }, @@ -1314,6 +1364,15 @@ "detail": "src.patch_apk.utils.raw_re_replace", "documentation": {} }, + { + "label": "remove_duplicate_classes", + "kind": 2, + "importPath": "src.patch_apk.utils.remove_duplicate_class", + "description": "src.patch_apk.utils.remove_duplicate_class", + "peekOfCode": "def remove_duplicate_classes(apkdir):\n \"\"\"\n Remove duplicate/conflicting smali classes from smali_assets directory.\n This prevents 'has already been interned' errors during APK rebuild.\n \"\"\"\n smali_assets = os.path.join(apkdir, \"smali_assets\")\n if not os.path.exists(smali_assets):\n print(\"[+] No smali_assets directory found, skipping duplicate check\")\n return\n print(\"[+] Scanning for conflicting smali classes...\")", + "detail": "src.patch_apk.utils.remove_duplicate_class", + "documentation": {} + }, { "label": "hackRemoveDuplicateStyleEntries", "kind": 2, diff --git a/src/patch_apk/main.py b/src/patch_apk/main.py index 7ac569f..f3b0ddb 100644 --- a/src/patch_apk/main.py +++ b/src/patch_apk/main.py @@ -14,7 +14,7 @@ from patch_apk.utils.cli_tools import getArgs, warningPrint, assertSubprocessSuccessfulRun from patch_apk.utils.dependencies import checkDependencies -from patch_apk.utils.frida_objection import fixAPKBeforeObjection +from patch_apk.utils.frida_objection import fixAPKBeforeObjection, patchingWithObjection from patch_apk.utils.get_target_apk import getTargetAPK from patch_apk.utils.get_apk_paths import getAPKPathsForPackage from patch_apk.utils.verify_package_name import verifyPackageName @@ -54,17 +54,8 @@ def main(): # Before patching with objection, add INTERNET permission if not already present, and set extractNativeLibs to true fixAPKBeforeObjection(apkfile, not args.no_enable_user_certs) - # Patch the target APK with objection - print("[+] Patching " + apkfile.split(os.sep)[-1] + " with objection.") - warningPrint("[!] The application will be patched with Frida 16.7.19. See https://github.com/sensepost/objection/issues/737 for more information.") - if subprocess.run(["objection", "patchapk", "-V", "16.7.19", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], capture_output=True).returncode != 0: - print("[+] Objection patching failed, trying alternative approach") - warningPrint("[!] If you get an error, the application might not have a launchable activity") - - # Try without --skip-resources, since objection potentially wasn't able to identify the starting activity - # There could have been another reason for the failure, but it's a sensible fallback - # Another reason could be a missing INTERNET permission - assertSubprocessSuccessfulRun(["objection", "patchapk","-V", "16.7.19", "--ignore-nativelibs", "-s", apkfile]) + # Patch the APK with objection + patchingWithObjection(apkfile) os.remove(apkfile) shutil.move(apkfile[:-4] + ".objection.apk", apkfile) diff --git a/src/patch_apk/utils/frida_objection.py b/src/patch_apk/utils/frida_objection.py index 9584361..b1671a7 100644 --- a/src/patch_apk/utils/frida_objection.py +++ b/src/patch_apk/utils/frida_objection.py @@ -2,6 +2,7 @@ import tempfile import shutil import xml.etree.ElementTree +import subprocess # core imports @@ -9,13 +10,14 @@ # utility imports -from patch_apk.utils.cli_tools import abort +from patch_apk.utils.cli_tools import abort, assertSubprocessSuccessfulRun, warningPrint +from patch_apk.utils.remove_duplicate_class import remove_duplicate_classes def fixAPKBeforeObjection(apkfile, fix_network_security_config): print("[+] Prepping AndroidManifest.xml") with tempfile.TemporaryDirectory() as tmppath: apkdir = os.path.join(tmppath, "apk") - ret = APKTool.runApkTool(["d", apkfile, "-o", apkdir]) + ret = APKTool.runApkTool(["d", "--only-main-classes", apkfile, "-o", apkdir,]) if ret["returncode"] != 0: abort("Error: Failed to run 'apktool d " + apkfile + " -o " + apkdir + "'.\nRun with --debug-output for more information.") @@ -63,7 +65,12 @@ def fixAPKBeforeObjection(apkfile, fix_network_security_config): # Save the updated AndroidManifest.xml tree.write(manifestPath, encoding="utf-8", xml_declaration=True) - + # Remove problematic duplicate classes + try: + remove_duplicate_classes(apkdir) + except Exception as e: + print(f"[!] Warning: Failed to remove duplicate classes: {e}") + pass # Rebuild apk file result = APKTool.runApkTool(["b", apkdir]) if result["returncode"] != 0: @@ -75,4 +82,18 @@ def fixAPKBeforeObjection(apkfile, fix_network_security_config): if os.path.exists(rebuilt_apk): shutil.move(rebuilt_apk, apkfile) else: - abort("Error: Rebuilt APK not found.") \ No newline at end of file + abort("Error: Rebuilt APK not found.") + + +def patchingWithObjection(apkfile): + # Patch the target APK with objection + print("[+] Patching " + apkfile.split(os.sep)[-1] + " with objection.") + warningPrint("[!] The application will be patched with Frida 16.7.19. See https://github.com/sensepost/objection/issues/737 for more information.") + if subprocess.run(["objection", "patchapk", "-V", "16.7.19", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], capture_output=True).returncode != 0: + print("[+] Objection patching failed, trying alternative approach") + warningPrint("[!] If you get an error, the application might not have a launchable activity") + + # Try without --skip-resources, since objection potentially wasn't able to identify the starting activity + # There could have been another reason for the failure, but it's a sensible fallback + # Another reason could be a missing INTERNET permission + assertSubprocessSuccessfulRun(["objection", "patchapk","-V", "16.7.19", "--ignore-nativelibs", "-s", apkfile]) \ No newline at end of file diff --git a/src/patch_apk/utils/remove_duplicate_class.py b/src/patch_apk/utils/remove_duplicate_class.py new file mode 100644 index 0000000..7edb07d --- /dev/null +++ b/src/patch_apk/utils/remove_duplicate_class.py @@ -0,0 +1,69 @@ +import os +import patch_apk.utils.remove_duplicate_class +import shutil + +def remove_duplicate_classes(apkdir): + """ + Remove duplicate/conflicting smali classes from smali_assets directory. + This prevents 'has already been interned' errors during APK rebuild. + """ + smali_assets = os.path.join(apkdir, "smali_assets") + + if not os.path.exists(smali_assets): + print("[+] No smali_assets directory found, skipping duplicate check") + return + + print("[+] Scanning for conflicting smali classes...") + + # Get all smali directories except smali_assets + main_smali_dirs = [os.path.join(apkdir, d) for d in os.listdir(apkdir) + if d.startswith("smali") and d != "smali_assets" + and os.path.isdir(os.path.join(apkdir, d))] + + if not main_smali_dirs: + print("[+] No main smali directories found") + return + + # Build a set of all class paths in main smali directories + main_classes = set() + for smali_dir in main_smali_dirs: + for root, dirs, files in os.walk(smali_dir): + for file in files: + if file.endswith(".smali"): + # Get relative path from smali_dir root + rel_path = os.path.relpath(os.path.join(root, file), smali_dir) + main_classes.add(rel_path) + + print(f"[+] Found {len(main_classes)} classes in main smali directories") + + # Check smali_assets for duplicates + duplicates_removed = 0 + for root, dirs, files in os.walk(smali_assets): + for file in files: + if file.endswith(".smali"): + full_path = os.path.join(root, file) + rel_path = os.path.relpath(full_path, smali_assets) + + # If this class exists in main smali dirs, it's a duplicate + if rel_path in main_classes: + try: + os.remove(full_path) + duplicates_removed += 1 + print(f"[+] Removed duplicate: {rel_path}") + except Exception as e: + print(f"[!] Failed to remove {rel_path}: {e}") + + # Clean up empty directories + for root, dirs, files in os.walk(smali_assets, topdown=False): + if not os.listdir(root): + try: + os.rmdir(root) + except Exception: + pass + + # If smali_assets is now empty, remove it entirely + if os.path.exists(smali_assets) and not os.listdir(smali_assets): + shutil.rmtree(smali_assets) + print("[+] Removed empty smali_assets directory") + + print(f"[+] Removed {duplicates_removed} conflicting smali classes") \ No newline at end of file From d9c4fa347283ebe88536bc218af7b8c9ace30462 Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 16:07:05 +0530 Subject: [PATCH 4/8] added vscode folder in gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e15106e..1894459 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ __pycache__/ *.py[codz] *$py.class +.vscode/ +.vscode +src/.vscode/ +src/.vscode # C extensions *.so From 6a3a418c5cb94cc4818635ca2c219bb6dbf732be Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 16:10:03 +0530 Subject: [PATCH 5/8] modified gitignore --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1894459..1bf5235 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,11 @@ __pycache__/ *.py[codz] *$py.class -.vscode/ +.vscode/* .vscode -src/.vscode/ +src/.vscode/* src/.vscode + # C extensions *.so From 016825f8d604237407fbd8b4638bedd5a00e90de Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 16:13:10 +0530 Subject: [PATCH 6/8] Remove .vscode from tracking --- .vscode/PythonImportHelper-v2-Completion.json | 1691 ----------------- 1 file changed, 1691 deletions(-) delete mode 100644 .vscode/PythonImportHelper-v2-Completion.json diff --git a/.vscode/PythonImportHelper-v2-Completion.json b/.vscode/PythonImportHelper-v2-Completion.json deleted file mode 100644 index 6ee998a..0000000 --- a/.vscode/PythonImportHelper-v2-Completion.json +++ /dev/null @@ -1,1691 +0,0 @@ -[ - { - "label": "os", - "kind": 6, - "isExtraImport": true, - "importPath": "os", - "description": "os", - "detail": "os", - "documentation": {} - }, - { - "label": "shutil", - "kind": 6, - "isExtraImport": true, - "importPath": "shutil", - "description": "shutil", - "detail": "shutil", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getArgs", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getArgs", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "importPath": "patch_apk.utils.fix_private_resources", - "description": "patch_apk.utils.fix_private_resources", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "importPath": "patch_apk.utils.fix_private_resources", - "description": "patch_apk.utils.fix_private_resources", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "subprocess", - "kind": 6, - "isExtraImport": true, - "importPath": "subprocess", - "description": "subprocess", - "detail": "subprocess", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "parse", - "importPath": "packaging.version", - "description": "packaging.version", - "isExtraImport": true, - "detail": "packaging.version", - "documentation": {} - }, - { - "label": "parse", - "importPath": "packaging.version", - "description": "packaging.version", - "isExtraImport": true, - "detail": "packaging.version", - "documentation": {} - }, - { - "label": "parse", - "importPath": "packaging.version", - "description": "packaging.version", - "isExtraImport": true, - "detail": "packaging.version", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "importPath": "patch_apk.utils.disable_apk_split", - "description": "patch_apk.utils.disable_apk_split", - "isExtraImport": true, - "detail": "patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "importPath": "patch_apk.utils.disable_apk_split", - "description": "patch_apk.utils.disable_apk_split", - "isExtraImport": true, - "detail": "patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "importPath": "patch_apk.utils.remove_duplicate_style", - "description": "patch_apk.utils.remove_duplicate_style", - "isExtraImport": true, - "detail": "patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "importPath": "patch_apk.utils.remove_duplicate_style", - "description": "patch_apk.utils.remove_duplicate_style", - "isExtraImport": true, - "detail": "patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "importPath": "patch_apk.utils.fix_resource_id", - "description": "patch_apk.utils.fix_resource_id", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "importPath": "patch_apk.utils.fix_resource_id", - "description": "patch_apk.utils.fix_resource_id", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "detectProGuard", - "importPath": "patch_apk.utils.apk_detect_proguard", - "description": "patch_apk.utils.apk_detect_proguard", - "isExtraImport": true, - "detail": "patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "detectProGuard", - "importPath": "patch_apk.utils.apk_detect_proguard", - "description": "patch_apk.utils.apk_detect_proguard", - "isExtraImport": true, - "detail": "patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "importPath": "patch_apk.utils.copy_split_apks", - "description": "patch_apk.utils.copy_split_apks", - "isExtraImport": true, - "detail": "patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "importPath": "patch_apk.utils.copy_split_apks", - "description": "patch_apk.utils.copy_split_apks", - "isExtraImport": true, - "detail": "patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "argparse", - "kind": 6, - "isExtraImport": true, - "importPath": "argparse", - "description": "argparse", - "detail": "argparse", - "documentation": {} - }, - { - "label": "sys", - "kind": 6, - "isExtraImport": true, - "importPath": "sys", - "description": "sys", - "detail": "sys", - "documentation": {} - }, - { - "label": "colored", - "importPath": "termcolor", - "description": "termcolor", - "isExtraImport": true, - "detail": "termcolor", - "documentation": {} - }, - { - "label": "colored", - "importPath": "termcolor", - "description": "termcolor", - "isExtraImport": true, - "detail": "termcolor", - "documentation": {} - }, - { - "label": "colored", - "importPath": "termcolor", - "description": "termcolor", - "isExtraImport": true, - "detail": "termcolor", - "documentation": {} - }, - { - "label": "xml.etree.ElementTree", - "kind": 6, - "isExtraImport": true, - "importPath": "xml.etree.ElementTree", - "description": "xml.etree.ElementTree", - "detail": "xml.etree.ElementTree", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "importPath": "patch_apk.config.constants", - "description": "patch_apk.config.constants", - "isExtraImport": true, - "detail": "patch_apk.config.constants", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "importPath": "patch_apk.config.constants", - "description": "patch_apk.config.constants", - "isExtraImport": true, - "detail": "patch_apk.config.constants", - "documentation": {} - }, - { - "label": "tempfile", - "kind": 6, - "isExtraImport": true, - "importPath": "tempfile", - "description": "tempfile", - "detail": "tempfile", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "re", - "kind": 6, - "isExtraImport": true, - "importPath": "re", - "description": "re", - "detail": "re", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "core.apk_tool", - "description": "core.apk_tool", - "isExtraImport": true, - "detail": "core.apk_tool", - "documentation": {} - }, - { - "label": "checkDependencies", - "importPath": "patch_apk.utils.dependencies", - "description": "patch_apk.utils.dependencies", - "isExtraImport": true, - "detail": "patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "checkDependencies", - "importPath": "patch_apk.utils.dependencies", - "description": "patch_apk.utils.dependencies", - "isExtraImport": true, - "detail": "patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "importPath": "patch_apk.utils.frida_objection", - "description": "patch_apk.utils.frida_objection", - "isExtraImport": true, - "detail": "patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "importPath": "patch_apk.utils.frida_objection", - "description": "patch_apk.utils.frida_objection", - "isExtraImport": true, - "detail": "patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "patchingWithObjection", - "importPath": "patch_apk.utils.frida_objection", - "description": "patch_apk.utils.frida_objection", - "isExtraImport": true, - "detail": "patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "getTargetAPK", - "importPath": "patch_apk.utils.get_target_apk", - "description": "patch_apk.utils.get_target_apk", - "isExtraImport": true, - "detail": "patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "getTargetAPK", - "importPath": "patch_apk.utils.get_target_apk", - "description": "patch_apk.utils.get_target_apk", - "isExtraImport": true, - "detail": "patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "importPath": "patch_apk.utils.get_apk_paths", - "description": "patch_apk.utils.get_apk_paths", - "isExtraImport": true, - "detail": "patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "importPath": "patch_apk.utils.get_apk_paths", - "description": "patch_apk.utils.get_apk_paths", - "isExtraImport": true, - "detail": "patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "verifyPackageName", - "importPath": "patch_apk.utils.verify_package_name", - "description": "patch_apk.utils.verify_package_name", - "isExtraImport": true, - "detail": "patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "verifyPackageName", - "importPath": "patch_apk.utils.verify_package_name", - "description": "patch_apk.utils.verify_package_name", - "isExtraImport": true, - "detail": "patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "patch_apk.utils.remove_duplicate_class", - "kind": 6, - "isExtraImport": true, - "importPath": "patch_apk.utils.remove_duplicate_class", - "description": "patch_apk.utils.remove_duplicate_class", - "detail": "patch_apk.utils.remove_duplicate_class", - "documentation": {} - }, - { - "label": "remove_duplicate_classes", - "importPath": "patch_apk.utils.remove_duplicate_class", - "description": "patch_apk.utils.remove_duplicate_class", - "isExtraImport": true, - "detail": "patch_apk.utils.remove_duplicate_class", - "documentation": {} - }, - { - "label": "Path", - "importPath": "pathlib", - "description": "pathlib", - "isExtraImport": true, - "detail": "pathlib", - "documentation": {} - }, - { - "label": "setup", - "importPath": "setuptools", - "description": "setuptools", - "isExtraImport": true, - "detail": "setuptools", - "documentation": {} - }, - { - "label": "find_packages", - "importPath": "setuptools", - "description": "setuptools", - "isExtraImport": true, - "detail": "setuptools", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "kind": 5, - "importPath": "build.lib.patch_apk.config.constants", - "description": "build.lib.patch_apk.config.constants", - "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", - "detail": "build.lib.patch_apk.config.constants", - "documentation": {} - }, - { - "label": "APKBuilder", - "kind": 6, - "importPath": "build.lib.patch_apk.core.apk_builder", - "description": "build.lib.patch_apk.core.apk_builder", - "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", - "detail": "build.lib.patch_apk.core.apk_builder", - "documentation": {} - }, - { - "label": "APKTool", - "kind": 6, - "importPath": "build.lib.patch_apk.core.apk_tool", - "description": "build.lib.patch_apk.core.apk_tool", - "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", - "detail": "build.lib.patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "detectProGuard", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.apk_detect_proguard", - "description": "build.lib.patch_apk.utils.apk_detect_proguard", - "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", - "detail": "build.lib.patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "getArgs", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.cli_tools", - "description": "build.lib.patch_apk.utils.cli_tools", - "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "build.lib.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.copy_split_apks", - "description": "build.lib.patch_apk.utils.copy_split_apks", - "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", - "detail": "build.lib.patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "checkDependencies", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.dependencies", - "description": "build.lib.patch_apk.utils.dependencies", - "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", - "detail": "build.lib.patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.disable_apk_split", - "description": "build.lib.patch_apk.utils.disable_apk_split", - "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", - "detail": "build.lib.patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.fix_private_resources", - "description": "build.lib.patch_apk.utils.fix_private_resources", - "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", - "detail": "build.lib.patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.fix_resource_id", - "description": "build.lib.patch_apk.utils.fix_resource_id", - "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", - "detail": "build.lib.patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.frida_objection", - "description": "build.lib.patch_apk.utils.frida_objection", - "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", - "detail": "build.lib.patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.get_apk_paths", - "description": "build.lib.patch_apk.utils.get_apk_paths", - "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", - "detail": "build.lib.patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "getTargetAPK", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.get_target_apk", - "description": "build.lib.patch_apk.utils.get_target_apk", - "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", - "detail": "build.lib.patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "rawREReplace", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.raw_re_replace", - "description": "build.lib.patch_apk.utils.raw_re_replace", - "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", - "detail": "build.lib.patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.remove_duplicate_style", - "description": "build.lib.patch_apk.utils.remove_duplicate_style", - "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", - "detail": "build.lib.patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "verifyPackageName", - "kind": 2, - "importPath": "build.lib.patch_apk.utils.verify_package_name", - "description": "build.lib.patch_apk.utils.verify_package_name", - "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", - "detail": "build.lib.patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "main", - "kind": 2, - "importPath": "build.lib.patch_apk.main", - "description": "build.lib.patch_apk.main", - "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", - "detail": "build.lib.patch_apk.main", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "kind": 5, - "importPath": "src.patch_apk.config.constants", - "description": "src.patch_apk.config.constants", - "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", - "detail": "src.patch_apk.config.constants", - "documentation": {} - }, - { - "label": "APKBuilder", - "kind": 6, - "importPath": "src.patch_apk.core.apk_builder", - "description": "src.patch_apk.core.apk_builder", - "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", - "detail": "src.patch_apk.core.apk_builder", - "documentation": {} - }, - { - "label": "APKTool", - "kind": 6, - "importPath": "src.patch_apk.core.apk_tool", - "description": "src.patch_apk.core.apk_tool", - "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", - "detail": "src.patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "detectProGuard", - "kind": 2, - "importPath": "src.patch_apk.utils.apk_detect_proguard", - "description": "src.patch_apk.utils.apk_detect_proguard", - "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", - "detail": "src.patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "getArgs", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "kind": 2, - "importPath": "src.patch_apk.utils.cli_tools", - "description": "src.patch_apk.utils.cli_tools", - "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "src.patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "kind": 2, - "importPath": "src.patch_apk.utils.copy_split_apks", - "description": "src.patch_apk.utils.copy_split_apks", - "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", - "detail": "src.patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "checkDependencies", - "kind": 2, - "importPath": "src.patch_apk.utils.dependencies", - "description": "src.patch_apk.utils.dependencies", - "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", - "detail": "src.patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "kind": 2, - "importPath": "src.patch_apk.utils.disable_apk_split", - "description": "src.patch_apk.utils.disable_apk_split", - "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", - "detail": "src.patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "kind": 2, - "importPath": "src.patch_apk.utils.fix_private_resources", - "description": "src.patch_apk.utils.fix_private_resources", - "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", - "detail": "src.patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "kind": 2, - "importPath": "src.patch_apk.utils.fix_resource_id", - "description": "src.patch_apk.utils.fix_resource_id", - "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", - "detail": "src.patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "kind": 2, - "importPath": "src.patch_apk.utils.frida_objection", - "description": "src.patch_apk.utils.frida_objection", - "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", \"--only-main-classes\", apkfile, \"-o\", apkdir,])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", - "detail": "src.patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "patchingWithObjection", - "kind": 2, - "importPath": "src.patch_apk.utils.frida_objection", - "description": "src.patch_apk.utils.frida_objection", - "peekOfCode": "def patchingWithObjection(apkfile):\n # Patch the target APK with objection\n print(\"[+] Patching \" + apkfile.split(os.sep)[-1] + \" with objection.\")\n warningPrint(\"[!] The application will be patched with Frida 16.7.19. See https://github.com/sensepost/objection/issues/737 for more information.\")\n if subprocess.run([\"objection\", \"patchapk\", \"-V\", \"16.7.19\", \"--skip-resources\", \"--ignore-nativelibs\", \"-s\", apkfile], capture_output=True).returncode != 0:\n print(\"[+] Objection patching failed, trying alternative approach\")\n warningPrint(\"[!] If you get an error, the application might not have a launchable activity\")\n # Try without --skip-resources, since objection potentially wasn't able to identify the starting activity\n # There could have been another reason for the failure, but it's a sensible fallback\n # Another reason could be a missing INTERNET permission", - "detail": "src.patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "kind": 2, - "importPath": "src.patch_apk.utils.get_apk_paths", - "description": "src.patch_apk.utils.get_apk_paths", - "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", - "detail": "src.patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "getTargetAPK", - "kind": 2, - "importPath": "src.patch_apk.utils.get_target_apk", - "description": "src.patch_apk.utils.get_target_apk", - "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", - "detail": "src.patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "rawREReplace", - "kind": 2, - "importPath": "src.patch_apk.utils.raw_re_replace", - "description": "src.patch_apk.utils.raw_re_replace", - "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", - "detail": "src.patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "remove_duplicate_classes", - "kind": 2, - "importPath": "src.patch_apk.utils.remove_duplicate_class", - "description": "src.patch_apk.utils.remove_duplicate_class", - "peekOfCode": "def remove_duplicate_classes(apkdir):\n \"\"\"\n Remove duplicate/conflicting smali classes from smali_assets directory.\n This prevents 'has already been interned' errors during APK rebuild.\n \"\"\"\n smali_assets = os.path.join(apkdir, \"smali_assets\")\n if not os.path.exists(smali_assets):\n print(\"[+] No smali_assets directory found, skipping duplicate check\")\n return\n print(\"[+] Scanning for conflicting smali classes...\")", - "detail": "src.patch_apk.utils.remove_duplicate_class", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "kind": 2, - "importPath": "src.patch_apk.utils.remove_duplicate_style", - "description": "src.patch_apk.utils.remove_duplicate_style", - "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", - "detail": "src.patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "verifyPackageName", - "kind": 2, - "importPath": "src.patch_apk.utils.verify_package_name", - "description": "src.patch_apk.utils.verify_package_name", - "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", - "detail": "src.patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "main", - "kind": 2, - "importPath": "src.patch_apk.main", - "description": "src.patch_apk.main", - "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", - "detail": "src.patch_apk.main", - "documentation": {} - }, - { - "label": "main", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")\ndef fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "checkDependencies", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "getArgs", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "dbgPrint", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\n####################\n# Abort will print given error message and exit the app", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "warningPrint", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\n####################\n# Abort will print given error message and exit the app\n####################\ndef abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\n####################\n# Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "abort", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\n####################\n# Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.\n####################\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "getStdout", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\n####################\n# Get apktool version\n####################\ndef getApktoolVersion():\n commands = [[\"version\"], [\"v\"], [\"-version\"], [\"-v\"]] ", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "getApktoolVersion", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def getApktoolVersion():\n commands = [[\"version\"], [\"v\"], [\"-version\"], [\"-v\"]] \n for cmd in commands:\n try:\n result = runApkTool(cmd)\n if result[\"returncode\"] != 0:\n continue\n version_output = result[\"stdout\"].strip().split(\"\\n\")[0].strip()\n version_str = version_output.split(\"-\")[0].strip()\n return parse_version(version_str)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "runApkTool", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def runApkTool(params):\n exe = \"apktool.bat\" if os.name == \"nt\" else \"apktool\"\n # Feed \"\\r\\n\" so apktool.bat's `pause` won’t block on Windows.\n cp = subprocess.run(\n [exe, *params],\n input=\"\\r\\n\", # Should be harmless on linux\n text=True,\n capture_output=True,\n check=False,\n )", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "build", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")\n####################\n# Sign the APK with apksigner and zip align\n# Fixes https://github.com/NickstaDB/patch-apk/issues/31 by no longer using jarsigner (V1 APK signatures)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "signAndZipAlign", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def signAndZipAlign(baseapkdir, baseapkfilename):\n # Zip align the new APK\n verbosePrint(\"[+] Zip aligning new APK.\")\n assertSubprocessSuccessfulRun([\"zipalign\", \"-f\", \"4\", \"-p\", os.path.join(baseapkdir, \"dist\", baseapkfilename),\n os.path.join(baseapkdir, \"dist\", baseapkfilename[:-4] + \"-aligned.apk\")])\n shutil.move(os.path.join(baseapkdir, \"dist\", baseapkfilename[:-4] + \"-aligned.apk\"), os.path.join(baseapkdir, \"dist\", baseapkfilename))\n # Sign the new APK\n verbosePrint(\"[+] Signing new APK.\")\n apkpath = os.path.join(baseapkdir, \"dist\", baseapkfilename)\n assertSubprocessSuccessfulRun([\"objection\", \"signapk\", apkpath])", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "verifyPackageName", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "getTargetAPK", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "verbosePrint", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\n####################\n# Combine app bundles/split APKs into a single APK for patching.\n####################\ndef combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only):\n warningPrint(\"[!] App bundle/split APK detected, rebuilding as a single APK.\")\n # Extract the individual APKs", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "combineSplitAPKs", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only):\n warningPrint(\"[!] App bundle/split APK detected, rebuilding as a single APK.\")\n # Extract the individual APKs\n baseapkdir = os.path.join(tmppath, pkgname + \"-base\")\n baseapkfilename = pkgname + \"-base.apk\"\n splitapkpaths = []\n bar = Bar('[+] Disassembling split APKs', max=len(localapks))\n verboseOutput = \"\"\n for apkpath in localapks:\n verboseOutput += \"\\nExtracted: \" + apkpath", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "detectProGuard", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return\n # Duplicates\n dupes = []\n # Parse styles.xml and find all elements with duplicate names\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\"))\n for styleEl in tree.getroot().findall(\"style\"):\n itemNames = []", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])])\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "rawREReplace", - "kind": 2, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "kind": 5, - "importPath": "patch-apk", - "description": "patch-apk", - "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"\n####################\n# Main()\n####################\ndef main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version", - "detail": "patch-apk", - "documentation": {} - }, - { - "label": "read_requirements", - "kind": 2, - "importPath": "setup", - "description": "setup", - "peekOfCode": "def read_requirements(req_file: Path):\n if not req_file.exists():\n return []\n lines = req_file.read_text(encoding=\"utf-8\").splitlines()\n reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith(\"#\")]\n return reqs\ndef get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"\n text = pkg_init.read_text(encoding=\"utf-8\")", - "detail": "setup", - "documentation": {} - }, - { - "label": "get_version", - "kind": 2, - "importPath": "setup", - "description": "setup", - "peekOfCode": "def get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"\n text = pkg_init.read_text(encoding=\"utf-8\")\n m = re.search(r\"__version__\\s*=\\s*['\\\"]([^'\\\"]+)['\\\"]\", text)\n return m.group(1) if m else \"0.0.0\"\nlong_description = \"\"\nreadme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")", - "detail": "setup", - "documentation": {} - }, - { - "label": "HERE", - "kind": 5, - "importPath": "setup", - "description": "setup", - "peekOfCode": "HERE = Path(__file__).parent\ndef read_requirements(req_file: Path):\n if not req_file.exists():\n return []\n lines = req_file.read_text(encoding=\"utf-8\").splitlines()\n reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith(\"#\")]\n return reqs\ndef get_version(pkg_init: Path):\n if not pkg_init.exists():\n return \"0.0.0\"", - "detail": "setup", - "documentation": {} - }, - { - "label": "long_description", - "kind": 5, - "importPath": "setup", - "description": "setup", - "peekOfCode": "long_description = \"\"\nreadme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")\ninstall_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,", - "detail": "setup", - "documentation": {} - }, - { - "label": "readme_file", - "kind": 5, - "importPath": "setup", - "description": "setup", - "peekOfCode": "readme_file = HERE / \"README.md\"\nif readme_file.exists():\n long_description = readme_file.read_text(encoding=\"utf-8\")\ninstall_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,\n long_description_content_type=\"text/markdown\",", - "detail": "setup", - "documentation": {} - }, - { - "label": "install_requires", - "kind": 5, - "importPath": "setup", - "description": "setup", - "peekOfCode": "install_requires = read_requirements(HERE / \"requirements.txt\")\nsetup(\n name=\"patch-apk\",\n version=get_version(HERE / \"src\" / \"patch_apk\" / \"__init__.py\"),\n description=\"App Bundle / Split APK aware patcher for objection\",\n long_description=long_description,\n long_description_content_type=\"text/markdown\",\n packages=find_packages(where=\"src\"),\n package_dir={\"\": \"src\"},\n include_package_data=True,", - "detail": "setup", - "documentation": {} - } -] \ No newline at end of file From 88b5b34a5ea42ed43cece007248cf3ee612aa675 Mon Sep 17 00:00:00 2001 From: Urten Date: Sun, 26 Oct 2025 16:18:19 +0530 Subject: [PATCH 7/8] removed .vscode from tracking --- .../PythonImportHelper-v2-Completion.json | 681 ------------------ 1 file changed, 681 deletions(-) delete mode 100644 src/.vscode/PythonImportHelper-v2-Completion.json diff --git a/src/.vscode/PythonImportHelper-v2-Completion.json b/src/.vscode/PythonImportHelper-v2-Completion.json deleted file mode 100644 index f25bd00..0000000 --- a/src/.vscode/PythonImportHelper-v2-Completion.json +++ /dev/null @@ -1,681 +0,0 @@ -[ - { - "label": "os", - "kind": 6, - "isExtraImport": true, - "importPath": "os", - "description": "os", - "detail": "os", - "documentation": {} - }, - { - "label": "shutil", - "kind": 6, - "isExtraImport": true, - "importPath": "shutil", - "description": "shutil", - "detail": "shutil", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getArgs", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "isExtraImport": true, - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "importPath": "patch_apk.utils.fix_private_resources", - "description": "patch_apk.utils.fix_private_resources", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "subprocess", - "kind": 6, - "isExtraImport": true, - "importPath": "subprocess", - "description": "subprocess", - "detail": "subprocess", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "Bar", - "importPath": "progress.bar", - "description": "progress.bar", - "isExtraImport": true, - "detail": "progress.bar", - "documentation": {} - }, - { - "label": "parse", - "importPath": "packaging.version", - "description": "packaging.version", - "isExtraImport": true, - "detail": "packaging.version", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "rawREReplace", - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "isExtraImport": true, - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "importPath": "patch_apk.utils.disable_apk_split", - "description": "patch_apk.utils.disable_apk_split", - "isExtraImport": true, - "detail": "patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "importPath": "patch_apk.utils.remove_duplicate_style", - "description": "patch_apk.utils.remove_duplicate_style", - "isExtraImport": true, - "detail": "patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "importPath": "patch_apk.utils.fix_resource_id", - "description": "patch_apk.utils.fix_resource_id", - "isExtraImport": true, - "detail": "patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "detectProGuard", - "importPath": "patch_apk.utils.apk_detect_proguard", - "description": "patch_apk.utils.apk_detect_proguard", - "isExtraImport": true, - "detail": "patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "importPath": "patch_apk.utils.copy_split_apks", - "description": "patch_apk.utils.copy_split_apks", - "isExtraImport": true, - "detail": "patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "argparse", - "kind": 6, - "isExtraImport": true, - "importPath": "argparse", - "description": "argparse", - "detail": "argparse", - "documentation": {} - }, - { - "label": "sys", - "kind": 6, - "isExtraImport": true, - "importPath": "sys", - "description": "sys", - "detail": "sys", - "documentation": {} - }, - { - "label": "colored", - "importPath": "termcolor", - "description": "termcolor", - "isExtraImport": true, - "detail": "termcolor", - "documentation": {} - }, - { - "label": "xml.etree.ElementTree", - "kind": 6, - "isExtraImport": true, - "importPath": "xml.etree.ElementTree", - "description": "xml.etree.ElementTree", - "detail": "xml.etree.ElementTree", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "importPath": "patch_apk.config.constants", - "description": "patch_apk.config.constants", - "isExtraImport": true, - "detail": "patch_apk.config.constants", - "documentation": {} - }, - { - "label": "tempfile", - "kind": 6, - "isExtraImport": true, - "importPath": "tempfile", - "description": "tempfile", - "detail": "tempfile", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "APKTool", - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "isExtraImport": true, - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "re", - "kind": 6, - "isExtraImport": true, - "importPath": "re", - "description": "re", - "detail": "re", - "documentation": {} - }, - { - "label": "checkDependencies", - "importPath": "patch_apk.utils.dependencies", - "description": "patch_apk.utils.dependencies", - "isExtraImport": true, - "detail": "patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "importPath": "patch_apk.utils.frida_objection", - "description": "patch_apk.utils.frida_objection", - "isExtraImport": true, - "detail": "patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "getTargetAPK", - "importPath": "patch_apk.utils.get_target_apk", - "description": "patch_apk.utils.get_target_apk", - "isExtraImport": true, - "detail": "patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "importPath": "patch_apk.utils.get_apk_paths", - "description": "patch_apk.utils.get_apk_paths", - "isExtraImport": true, - "detail": "patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "verifyPackageName", - "importPath": "patch_apk.utils.verify_package_name", - "description": "patch_apk.utils.verify_package_name", - "isExtraImport": true, - "detail": "patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "NULL_DECODED_DRAWABLE_COLOR", - "kind": 5, - "importPath": "patch_apk.config.constants", - "description": "patch_apk.config.constants", - "peekOfCode": "NULL_DECODED_DRAWABLE_COLOR = \"#000000ff\"", - "detail": "patch_apk.config.constants", - "documentation": {} - }, - { - "label": "APKBuilder", - "kind": 6, - "importPath": "patch_apk.core.apk_builder", - "description": "patch_apk.core.apk_builder", - "peekOfCode": "class APKBuilder:\n \"\"\"Handles APK building and rebuilding operations.\"\"\"\n @staticmethod\n def build(baseapkdir):\n # Fix private resources preventing builds (apktool wontfix: https://github.com/iBotPeaches/Apktool/issues/2761)\n fixPrivateResources(baseapkdir)\n verbosePrint(\"[+] Rebuilding APK with apktool.\")\n result = APKTool.runApkTool([\"b\", baseapkdir])\n if result[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool b \" + baseapkdir + \"'.\\nRun with --debug-output for more information.\")", - "detail": "patch_apk.core.apk_builder", - "documentation": {} - }, - { - "label": "APKTool", - "kind": 6, - "importPath": "patch_apk.core.apk_tool", - "description": "patch_apk.core.apk_tool", - "peekOfCode": "class APKTool:\n '''\n ApkTool class for handling APK files.\n This class provides a way to interface with apktool, a powerful tool for\n decompiling and recompiling Android APK files. It also provides helper\n functions for common operations when working with APK files.\n Attributes:\n None\n Methods:\n runApkTool(params): Run apktool with the given parameters.", - "detail": "patch_apk.core.apk_tool", - "documentation": {} - }, - { - "label": "detectProGuard", - "kind": 2, - "importPath": "patch_apk.utils.apk_detect_proguard", - "description": "patch_apk.utils.apk_detect_proguard", - "peekOfCode": "def detectProGuard(extractedPath):\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"proguard\")):\n return True\n if os.path.exists(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\")):\n fh = open(os.path.join(extractedPath, \"original\", \"META-INF\", \"MANIFEST.MF\"))\n d = fh.read()\n fh.close()\n if \"proguard\" in d.lower():\n return True\n return False", - "detail": "patch_apk.utils.apk_detect_proguard", - "documentation": {} - }, - { - "label": "getArgs", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def getArgs():\n # Only parse args once\n if not hasattr(getArgs, \"parsed_args\"):\n # Parse the command line\n parser = argparse.ArgumentParser(\n description=\"patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs.\"\n )\n parser.add_argument(\"--no-enable-user-certs\", help=\"Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.\", action=\"store_true\")\n parser.add_argument(\"--save-apk\", help=\"Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.\")\n parser.add_argument(\"--extract-only\", help=\"Disable including objection and pushing modified APK to device.\", action=\"store_true\")", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "abort", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def abort(msg):\n print(colored(msg, \"red\"))\n sys.exit(1)\ndef verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "verbosePrint", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def verbosePrint(msg):\n if getArgs().verbose:\n for line in msg.split(\"\\n\"):\n print(colored(\" \" + line, \"light_grey\"))\ndef dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "dbgPrint", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def dbgPrint(msg):\n if getArgs().debug_output:\n print(msg)\n####################\n# Warning print\n####################\ndef warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "warningPrint", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def warningPrint(msg):\n print(colored(msg, \"yellow\"))\ndef getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "getStdout", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def getStdout():\n if getArgs().debug_output:\n return None\n else:\n return subprocess.DEVNULL\ndef assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "assertSubprocessSuccessfulRun", - "kind": 2, - "importPath": "patch_apk.utils.cli_tools", - "description": "patch_apk.utils.cli_tools", - "peekOfCode": "def assertSubprocessSuccessfulRun(args):\n if subprocess.run(args, stdout=getStdout(), stderr=getStdout()).returncode != 0:\n abort(f\"Error: Failed to run {' '.join(args)}.\\nRun with --debug-output for more information.\")", - "detail": "patch_apk.utils.cli_tools", - "documentation": {} - }, - { - "label": "copySplitApkFiles", - "kind": 2, - "importPath": "patch_apk.utils.copy_split_apks", - "description": "patch_apk.utils.copy_split_apks", - "peekOfCode": "def copySplitApkFiles(baseapkdir, splitapkpaths):\n for apkdir in splitapkpaths:\n for (root, dirs, files) in os.walk(apkdir):\n # Skip the original files directory\n if not root.startswith(os.path.join(apkdir, \"original\")):\n # Create any missing directories\n for d in dirs:\n # Translate directory path to base APK path and create the directory if it doesn't exist\n p = baseapkdir + os.path.join(root, d)[len(apkdir):]\n if not os.path.exists(p):", - "detail": "patch_apk.utils.copy_split_apks", - "documentation": {} - }, - { - "label": "checkDependencies", - "kind": 2, - "importPath": "patch_apk.utils.dependencies", - "description": "patch_apk.utils.dependencies", - "peekOfCode": "def checkDependencies(extract_only):\n deps = [\"adb\", \"apktool\", \"aapt\"]\n if not extract_only:\n deps += [\"objection\", \"zipalign\", \"apksigner\"]\n missing = []\n for dep in deps:\n if shutil.which(dep) is None:\n missing.append(dep)\n if len(missing) > 0:\n abort(\"Error, missing dependencies, ensure the following commands are available on the PATH: \" + (\", \".join(missing)))", - "detail": "patch_apk.utils.dependencies", - "documentation": {} - }, - { - "label": "disableApkSplitting", - "kind": 2, - "importPath": "patch_apk.utils.disable_apk_split", - "description": "patch_apk.utils.disable_apk_split", - "peekOfCode": "def disableApkSplitting(baseapkdir):\n verbosePrint(\"[+] Disabling APK splitting in AndroidManifest.xml of base APK.\")\n # Load AndroidManifest.xml\n tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, \"AndroidManifest.xml\"))\n # Register the namespaces and get the prefix for the \"android\" namespace\n namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, \"AndroidManifest.xml\"), events=[\"start-ns\"])]) # pyright: ignore[reportArgumentType]\n for ns in namespaces:\n xml.etree.ElementTree.register_namespace(ns, namespaces[ns])\n ns = \"{\" + namespaces[\"android\"] + \"}\"\n # Disable APK splitting", - "detail": "patch_apk.utils.disable_apk_split", - "documentation": {} - }, - { - "label": "fixPrivateResources", - "kind": 2, - "importPath": "patch_apk.utils.fix_private_resources", - "description": "patch_apk.utils.fix_private_resources", - "peekOfCode": "def fixPrivateResources(baseapkdir):\n verbosePrint(\"[+] Forcing all private resources to be public\")\n updated = 0\n for (root, dirs, files) in os.walk(os.path.join(baseapkdir, \"res\")):\n for f in files:\n if f.lower().endswith(\".xml\"):\n rawREReplace(os.path.join(root, f), '@android', '@*android')\n updated += 1\n if updated > 0:\n verbosePrint(\"[+] Updated \" + str(updated) + \" private resources before building APK.\")", - "detail": "patch_apk.utils.fix_private_resources", - "documentation": {} - }, - { - "label": "fixPublicResourceIDs", - "kind": 2, - "importPath": "patch_apk.utils.fix_resource_id", - "description": "patch_apk.utils.fix_resource_id", - "peekOfCode": "def fixPublicResourceIDs(baseapkdir, splitapkpaths):\n # Bail if the base APK does not have a public.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"public.xml\")):\n return\n verbosePrint(\"[+] Found public.xml in the base APK, fixing resource identifiers across split APKs.\")\n # Mappings of resource IDs and names\n idToDummyName = {}\n dummyNameToRealName = {}\n # Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.\n # Load these into the lookup tables ready to resolve the real resource names from", - "detail": "patch_apk.utils.fix_resource_id", - "documentation": {} - }, - { - "label": "fixAPKBeforeObjection", - "kind": 2, - "importPath": "patch_apk.utils.frida_objection", - "description": "patch_apk.utils.frida_objection", - "peekOfCode": "def fixAPKBeforeObjection(apkfile, fix_network_security_config):\n print(\"[+] Prepping AndroidManifest.xml\")\n with tempfile.TemporaryDirectory() as tmppath:\n apkdir = os.path.join(tmppath, \"apk\")\n ret = APKTool.runApkTool([\"d\", apkfile, \"-o\", apkdir])\n if ret[\"returncode\"] != 0:\n abort(\"Error: Failed to run 'apktool d \" + apkfile + \" -o \" + apkdir + \"'.\\nRun with --debug-output for more information.\")\n # Load AndroidManifest.xml\n manifestPath = os.path.join(apkdir, \"AndroidManifest.xml\")\n tree = xml.etree.ElementTree.parse(manifestPath)", - "detail": "patch_apk.utils.frida_objection", - "documentation": {} - }, - { - "label": "getAPKPathsForPackage", - "kind": 2, - "importPath": "patch_apk.utils.get_apk_paths", - "description": "patch_apk.utils.get_apk_paths", - "peekOfCode": "def getAPKPathsForPackage(pkgname, current_user = \"0\", users_to_try = None):\n print(f\"[+] Retrieving APK path(s) for package: {pkgname} for user {current_user}\")\n paths = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"path\", \"--user\", current_user, pkgname], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n if not users_to_try:\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"users\"], stdout=subprocess.PIPE)\n out = proc.stdout.decode(\"utf-8\")\n pattern = r'UserInfo{(\\d+):'\n users_to_try = re.findall(pattern, out)", - "detail": "patch_apk.utils.get_apk_paths", - "documentation": {} - }, - { - "label": "getTargetAPK", - "kind": 2, - "importPath": "patch_apk.utils.get_target_apk", - "description": "patch_apk.utils.get_target_apk", - "peekOfCode": "def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only):\n # Pull the APKs from the device\n bar = Bar('[+] Pulling APK file(s) from device', max=len(apkpaths))\n verboseOutput = \"\"\n localapks = []\n for remotepath in apkpaths:\n baseapkname = remotepath.split('/')[-1]\n localapks.append(os.path.join(tmppath, pkgname + \"-\" + baseapkname))\n verboseOutput += f\"[+] Pulled: {pkgname}-{baseapkname}\"\n bar.next()", - "detail": "patch_apk.utils.get_target_apk", - "documentation": {} - }, - { - "label": "rawREReplace", - "kind": 2, - "importPath": "patch_apk.utils.raw_re_replace", - "description": "patch_apk.utils.raw_re_replace", - "peekOfCode": "def rawREReplace(path, pattern, replacement):\n if os.path.exists(path):\n contents = \"\"\n with open(path, 'r') as file:\n contents = file.read()\n newContents = re.sub(pattern, replacement, contents)\n if (contents != newContents):\n dbgPrint(\"[~] Patching \" + path)\n with open(path, 'w') as file:\n file.write(newContents)", - "detail": "patch_apk.utils.raw_re_replace", - "documentation": {} - }, - { - "label": "hackRemoveDuplicateStyleEntries", - "kind": 2, - "importPath": "patch_apk.utils.remove_duplicate_style", - "description": "patch_apk.utils.remove_duplicate_style", - "peekOfCode": "def hackRemoveDuplicateStyleEntries(baseapkdir):\n # Bail if there is no styles.xml\n if not os.path.exists(os.path.join(baseapkdir, \"res\", \"values\", \"styles.xml\")):\n return", - "detail": "patch_apk.utils.remove_duplicate_style", - "documentation": {} - }, - { - "label": "verifyPackageName", - "kind": 2, - "importPath": "patch_apk.utils.verify_package_name", - "description": "patch_apk.utils.verify_package_name", - "peekOfCode": "def verifyPackageName(pkgname):\n # Get a list of installed packages matching the given name\n packages = []\n proc = subprocess.run([\"adb\", \"shell\", \"pm\", \"list\", \"packages\"], stdout=subprocess.PIPE)\n if proc.returncode != 0:\n abort(\"Error: Failed to run 'adb shell pm list packages'.\")\n out = proc.stdout.decode(\"utf-8\")\n for line in out.split(os.linesep):\n if line.startswith(\"package:\"):\n line = line[8:].strip()", - "detail": "patch_apk.utils.verify_package_name", - "documentation": {} - }, - { - "label": "main", - "kind": 2, - "importPath": "patch_apk.main", - "description": "patch_apk.main", - "peekOfCode": "def main():\n # Grab argz\n args = getArgs()\n # Check that dependencies are available\n checkDependencies(args.extract_only)\n # Warn for unexpected version\n apktoolVersion = APKTool.getApktoolVersion()\n print(f\"Using apktool v{apktoolVersion}\")\n # Verify the package name and ensure it's installed (also supports partial package names)\n pkgname = verifyPackageName(args.pkgname)", - "detail": "patch_apk.main", - "documentation": {} - } -] \ No newline at end of file From 8b5496ebe34add220b769ba7370ea14c40767f94 Mon Sep 17 00:00:00 2001 From: Nandan Kumar Kashyap Date: Sun, 26 Oct 2025 16:41:01 +0530 Subject: [PATCH 8/8] fixed minor setup issues and updated readme --- README.md | 18 +++++++++--------- requirements.txt | Bin 376 -> 172 bytes setup.py | 28 ++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f5cdb49..48bc3fd 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ The `{package-name}` parameter can be a fully-qualified package name such as `co By default the tool will inject the Frida gadget and enable support for user-installed CA certificates by modifying the app's network security config. To disable the network cert modification, pass `--no-enable-user-certs` on the command line. ### Examples ### -**Basic usage:** Simply install the target Android app on your device, make sure `adb devices` can see your device, then pass the package name to `patch-apk.py`. +**Basic usage:** Simply install the target Android app on your device, make sure `adb devices` can see your device, then pass the package name to `patch-apk`. ``` -$ python3 patch-apk.py com.whatsapp +$ patch-apk com.whatsapp Getting APK path(s) for package: com.whatsapp [+] APK path: /data/app/com.whatsapp-NKLgchoExRFTDLkkbDqBGg==/base.apk @@ -93,12 +93,12 @@ Installing the patched APK to the device. Done, cleaning up temporary files. ``` -When `patch-apk.py` is done, the installed app should be patched with objection and have support for user-installed CA certificates enabled. Launch the app on the device and run `objection explore` as you normally would to connect to the agent. +When `patch-apk` is done, the installed app should be patched with objection and have support for user-installed CA certificates enabled. Launch the app on the device and run `objection explore` as you normally would to connect to the agent. -**Partial Package Name Matching:** Pass a partial package name to `patch-apk.py` and it'll automatically grab the correct package name or ask you to confirm from available options. +**Partial Package Name Matching:** Pass a partial package name to `patch-apk` and it'll automatically grab the correct package name or ask you to confirm from available options. ``` -$ python3 patch-apk.py proxy +$ patch-apk proxy [!] Multiple matching packages installed, select the package to patch. @@ -123,7 +123,7 @@ package:/data/app/~~TP7sglBuEoDc3yH0wpZdiA==/org.proxydroid-PCy1JxTMVJT3KmxVqaag The following shows `patch-apk.py` detecting, rebuilding, and patching a split APK. Some output has been snipped for brevity. The `-v` flag has been set to show additional info. ``` -$ python3 patch-apk.py org.proxydroid -v +$ patch-apk org.proxydroid -v [+] Retrieving APK path(s) for package: org.proxydroid [+] APK path: /data/app/~~FTVBmscrJiLerJdXIEa5tw==/org.proxydroid-KMq91nU1y9Qz8ZZAGM--RA==/base.apk @@ -150,7 +150,7 @@ $ python3 patch-apk.py org.proxydroid -v Extracted: /var/folders/t3/vz305z151ng8y2rwvpkx28xw0000gn/T/tmpyw7wl64i/org.proxydroid-split_config.en.apk Extracted: /var/folders/t3/vz305z151ng8y2rwvpkx28xw0000gn/T/tmpyw7wl64i/org.proxydroid-split_config.fr.apk Extracted: /var/folders/t3/vz305z151ng8y2rwvpkx28xw0000gn/T/tmpyw7wl64i/org.proxydroid-split_config.nl.apk - Extracted: /var/folders/t3/vz305z151ng8y2rwvpkx28xw0000gn/T/tmpyw7wl64i/org.proxydroid-split_config.xxhdpi.apk + Extracted: /var/folders/t3/vz305z151ng8y2rwvpkx28xw0000gn/T/tmpyw7wl64xk/org.proxydroid-split_config.xxhdpi.apk [+] Rebuilding as a single APK @@ -191,7 +191,7 @@ package:/data/app/org.proxydroid-9NuZnT-lK3qM_IZQEHhTgA==/base.apk By default, patch-apk will inject the frida gadget and modify the network security config. It is also possible to only perform an extraction by providing the `--extract-only` flag. Any split apks will still be merged and a local copy of the APK will be produced: ``` -$ python3 patch-apk.py org.proxydroid --extract-only +$ patch-apk org.proxydroid --extract-only [+] Retrieving APK path(s) for package: org.proxydroid @@ -219,7 +219,7 @@ package:/data/app/~~TP7sglBuEoDc3yH0wpZdiA==/org.proxydroid-PCy1JxTMVJT3KmxVqaag package:/data/app/~~TP7sglBuEoDc3yH0wpZdiA==/org.proxydroid-PCy1JxTMVJT3KmxVqaagGQ==/split_config.xxhdpi.apk ``` -These can be combined into a single APK for use with other tools such as `objection patchapk`. This is done by `patch-apk.py` as follows: +These can be combined into a single APK for use with other tools such as `objection patchapk`. This is done by `patch-apk` as follows: **Step 1 - Extract APKs:** First, the individual APK files are pulled from the device and extracted using `apktool`. diff --git a/requirements.txt b/requirements.txt index cb7a9c1d16d52235697dab029c081ef1af7aa123..f80425f077721ce77d38ba0957283dfb0a23f267 100644 GIT binary patch delta 9 Qcmeytw1#nl%ES*H02KxVWdHyG literal 376 zcmYk2-3o$06oltG=pBMCLv72yf}oe^I?A*n%SDaezWR1Ch+z-woH=K9{JgCWDs-cs zb=8Bq0ShGxHP=`p1u8Z1-Ci?Y)B~$dYem3K^{`JJnJCog4!cteTe2@gd*EK63AkCj z!eMI5Os&AKOW_k`^eU}TCulNmuDjw?a24G-{1neK*!;Qf2UnNH z@~(z{i>?DMtKmH<$N01zot`w7+JJgc str: + """Read a text file trying several encodings to avoid UnicodeDecodeError. + + Tries utf-8, utf-8-sig, utf-16, and latin-1 in that order. If the file + doesn't exist returns an empty string. + """ + if not p.exists(): + return "" + encodings = ("utf-8", "utf-8-sig", "utf-16", "latin-1") + data = p.read_bytes() + for enc in encodings: + try: + return data.decode(enc) + except (UnicodeDecodeError, LookupError): + continue + # as a last resort, decode with latin-1 replacing errors to avoid failing the build + return data.decode("latin-1", errors="replace") + + def read_requirements(req_file: Path): - if not req_file.exists(): + text = _safe_read_text(req_file) + if not text: return [] - lines = req_file.read_text(encoding="utf-8").splitlines() + lines = text.splitlines() reqs = [l.strip() for l in lines if l.strip() and not l.strip().startswith("#")] return reqs @@ -17,7 +37,7 @@ def read_requirements(req_file: Path): def get_version(pkg_init: Path): if not pkg_init.exists(): return "0.0.0" - text = pkg_init.read_text(encoding="utf-8") + text = _safe_read_text(pkg_init) m = re.search(r"__version__\s*=\s*['\"]([^'\"]+)['\"]", text) return m.group(1) if m else "0.0.0" @@ -25,7 +45,7 @@ def get_version(pkg_init: Path): long_description = "" readme_file = HERE / "README.md" if readme_file.exists(): - long_description = readme_file.read_text(encoding="utf-8") + long_description = _safe_read_text(readme_file) install_requires = read_requirements(HERE / "requirements.txt")