├── tests ├── __init__.py └── test_cli.py ├── images ├── carbon.png └── decompile.png ├── requirements.txt ├── pypi.sh ├── .gitignore ├── scripts ├── __version__.py ├── __init__.py ├── logger.py ├── apk_utils.py ├── frida_github.py ├── uber_apk_signer_github.py └── cli.py ├── .dockerignore ├── .github └── workflows │ └── contributors.yml ├── LICENSE ├── Dockerfile ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahwul/frida-gadget/trunk/images/carbon.png -------------------------------------------------------------------------------- /images/decompile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahwul/frida-gadget/trunk/images/decompile.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | androguard >= 4.0.0 3 | apk-signer 4 | pytest 5 | colorlog 6 | coverage 7 | pylint 8 | requests 9 | -------------------------------------------------------------------------------- /pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf dist/* && 3 | python setup.py sdist && 4 | twine check dist/* && 5 | twine upload -r frida-gadget dist/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.pyc 4 | venv/ 5 | .DS_Store 6 | *.egg-info 7 | clean.sh 8 | test/demo-apk/* 9 | !test/demo-apk/*.apk 10 | .coverage 11 | scripts/files/ 12 | .env 13 | /.idea 14 | test_apks 15 | .vscode 16 | docker.sh 17 | -------------------------------------------------------------------------------- /scripts/__version__.py: -------------------------------------------------------------------------------- 1 | """frida-gadget version information.""" 2 | 3 | __title__ = "frida-gadget" 4 | __version__ = "1.6.2" 5 | __description__ = "Automated Frida Gadget injection tool" 6 | __url__ = "https://github.com/ksg97031/frida-gadget" 7 | __author__ = "ksg97031" 8 | __author_email__ = "ksg97031@gmail.com" 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | images 4 | CONTRIBUTORS.svg 5 | *.apk 6 | .vscode 7 | frida_gadget.egg-info 8 | tests 9 | pypi.sh 10 | .env 11 | dist 12 | scripts/files/* 13 | **/__pycache__/ 14 | test_apks 15 | build/ 16 | dist/ 17 | *.pyc 18 | venv/ 19 | .DS_Store 20 | *.egg-info 21 | clean.sh 22 | test/demo-apk/* 23 | !test/demo-apk/*.apk 24 | .coverage 25 | scripts/files/ 26 | .env 27 | /.idea 28 | test_apks 29 | .vscode 30 | docker.sh 31 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """test_cli.py""" 2 | from pathlib import Path 3 | from scripts.cli import run 4 | from click.testing import CliRunner 5 | 6 | p = Path(__file__) 7 | ROOT_DIR = p.parent.resolve() 8 | 9 | def test_run(): 10 | """test frida-gadget run 11 | """ 12 | runner = CliRunner() 13 | demo_apk_path = str(ROOT_DIR.joinpath('demo-apk/handtrackinggpu.apk').resolve()) 14 | result = runner.invoke(run, [demo_apk_path]) 15 | assert result.exit_code == 0 16 | -------------------------------------------------------------------------------- /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | name: Contributors 2 | on: 3 | schedule: 4 | - cron: '0 1 * * 0' # At 01:00 on Sunday. 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | inputs: 10 | logLevel: 11 | description: 'manual run' 12 | required: false 13 | default: '' 14 | jobs: 15 | contributors: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: bubkoo/contributors-list@v1 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | round: true 22 | permissions: 23 | contents: read 24 | pull-requests: write 25 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """init file for scripts folder.""" 2 | import sys 3 | import subprocess 4 | from .logger import logger 5 | 6 | 7 | def import_or_install(package): 8 | """Function to install missing packages. 9 | 10 | Args: 11 | package (string): Name of the package to install. 12 | """ 13 | try: 14 | __import__(package) 15 | except ImportError: 16 | subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'frida', '--user']) 17 | logger.info('Try Again') 18 | sys.exit(0) 19 | 20 | 21 | import_or_install('frida') # Install missing packages 22 | INSTALLED_FRIDA_VERSION: str = __import__('frida').__version__ # Get installed frida version 23 | -------------------------------------------------------------------------------- /scripts/logger.py: -------------------------------------------------------------------------------- 1 | """logger.py""" 2 | import logging 3 | from colorlog import ColoredFormatter 4 | 5 | ch = logging.StreamHandler() 6 | ch.setLevel(logging.DEBUG) 7 | formatter = ColoredFormatter( 8 | "%(log_color)s[%(levelname)s] %(message)s", 9 | datefmt=None, 10 | reset=True, 11 | log_colors={ 12 | 'DEBUG': 'cyan', 13 | 'INFO': 'white,bold', 14 | 'INFOV': 'cyan,bold', 15 | 'WARNING': 'yellow', 16 | 'ERROR': 'red,bold', 17 | 'CRITICAL': 'red,bg_white', 18 | }, 19 | secondary_log_colors={}, 20 | style='%' 21 | ) 22 | ch.setFormatter(formatter) 23 | logger = logging.getLogger('frida-gadget') 24 | logger.setLevel(logging.DEBUG) 25 | logger.addHandler(ch) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ksg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17-jdk-jammy 2 | LABEL MAINTAINER ksg97031 (ksg97031@gmail.com) 3 | 4 | # Install dependencies 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | curl \ 8 | python3 \ 9 | python3-pip \ 10 | python-is-python3 \ 11 | && apt-get clean && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | # Install Frida 15 | RUN pip3 install --no-cache-dir --upgrade pip && \ 16 | pip3 install --no-cache-dir frida 17 | 18 | # Install apktool 19 | ENV APKTOOL_VERSION=2.11.1 20 | WORKDIR /usr/local/bin 21 | RUN curl -sLO https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool && \ 22 | chmod +x apktool 23 | RUN curl -sL -o apktool.jar https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar && \ 24 | chmod +x apktool.jar 25 | 26 | # Create a non-root user 27 | RUN useradd -m -s /bin/bash frida-user 28 | 29 | # Install dependencies 30 | WORKDIR /workspace 31 | COPY scripts /workspace/scripts 32 | COPY requirements.txt /workspace/requirements.txt 33 | RUN pip3 install --no-cache-dir -r requirements.txt 34 | 35 | # Set ownership of workspace directory 36 | RUN chown -R frida-user:frida-user /workspace 37 | 38 | # Switch to non-root user 39 | USER frida-user 40 | 41 | ENTRYPOINT ["python3", "-m", "scripts.cli"] 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import setuptools 4 | 5 | if sys.version_info < (3, 6): 6 | print("Unfortunately, your python version is not supported!\n" + 7 | "Please upgrade at least to Python 3.6!") 8 | sys.exit(1) 9 | 10 | about = {} 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | with open(os.path.join(here, "scripts", "__version__.py"), "r", encoding="utf-8") as f: 13 | exec(f.read(), about) 14 | 15 | with open("README.rst", "r", encoding="utf-8") as fh: 16 | long_description = fh.read() 17 | 18 | requires = [ 19 | 'click', 20 | 'androguard >= 4.0.0', 21 | 'apk-signer', 22 | 'pytest', 23 | 'colorlog', 24 | 'coverage', 25 | 'requests', 26 | ] 27 | 28 | setuptools.setup( 29 | name=about["__title__"], 30 | python_requires='>=3.6', 31 | version=about["__version__"], 32 | author=about["__author__"], 33 | author_email=about["__author_email__"], 34 | description=about["__description__"], 35 | install_requires=requires, 36 | long_description=long_description, 37 | long_description_content_type="text/x-rst", 38 | url=about["__url__"], 39 | packages=setuptools.find_packages(), 40 | package_data={ 41 | 'scripts': [ 42 | "files/README.md", 43 | ] 44 | }, 45 | entry_points={ 46 | 'console_scripts': ['frida-gadget = scripts.cli:run'], 47 | }, 48 | classifiers=[ 49 | "Programming Language :: Python :: 3.6", 50 | "Programming Language :: Python :: 3.7", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Programming Language :: Python :: 3.10", 54 | "Programming Language :: Python :: 3.11", 55 | "Programming Language :: Python :: 3.12", 56 | "Programming Language :: Python :: 3.13", 57 | "License :: OSI Approved :: MIT License", 58 | "Operating System :: OS Independent", 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /scripts/apk_utils.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | 3 | from .logger import logger 4 | 5 | def get_main_activity(apk:APK): 6 | x = set() 7 | y = set() 8 | 9 | for i in apk.xml: 10 | if apk.xml[i] is None: 11 | continue 12 | activities_and_aliases = apk.xml[i].findall(".//activity") + \ 13 | apk.xml[i].findall(".//activity-alias") 14 | 15 | for item in activities_and_aliases: 16 | # Some applications have more than one MAIN activity. 17 | # For example: paid and free content 18 | activityEnabled = item.get(apk._ns("enabled")) 19 | if activityEnabled == "false": 20 | continue 21 | 22 | for sitem in item.findall(".//action"): 23 | val = sitem.get(apk._ns("name")) 24 | if val == "android.intent.action.MAIN": 25 | activity = item.get(apk._ns("name")) 26 | target_activty = item.get(apk._ns("targetActivity")) 27 | if target_activty is not None: 28 | logger.debug('Target activity found: %s -> %s', activity, target_activty) 29 | activity = target_activty 30 | if activity is not None: 31 | x.add(activity) 32 | else: 33 | logger.warning('Main activity without name') 34 | 35 | for sitem in item.findall(".//category"): 36 | val = sitem.get(apk._ns("name")) 37 | if val == "android.intent.category.LAUNCHER": 38 | activity = item.get(apk._ns("name")) 39 | target_activty = item.get(apk._ns("targetActivity")) 40 | if target_activty is not None: 41 | activity = target_activty 42 | if activity is not None: 43 | y.add(activity) 44 | else: 45 | logger.warning('Launcher activity without name') 46 | 47 | activities = x.intersection(y) 48 | if len(activities) == 0: 49 | return None 50 | elif len(activities) > 1: 51 | logger.error("Multiple main activities found: %s", activities) 52 | logger.error('Please specify one using the --main-activity option.') 53 | return -1 54 | 55 | main_activity = activities.pop() 56 | return main_activity -------------------------------------------------------------------------------- /scripts/frida_github.py: -------------------------------------------------------------------------------- 1 | """Github module for download frida gadget library""" 2 | # Base code is sourced from the GitHub repository of Objection. 3 | # Source: https://github.com/sensepost/objection/blob/master/objection/utils/patchers/github.py 4 | import lzma 5 | from pathlib import Path 6 | import requests 7 | 8 | class FridaGithub: 9 | """ Interact with Github """ 10 | 11 | GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest' 12 | GITHUB_TAGGED_RELEASE = 'https://api.github.com/repos/frida/frida/releases/tags/{tag}' 13 | 14 | # the 'context' of this Github instance 15 | gadget_version = None 16 | 17 | def __init__(self, gadget_version: str = None): 18 | """ 19 | Init a new instance of Github 20 | """ 21 | 22 | if gadget_version: 23 | self.gadget_version = gadget_version 24 | 25 | self.request_cache = {} 26 | 27 | def _call(self, endpoint: str) -> dict: 28 | """ 29 | Make a call to Github and cache the response. 30 | 31 | :param endpoint: 32 | :return: 33 | """ 34 | 35 | # return a cached response if possible 36 | if endpoint in self.request_cache: 37 | return self.request_cache[endpoint] 38 | 39 | # get a new response 40 | results = requests.get(endpoint, timeout=30).json() 41 | 42 | # cache it 43 | self.request_cache[endpoint] = results 44 | 45 | # and return it 46 | return results 47 | 48 | def get_latest_version(self) -> str: 49 | """ 50 | Call Github and get the tag_name of the latest 51 | release. 52 | 53 | :return: 54 | """ 55 | 56 | self.gadget_version = self._call(self.GITHUB_LATEST_RELEASE)['tag_name'] 57 | 58 | return self.gadget_version 59 | 60 | def get_assets(self) -> dict: 61 | """ 62 | Gets the assets for the currently selected gadget_version. 63 | 64 | :return: 65 | """ 66 | 67 | assets = self._call(self.GITHUB_TAGGED_RELEASE.format(tag=self.gadget_version)) 68 | 69 | if 'assets' not in assets: 70 | raise FileNotFoundError( 71 | f'Unable to determine assets for gadget version \'{self.gadget_version}\'. ' 72 | 'Are you sure this version is available on Github?') 73 | 74 | return assets['assets'] 75 | 76 | def download_asset(self, url: str, output_file: str) -> None: 77 | """ 78 | Download an asset from Github. 79 | 80 | :param url: 81 | :param output_file: 82 | :return: 83 | """ 84 | 85 | assert output_file.endswith('.xz') 86 | filepath = Path(output_file) 87 | if filepath.exists() and filepath.stat().st_size > 0: 88 | return 89 | 90 | response = requests.get(url, timeout=600, stream=True) 91 | with open(output_file, 'wb') as asset: 92 | for chunk in response.iter_content(chunk_size=1024): 93 | if chunk: 94 | asset.write(chunk) 95 | 96 | def download_gadget_so(self, url, gadget_fullpath: str) -> str: 97 | """ 98 | Download the gadget library from Github. 99 | 100 | :param gadget_path: 101 | :return: 102 | """ 103 | 104 | assert gadget_fullpath.endswith('.so') 105 | gadget_path = Path(gadget_fullpath) 106 | download_directory = gadget_path.parent 107 | if gadget_path.exists(): 108 | return gadget_fullpath 109 | 110 | if not download_directory.exists(): 111 | download_directory.mkdir(parents=True, exist_ok=True) 112 | 113 | xz_gadget_fullpath = gadget_fullpath + ".xz" 114 | self.download_asset(url, xz_gadget_fullpath) 115 | with lzma.open(xz_gadget_fullpath, "rb") as lzma_file: 116 | decompressed_data = lzma_file.read() 117 | 118 | with open(gadget_fullpath, "wb") as gadget_file: 119 | gadget_file.write(decompressed_data) 120 | 121 | return gadget_fullpath 122 | -------------------------------------------------------------------------------- /scripts/uber_apk_signer_github.py: -------------------------------------------------------------------------------- 1 | """Github module for download frida gadget library""" 2 | # Base code is sourced from the GitHub repository of Objection. 3 | # Source: https://github.com/sensepost/objection/blob/master/objection/utils/patchers/github.py 4 | from pathlib import Path 5 | import hashlib 6 | import os 7 | import requests 8 | 9 | class UberApkSignerGithub: 10 | """ Interact with Github """ 11 | 12 | GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/patrickfav/uber-apk-signer/releases/latest' 13 | 14 | # the 'context' of this Github instance 15 | signer_version = None 16 | 17 | def __init__(self, signer_version: str = None): 18 | """ 19 | Init a new instance of Github 20 | """ 21 | 22 | if signer_version: 23 | self.signer_version = signer_version 24 | 25 | self.request_cache = {} 26 | 27 | def _call(self, endpoint: str) -> dict: 28 | """ 29 | Make a call to Github and cache the response. 30 | 31 | :param endpoint: 32 | :return: 33 | """ 34 | 35 | # return a cached response if possible 36 | if endpoint in self.request_cache: 37 | return self.request_cache[endpoint] 38 | 39 | # get a new response 40 | results = requests.get(endpoint, timeout=30).json() 41 | 42 | # cache it 43 | self.request_cache[endpoint] = results 44 | 45 | # and return it 46 | return results 47 | 48 | def get_assets(self) -> dict: 49 | """ 50 | Gets the assets for the currently selected signer_version. 51 | 52 | :return: 53 | """ 54 | 55 | assets = self._call(self.GITHUB_LATEST_RELEASE) 56 | 57 | self.signer_version = assets['tag_name'][1:] 58 | 59 | if 'assets' not in assets: 60 | raise FileNotFoundError( 61 | f'Unable to determine assets for signer version \'{self.signer_version}\'. ' 62 | 'Are you sure this version is available on Github?') 63 | 64 | return assets['assets'] 65 | 66 | def download_asset(self, url: str, output_file: str) -> None: 67 | """ 68 | Download an asset from Github. 69 | 70 | :param url: 71 | :param output_file: 72 | :return: 73 | """ 74 | filepath = Path(output_file) 75 | if filepath.exists() and filepath.stat().st_size > 0: 76 | return 77 | 78 | response = requests.get(url, timeout=600, stream=True) 79 | with open(output_file, 'wb') as asset: 80 | for chunk in response.iter_content(chunk_size=1024): 81 | if chunk: 82 | asset.write(chunk) 83 | 84 | def download_signer_jar(self, assets: list, signer_fullpath: str) -> str: 85 | """ 86 | Download the signer jar library from Github. 87 | 88 | :param assets: 89 | :param signer_path: 90 | :return: 91 | """ 92 | assert len(assets) == 2, 'Unable to determine the correct asset to download.' 93 | assert signer_fullpath.endswith('.jar'), 'Signer path must end with .jar' 94 | 95 | checksum_download_url = assets[0]['browser_download_url'] 96 | uber_apk_signer_download_url = assets[1]['browser_download_url'] 97 | if assets[1]['name'] == 'checksum-sha256.txt': 98 | checksum_download_url, uber_apk_signer_download_url = \ 99 | uber_apk_signer_download_url, checksum_download_url 100 | assert uber_apk_signer_download_url.endswith('.jar'), 'Download URL must end with .jar' 101 | 102 | signer_path = Path(signer_fullpath) 103 | download_directory = signer_path.parent 104 | if signer_path.exists(): 105 | return signer_fullpath 106 | 107 | if not download_directory.exists(): 108 | download_directory.mkdir(parents=True, exist_ok=True) 109 | 110 | check_sum_fullpath = signer_fullpath[:-4] + '.sha256' 111 | self.download_asset(checksum_download_url, check_sum_fullpath) 112 | 113 | with open(check_sum_fullpath, 'rb') as checksum_file: 114 | checksum = checksum_file.read(64).decode('utf-8') 115 | 116 | self.download_asset(uber_apk_signer_download_url, signer_fullpath) 117 | with open(signer_fullpath, 'rb') as signer_file: 118 | signer_data = signer_file.read() 119 | signer_hash = hashlib.sha256(signer_data).hexdigest() 120 | 121 | if checksum != signer_hash: 122 | os.remove(signer_fullpath) 123 | os.remove(check_sum_fullpath) 124 | raise ValueError('The downloaded uber-apk-signer-*.jar file does not match the checksum.') 125 | 126 | return signer_fullpath 127 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | frida-gadget 2 | ============ 3 | 4 | |Codacy-Grade| |Docker| |LICENCE| 5 | 6 | 7 | | ``frida-gadget`` is a tool for patching Android applications to integrate the `Frida Gadget `_. 8 | | This tool automates the process of downloading the Frida gadget library and injecting the ``loadLibrary`` code into the main activity. 9 | 10 | 11 | Installation 12 | ------------ 13 | 14 | |Py-Versions| |PyPI-Downloads| 15 | 16 | .. code:: sh 17 | 18 | pip install frida-gadget --upgrade 19 | 20 | Prerequirement 21 | ---------------- 22 | 23 | | You should install ``apktool`` and add it to your ``PATH`` environment variable. 24 | | 25 | 26 | .. code:: sh 27 | 28 | # Install Apktool on macOS 29 | brew install apktool 30 | 31 | # Add Apktool to your PATH environment variable 32 | export PATH=$PATH:$HOME/.brew/bin 33 | 34 | | For other operating systems, such as ``Windows``, you can refer to the `Install Guide `_. 35 | 36 | 37 | Usage 38 | ------------ 39 | 40 | .. code:: sh 41 | 42 | $ frida-gadget --help 43 | Usage: cli.py [OPTIONS] APK_PATH 44 | 45 | Patch an APK with the Frida gadget library 46 | 47 | Options: 48 | --arch TEXT Specify the target architecture of the device. (options: arm64, x86_64, arm, x86) 49 | --config TEXT Specify the Frida configuration file. 50 | --js TEXT Specify the Frida gadget JavaScript file. 51 | --js-delay INTEGER Specify seconds to wait before executing the JavaScript file. 52 | --force-manifest Force modify AndroidManifest.xml even if it already has required permissions. 53 | --custom-gadget-name TEXT Specify a custom name for the Frida gadget. 54 | --no-res Skip decoding resources. 55 | --main-activity TEXT Specify the main activity if known. 56 | --sign Automatically sign the APK using uber-apk-signer. 57 | --skip-decompile Skip the decompilation step. 58 | --skip-recompile Skip the recompilation step. 59 | --use-aapt2 Use aapt2 instead of aapt for resource processing. 60 | --decompile-opts TEXT Specify additional options for apktool decompile. 61 | --recompile-opts TEXT Specify additional options for apktool recompile. 62 | --apktool-path TEXT Specify the path or command to run apktool. 63 | --frida-version TEXT Specify the Frida version to use. 64 | --ks TEXT The keystore file. If not provided, will use debug keystore. 65 | --ks-alias TEXT The alias of the used key in the keystore. 66 | --ks-key-pass TEXT The password for the key. 67 | --ks-pass TEXT The password for the keystore. 68 | --version Show the version and exit. 69 | --help Show this message and exit. 70 | 71 | How do I begin? 72 | ~~~~~~~~~~~~~~~~~~~~~~ 73 | | Simply provide the APK file with the target architecture. 74 | | 75 | 76 | .. code:: sh 77 | 78 | $ frida-gadget target.apk --sign 79 | [INFO] Auto-detected frida version: 16.1.3 80 | [INFO] APK: '[REDACTED]/demo-apk/target.apk' 81 | [INFO] Auto-detected architecture via ADB: arm64-v8a # Alternatively, specify the architecture with --arch arm64 82 | [INFO] Gadget Architecture(--arch): arm64(default) 83 | [DEBUG] Decompiling the target APK using apktool 84 | [DEBUG] Downloading the frida gadget library for arm64 85 | [DEBUG] Checking internet permission and extractNativeLibs settings 86 | [DEBUG] Adding 'android.permission.INTERNET' permission to AndroidManifest.xml 87 | [DEBUG] Searching for the main activity in the smali files 88 | [DEBUG] Found the main activity at '[REDACTED]/frida-gadget/tests/demo-apk/target/smali/com/google/mediap/apps/target/MainActivity.smali' 89 | [DEBUG] Locating the onCreate method and injecting the loadLibrary code 90 | [DEBUG] Recompiling the new APK using apktool 91 | ... 92 | [INFO] APK signing finished: ./target/dist/target-aligned-debugSigned.apk (72.78 MiB) 93 | 94 | With Docker 95 | ~~~~~~~~~~~~~~~~~~ 96 | | You can also use this tool with Docker. Here's how to use it: 97 | | 98 | | 1. First, pull the Docker image: 99 | | 100 | 101 | .. code:: sh 102 | 103 | docker pull ksg97031/frida-gadget 104 | 105 | | 2. Mount your local directory containing the APK file to the container: 106 | | 107 | 108 | .. code:: sh 109 | 110 | docker run -v $(pwd):/workspace/mount ksg97031/frida-gadget /workspace/mount/your-app.apk --arch arm64 111 | 112 | | Note: Replace ``your-app.apk`` with your actual APK filename. The patched APK will be created in the same directory as your original APK. 113 | | 114 | | For example, if your APK is named ``example.apk``: 115 | | 116 | 117 | .. code:: sh 118 | 119 | docker run -v $(pwd):/workspace/mount ksg97031/frida-gadget /workspace/mount/example.apk --arch arm64 120 | # The patched APK will be located at ./example/dist/example.apk 121 | 122 | Compatibility 123 | ---------------- 124 | Device Architecture 125 | ~~~~~~~~~~~~~~~~~~~~~~~ 126 | | The tool automatically detects the device architecture when an ADB device is connected. You can also manually specify the architecture using the ``--arch`` option. 127 | | 128 | | To determine your device's architecture, connect your device and run the following command: 129 | | 130 | 131 | .. code:: sh 132 | 133 | adb shell getprop ro.product.cpu.abi 134 | 135 | | This command will output the architecture of your device, such as ``arm64-v8a``, ``armeabi-v7a``, ``x86``, ``x86_64`` or ``multi-arch``. 136 | 137 | | Example of automatic detection: 138 | | 139 | 140 | .. code:: sh 141 | 142 | $ frida-gadget target.apk --sign 143 | [INFO] Auto-detected architecture via ADB: arm64-v8a 144 | 145 | | Example of manual specification: 146 | | 147 | 148 | .. code:: sh 149 | 150 | $ frida-gadget target.apk --arch arm64 --sign 151 | [INFO] Gadget Architecture(--arch): arm64 152 | 153 | Android Version Support 154 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 155 | | The following table shows the minimum Frida version required for different Android versions: 156 | | (Note: This information may not be completely accurate) 157 | 158 | .. list-table:: 159 | :header-rows: 1 160 | 161 | * - Android Version 162 | - Minimum Frida Version 163 | - Notes 164 | * - Android 5.x ~ 7.x (Lollipop~Nougat) 165 | - Frida 14.2+ 166 | - Support for older Android versions was improved in Frida 12.6. Frida 14.2 includes fixes for libc detection errors and restored Houdini (translator) support. Latest Frida (16.x) continues to support Android 5~7. 167 | * - Android 8.0 ~ 8.1 (Oreo) 168 | - Frida 12.6.6+ 169 | - Java API issues like Java.choose were resolved in Frida 12.6.3+. Java integration issues on 32-bit ARM devices were fixed in Frida 12.6.6. Frida 14.x and newer versions work stably on Oreo. 170 | * - Android 9.0 (Pie) 171 | - Frida 12.7+ 172 | - Frida was extensively tested on Pixel 3 (Android 9). Frida 12.x ~ 15.x versions work stably on AOSP-based Android 9. Latest Frida 16.x also supports Android 9. (For emulators, Google-provided Android 9 images for arm/arm64 are recommended.) 173 | * - Android 10 (Q) 174 | - Frida 14.2+ 175 | - While there were no major changes specific to Android 10, Frida 14.2+ is recommended for overall stability. Frida 14.2 includes various compatibility improvements for both pre and post Android 10 versions. Latest Frida 15.x and 16.x versions work without issues on Android 10. 176 | * - Android 11 (R) 177 | - Frida 14.2+ 178 | - Frida 14.2 includes modifications to address ART changes and ARM->x86 translation in Android 11. Frida 14.2 or higher is recommended for Android 11. Frida 15.x~16.x fully support Android 11. (May have separate issues on custom ROMs like Samsung.) 179 | * - Android 12 (S) 180 | - Frida 15.0+ 181 | - Official support for Android 12 was first added in Frida 15.0. Initial 15.0 version had minor compatibility issues, but Frida 15.1.23 includes several stability improvements for Android 12. Frida 15.1.23 or higher (preferably 15.2 or latest 16.x) is recommended for Android 12 devices. 182 | * - Android 13 (T) 183 | - Frida 15.1.23+ 184 | - Preliminary support for Android 13 was introduced in Frida 15.1.23, and support matured in Frida 16.x versions. Minimum Frida 15.1.23 is required for Android 13 devices, but using the latest Frida 16 version is recommended (includes fixes for Android 13's internal behavior changes). 185 | * - Android 14 (UpsideDownCake) 186 | - Frida 16.2.0+ 187 | - Due to ART structure changes in Android 14, initial Frida 16.0~16.1 versions had issues with Java hooking, but Frida 16.2.0 improved hooking support for Android 14. Frida 16.2 or higher is recommended for Android 14 (Frida 16.2 added support for Android 14's new ART entrypoints). 188 | 189 | How to Identify the Injection? 190 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 191 | | You can observe the main activity to see the injected ``loadLibrary`` code. 192 | | Additionally, the Frida gadget library will be present in your APK. 193 | 194 | .. code:: sh 195 | 196 | $ unzip -l [REDACTED]/demo-apk/target/dist/target.apk | grep libfrida-gadget 197 | 21133848 09-15-2021 02:28 lib/arm64-v8a/libfrida-gadget-16.1.3-android-arm64.so 198 | 199 | Tips 200 | ------------ 201 | 202 | Specifying a Different Main Activity 203 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 204 | | If the main activity is not automatically detected, you can specify it manually using the ``--main-activity`` option: 205 | | 206 | 207 | .. code:: sh 208 | 209 | $ frida-gadget target.apk --main-activity com.example.MainActivity --no-res --sign 210 | 211 | Creating Self-Contained SSL Bypass App with --js 212 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 213 | | 1. Download the `@akabe1/frida-multiple-unpinning `_ script. 214 | | 2. Inject the script into the target application using the ``--js`` flag. 215 | 216 | .. code:: sh 217 | 218 | frida-gadget target.apk --js frida-multiple-unpinning.js --sign --no-res 219 | 220 | | 3. Run the injected application on your device or emulator. 221 | | 4. Observe the network traffic using a proxy tool such as `Burp Suite `_ or `Caido `_. 222 | | 223 | | Note: If the app crashes, try adding ``--js-delay 2`` to delay script execution: 224 | 225 | .. code:: sh 226 | 227 | frida-gadget target.apk --js frida-multiple-unpinning.js --js-delay 2 --sign --no-res 228 | 229 | | This gives the app time to initialize before applying hooks. 230 | | 231 | | You can also specify a custom Frida version using ``--frida-version``: 232 | 233 | .. code:: sh 234 | 235 | frida-gadget target.apk --js frida-multiple-unpinning.js --frida-version 16.1.3 --sign --no-res 236 | 237 | | This is useful when you need to use a specific Frida version for compatibility reasons. 238 | 239 | Using a Custom Apktool 240 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241 | | You can specify a custom apktool path or command using the ``--apktool-path`` option. 242 | | For example, you can use a script or a specific jar file: 243 | | 244 | 245 | .. code:: sh 246 | 247 | $ frida-gadget target.apk --apktool-path ./tools/apktool.bat --sign # Windows 248 | $ frida-gadget target.apk --apktool-path "java -Xmx16g -jar ~/Download/apktool.jar" --sign # Java with 16GB memory 249 | 250 | Custom Apktool Options 251 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 252 | | You can also specify custom options for apktool decompile and recompile using the ``--decompile-opts`` and ``--recompile-opts`` options. 253 | | For example, you can pass additional flags to apktool: 254 | | 255 | 256 | .. code:: sh 257 | 258 | $ frida-gadget target.apk --decompile-opts "--only-main-classes --no-res" --recompile-opts "--force-all" --sign 259 | 260 | Contributing 261 | ----------------- 262 | .. image:: CONTRIBUTORS.svg 263 | :target: ./CONTRIBUTORS.svg 264 | 265 | 266 | .. |Coverage-Status| image:: https://img.shields.io/coveralls/github/ksg97031/frida-gadget/master?logo=coveralls 267 | :target: https://coveralls.io/github/ksg97031/frida-gadget 268 | .. |Branch-Coverage-Status| image:: https://codecov.io/gh/ksg97031/frida-gadget/branch/master/graph/badge.svg 269 | :target: https://codecov.io/gh/ksg97031/frida-gadget 270 | .. |Codacy-Grade| image:: https://app.codacy.com/project/badge/Grade/a1e2ef93fd3842e4b9e92971c135ed3f 271 | :target: https://app.codacy.com/gh/ksg97031/frida-gadget/dashboard 272 | .. |CII Best Practices| image:: https://bestpractices.coreinfrastructure.org/projects/3264/badge 273 | :target: https://bestpractices.coreinfrastructure.org/projects/3264 274 | .. |GitHub-Status| image:: https://img.shields.io/github/tag/ksg97031/frida-gadget.svg?maxAge=86400&logo=github&logoColor=white 275 | :target: https://github.com/ksg97031/frida-gadget/releases 276 | .. |GitHub-Forks| image:: https://img.shields.io/github/forks/ksg97031/frida-gadget.svg?logo=github&logoColor=white 277 | :target: https://github.com/ksg97031/frida-gadget/network 278 | .. |GitHub-Stars| image:: https://img.shields.io/github/stars/ksg97031/frida-gadget.svg?logo=github&logoColor=white 279 | :target: https://github.com/ksg97031/frida-gadget/stargazers 280 | .. |GitHub-Commits| image:: https://img.shields.io/github/commit-activity/y/ksg97031/frida-gadget.svg?logo=git&logoColor=white 281 | :target: https://github.com/ksg97031/frida-gadget/graphs/commit-activity 282 | .. |GitHub-Issues| image:: https://img.shields.io/github/issues-closed/ksg97031/frida-gadget.svg?logo=github&logoColor=white 283 | :target: https://github.com/ksg97031/frida-gadget/issues?q= 284 | .. |GitHub-PRs| image:: https://img.shields.io/github/issues-pr-closed/ksg97031/frida-gadget.svg?logo=github&logoColor=white 285 | :target: https://github.com/ksg97031/frida-gadget/pulls 286 | .. |GitHub-Contributions| image:: https://img.shields.io/github/contributors/ksg97031/frida-gadget.svg?logo=github&logoColor=white 287 | :target: https://github.com/ksg97031/frida-gadget/graphs/contributors 288 | .. |GitHub-Updated| image:: https://img.shields.io/github/last-commit/ksg97031/frida-gadget/master.svg?logo=github&logoColor=white&label=pushed 289 | :target: https://github.com/ksg97031/frida-gadget/pulse 290 | .. |Gift-Casper| image:: https://img.shields.io/badge/dynamic/json.svg?color=ff69b4&label=gifts%20received&prefix=%C2%A3&query=%24..sum&url=https%3A%2F%2Fcaspersci.uk.to%2Fgifts.json 291 | :target: https://cdcl.ml/sponsor 292 | .. |PyPI-Downloads| image:: https://static.pepy.tech/badge/frida-gadget 293 | :target: https://pepy.tech/project/frida-gadget 294 | .. |Py-Versions| image:: https://img.shields.io/pypi/pyversions/frida-gadget 295 | :target: https://pypi.org/project/frida-gadget 296 | .. |Conda-Forge-Status| image:: https://img.shields.io/conda/v/conda-forge/frida-gadget.svg?label=conda-forge&logo=conda-forge 297 | :target: https://anaconda.org/conda-forge/frida-gadget 298 | .. |Docker| image:: https://img.shields.io/badge/docker-pull-blue.svg?logo=docker&logoColor=white 299 | :target: https://github.com/ksg97031/frida-gadget/pkgs/container/frida-gadget 300 | .. |Libraries-Dependents| image:: https://img.shields.io/librariesio/dependent-repos/pypi/frida-gadget.svg?logo=koding&logoColor=white 301 | :target: https://github.com/ksg97031/frida-gadget/network/dependents 302 | .. |OpenHub-Status| image:: https://www.openhub.net/p/frida-gadget/widgets/project_thin_badge?format=gif 303 | :target: https://www.openhub.net/p/frida-gadget?ref=Thin+badge 304 | .. |awesome-python| image:: https://awesome.re/mentioned-badge.svg 305 | :target: https://github.com/vinta/awesome-python 306 | .. |LICENCE| image:: https://img.shields.io/pypi/l/frida-gadget.svg 307 | :target: https://raw.githubusercontent.com/ksg97031/frida-gadget/master/LICENCE 308 | .. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.595120-blue.svg 309 | :target: https://doi.org/10.5281/zenodo.595120 310 | .. |binder-demo| image:: https://mybinder.org/badge_logo.svg 311 | :target: https://mybinder.org/v2/gh/ksg97031/frida-gadget/master?filepath=DEMO.ipynb 312 | -------------------------------------------------------------------------------- /scripts/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Frida Gadget Injector for Android APK 3 | 4 | This script allows you to inject the Frida gadget library into an Android APK. 5 | It provides various functionalities including: 6 | - Decompiling the APK using apktool 7 | - Downloading the appropriate Frida gadget library based on the device architecture 8 | - Injecting the Frida gadget library into the APK 9 | - Modifying the AndroidManifest.xml to add necessary permissions 10 | - Recompiling the APK 11 | - Optionally signing the APK using uber-apk-signer 12 | """ 13 | 14 | import os 15 | import re 16 | import sys 17 | import shutil 18 | import subprocess 19 | import json 20 | import tempfile 21 | import zipfile 22 | from shutil import which 23 | from pathlib import Path 24 | import click 25 | from androguard.core.apk import APK 26 | from .apk_utils import get_main_activity 27 | from .logger import logger 28 | from .__version__ import __version__ 29 | from .frida_github import FridaGithub 30 | from .uber_apk_signer_github import UberApkSignerGithub 31 | from . import INSTALLED_FRIDA_VERSION 32 | 33 | 34 | p = Path(__file__) 35 | ROOT_DIR = p.parent.resolve() 36 | TEMP_DIR = ROOT_DIR.joinpath("temp") 37 | FILE_DIR = ROOT_DIR.joinpath("files") 38 | 39 | APKTOOL = which("apktool") 40 | 41 | 42 | def run_apktool(option: list, apk_path: str): 43 | """Run apktool with option 44 | 45 | Args: 46 | option (list|str): option of apktool 47 | apk_path (str): path of apk file 48 | 49 | """ 50 | 51 | pipe = subprocess.PIPE 52 | cmd = APKTOOL.split() + option + [apk_path] 53 | with subprocess.Popen( 54 | cmd, stdin=pipe, stdout=sys.stdout, stderr=sys.stderr 55 | ) as process: 56 | process.communicate(b"\n") 57 | if process.returncode != 0: 58 | recommend_options = ["--no-res", "--use-aapt2"] 59 | for opt in recommend_options: 60 | if opt in option: 61 | recommend_options.remove(opt) 62 | 63 | logger.error( 64 | "It looks like you're having trouble with apktool.\n" 65 | "Consider trying the '%s' options, or if you'd prefer more control,\n" 66 | "you can manually specify apktool settings using ['--decompile-opts', '--recompile-opts', '--apktool-path'].", 67 | recommend_options, 68 | ) 69 | 70 | raise subprocess.CalledProcessError( 71 | process.returncode, cmd, sys.stdout, sys.stderr 72 | ) 73 | return True 74 | 75 | 76 | def download_gadget(arch: str, frida_version: str = None): 77 | """Download the frida gadget library 78 | 79 | Args: 80 | arch (str): architecture of the device 81 | frida_version (str): specific frida version to use 82 | """ 83 | if frida_version: 84 | logger.info("Using specified frida version: %s", frida_version) 85 | version = frida_version 86 | else: 87 | logger.info("Auto-detected your frida version: %s", INSTALLED_FRIDA_VERSION) 88 | version = INSTALLED_FRIDA_VERSION 89 | 90 | frida_github = FridaGithub(version) 91 | assets = frida_github.get_assets() 92 | file = f"frida-gadget-{version}-android-{arch}.so.xz" 93 | for asset in assets: 94 | if asset["name"] == file: 95 | logger.debug( 96 | "Downloading the frida gadget library(%s) for %s", version, arch 97 | ) 98 | so_gadget_path = str(FILE_DIR.joinpath(file[:-3])) 99 | return frida_github.download_gadget_so( 100 | asset["browser_download_url"], so_gadget_path 101 | ) 102 | 103 | raise FileNotFoundError(f"'{file}' not found in the github releases") 104 | 105 | 106 | def download_signer(): 107 | """Download the Uber Apk Signer""" 108 | signer_github = UberApkSignerGithub() 109 | assets = signer_github.get_assets() 110 | file = f"uber-apk-signer-{signer_github.signer_version}.jar" 111 | signer_path = str(FILE_DIR.joinpath(file)) 112 | if os.path.exists(signer_path): 113 | return signer_path 114 | 115 | logger.debug("Downloading the %s file for signing", file) 116 | return signer_github.download_signer_jar(assets, signer_path) 117 | 118 | 119 | def insert_loadlibary(decompiled_path, main_activity, load_library_name): 120 | """Inject loadlibary code to main activity 121 | 122 | Args: 123 | decompiled_path (str): decomplied path of apk file 124 | main_activity (str): main activity of apk file 125 | load_library_name (str): name of load library 126 | """ 127 | logger.debug("Searching for the main activity in the smali files") 128 | target_smali = None 129 | target_smali_class_number = None 130 | 131 | target_relative_path = main_activity.replace(".", os.sep) 132 | for directory in decompiled_path.iterdir(): 133 | if directory.is_dir() and directory.name.startswith("smali"): 134 | target_smali = directory.joinpath(target_relative_path + ".smali") 135 | if target_smali.exists(): 136 | if directory.name.startswith("smali_classes"): 137 | target_smali_class_number = int(directory.name.split("smali_classes")[1]) 138 | break 139 | 140 | if not target_smali or not target_smali.exists(): 141 | raise FileNotFoundError(f"The target class file {target_smali} was not found.") 142 | 143 | logger.debug("Found the main activity at '%s'", str(target_smali)) 144 | text = target_smali.read_text() 145 | 146 | text = text.replace("invoke-virtual {v0, v1}, Ljava/lang/Runtime;->exit(I)V", "") 147 | text = text.split("\n") 148 | 149 | logger.debug("Locating the entrypoint method and injecting the loadLibrary code") 150 | status = False 151 | entrypoints = [" onCreate(", ""] 152 | for entrypoint in entrypoints: 153 | idx = 0 154 | while idx != len(text): 155 | line = text[idx].strip() 156 | if line.startswith(".method") and entrypoint in line: 157 | if ".locals" not in text[idx + 1]: 158 | idx += 1 159 | continue 160 | else: 161 | # Increase the number of locals 0 to 1 162 | if ".locals 0" in text[idx + 1]: 163 | text[idx + 1] = text[idx + 1].replace(".locals 0", ".locals 1") 164 | 165 | if load_library_name.startswith("lib"): 166 | load_library_name = load_library_name[3:] 167 | text.insert( 168 | idx + 2, 169 | " invoke-static {v0}, " 170 | "Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V", 171 | ) 172 | text.insert(idx + 2, f" const-string v0, " f'"{load_library_name}"') 173 | status = True 174 | break 175 | idx += 1 176 | 177 | if status: 178 | break 179 | 180 | if not status: 181 | logger.error("Cannot find the appropriate position in the main activity.") 182 | logger.error( 183 | "Please report the issue at %s with the following information:", 184 | "https://github.com/ksg97031/frida-gadget/issues", 185 | ) 186 | logger.error("APK Name: ") 187 | logger.error("APK Version: ") 188 | logger.error("APKTOOL Version: ") 189 | sys.exit(-1) 190 | 191 | # Replace the smali file with the new one 192 | target_smali.write_text("\n".join(text)) 193 | return target_smali_class_number 194 | 195 | def modify_manifest(decompiled_path): 196 | """Modify manifest permssions 197 | 198 | Args: 199 | decompiled_path (str): decomplied path of apk file 200 | """ 201 | # Add internet permission 202 | logger.debug("Checking internet permission and extractNativeLibs settings") 203 | android_manifest = decompiled_path.joinpath("AndroidManifest.xml") 204 | txt = android_manifest.read_text(encoding="utf-8") 205 | pos = txt.index("") 206 | permission = "android.permission.INTERNET" 207 | 208 | if permission not in txt: 209 | logger.debug( 210 | "Adding 'android.permission.INTERNET' permission to AndroidManifest.xml" 211 | ) 212 | permissions_txt = f"" 213 | txt = txt[:pos] + permissions_txt + txt[pos:] 214 | 215 | # Set extractNativeLibs to true 216 | if ':extractNativeLibs="false"' in txt: 217 | logger.debug('Editing the extractNativeLibs="true"') 218 | txt = txt.replace(':extractNativeLibs="false"', ':extractNativeLibs="true"') 219 | android_manifest.write_text(txt, encoding="utf-8") 220 | 221 | 222 | def detect_apk_architectures(decompiled_path): 223 | """Detect architectures from the APK's lib directory 224 | 225 | Args: 226 | decompiled_path (str): decompiled path of apk file 227 | 228 | Returns: 229 | list: List of detected architectures 230 | """ 231 | lib_dir = decompiled_path.joinpath("lib") 232 | if not lib_dir.exists(): 233 | logger.warning( 234 | "No lib directory found in the APK. Returning default architecture (arm64)." 235 | ) 236 | return ["arm64"] 237 | 238 | arch_mapping = { 239 | "arm64-v8a": "arm64", 240 | "armeabi-v7a": "arm", 241 | "x86": "x86", 242 | "x86_64": "x86_64", 243 | } 244 | 245 | detected_archs = [] 246 | for arch_dir in lib_dir.iterdir(): 247 | if arch_dir.is_dir() and arch_dir.name in arch_mapping: 248 | detected_archs.append(arch_mapping[arch_dir.name]) 249 | 250 | if not detected_archs: 251 | logger.warning( 252 | "No supported architectures found in the APK. Returning default architecture (arm64)." 253 | ) 254 | return ["arm64"] 255 | 256 | logger.info("Detected architectures in APK: %s", ", ".join(detected_archs)) 257 | return detected_archs 258 | 259 | 260 | def inject_gadget_into_apk( 261 | apk_path: str, 262 | arch: str, 263 | decompiled_path: str, 264 | no_res, 265 | force_manifest, 266 | main_activity: str = None, 267 | config: str = None, 268 | js: str = None, 269 | custom_gadget_name: str = None, 270 | frida_version: str = None, 271 | ): 272 | """Inject frida gadget into an APK 273 | 274 | Args: 275 | apk (APK): path of apk file 276 | arch (str): architecture of the device 277 | decompiled_path (str): decomplied path of apk file 278 | 279 | Raises: 280 | FileNotFoundError: file not found 281 | NotImplementedError: not implemented 282 | """ 283 | apk = APK(apk_path) 284 | 285 | # Handle 'multi-arch' option 286 | if arch == "multi-arch": 287 | archs = detect_apk_architectures(decompiled_path) 288 | logger.info( 289 | "Using multiple architectures detected from APK: %s", ", ".join(archs) 290 | ) 291 | else: 292 | archs = [arch] 293 | 294 | # Get main activity if not provided 295 | if not main_activity: 296 | main_activity = get_main_activity(apk) 297 | if main_activity == -1: # multiple main activities 298 | sys.exit(-1) 299 | 300 | if not main_activity: 301 | if len(apk.get_activities()) == 1: 302 | logger.warning( 303 | "The main activity was not found.\n" 304 | "Using the first activity from the manifest file." 305 | ) 306 | main_activity = apk.get_activities()[0] 307 | else: 308 | logger.error( 309 | "The main activity was not found.\n" 310 | "Please specify the main activity using the --main-activity option.\n" 311 | "Select the activity from %s", 312 | apk.get_activities(), 313 | ) 314 | sys.exit(-1) 315 | 316 | # Apply permission to android manifest 317 | if not no_res or force_manifest: 318 | modify_manifest(decompiled_path) 319 | 320 | for current_arch in archs: 321 | gadget_path = download_gadget(current_arch, frida_version) 322 | gadget_name = Path(gadget_path).name 323 | 324 | # Apply custom gadget name if provided 325 | if custom_gadget_name: 326 | custom_gadget_name_with_ext = custom_gadget_name + ".so" 327 | logger.info("Using custom gadget name: %s", custom_gadget_name_with_ext) 328 | gadget_name = custom_gadget_name_with_ext 329 | if arch == "multi-arch": 330 | # TODO: custom gadget name for multi-arch 331 | gadget_name = "libfrida-gadget.so" 332 | logger.info("Using multi-arch gadget name: %s", gadget_name) 333 | 334 | # Save the first gadget info for loadLibrary 335 | # Copy the frida gadget library to the lib directory 336 | lib = decompiled_path.joinpath("lib") 337 | if not lib.exists(): 338 | lib.mkdir() 339 | 340 | arch_dirnames = { 341 | "arm": "armeabi-v7a", 342 | "x86": "x86", 343 | "arm64": "arm64-v8a", 344 | "x86_64": "x86_64", 345 | } 346 | if current_arch not in arch_dirnames: 347 | raise NotImplementedError( 348 | f"The architecture '{current_arch}' is not supported." 349 | ) 350 | 351 | arch_dirname = arch_dirnames[current_arch] 352 | lib_arch_dir = lib.joinpath(arch_dirname) 353 | if not lib_arch_dir.exists(): 354 | lib_arch_dir.mkdir() 355 | 356 | lib_library_name = gadget_name 357 | if not lib_library_name.startswith("lib"): 358 | lib_library_name = "lib" + gadget_name 359 | shutil.copy(gadget_path, lib_arch_dir.joinpath(lib_library_name)) 360 | 361 | # Upload gadget config and js files for each architecture 362 | upload_files = {"config": config, "script": js} 363 | load_library_name = lib_library_name[:-3] 364 | 365 | if js and config: 366 | with open(config, "r") as f: 367 | contents = f.read() 368 | config_data = json.loads(contents) 369 | if "interaction" not in config_data: 370 | logger.error("The config file must contain an 'interaction' key.") 371 | sys.exit(-1) 372 | if "path" in config_data["interaction"]: 373 | logger.debug( 374 | "Updating the script path in '%s' from '%s' to 'lib%s.script.so'", 375 | config, 376 | config_data["interaction"]["path"], 377 | load_library_name, 378 | ) 379 | config_data["interaction"]["path"] = f"{load_library_name}.script.so" 380 | with open( 381 | lib_arch_dir.joinpath(f"{load_library_name}.config.so"), "w" 382 | ) as f: 383 | f.write(json.dumps(config_data, indent=4)) 384 | del upload_files["config"] 385 | elif js: 386 | config_name = f"{load_library_name}.config.so" 387 | with open(lib_arch_dir.joinpath(config_name), "w") as f: 388 | contents = ( 389 | """\ 390 | \r{ 391 | \r "interaction": { 392 | \r "type": "script", 393 | \r "path": \"""" 394 | + load_library_name 395 | + """.script.so" 396 | \r } 397 | \r} 398 | """ 399 | ) 400 | f.write(contents) 401 | logger.debug("Created the default config file: %s", config_name) 402 | logger.debug(contents) 403 | del upload_files["config"] 404 | elif config: 405 | logger.warning( 406 | "The '%s' config file was provided without the script file.", config 407 | ) 408 | logger.warning( 409 | "To upload the script file to the APK, please provide the --js option." 410 | ) 411 | with open(config, "r") as f: 412 | contents = f.read() 413 | config_data = json.loads(contents) 414 | if "interaction" not in config_data: 415 | logger.error("The config file must contain an 'interaction' key.") 416 | sys.exit(-1) 417 | if "path" not in config_data["interaction"]: 418 | logger.error("The config file must contain a 'path' key.") 419 | sys.exit(-1) 420 | logger.warning( 421 | "The script file must be located at '%s' on your device", 422 | config_data["interaction"]["path"], 423 | ) 424 | 425 | for file_type, file_path in upload_files.items(): 426 | if file_path: 427 | file_path = Path(file_path) 428 | if not file_path.exists(): 429 | logger.error("Frida %s file not found: %s", file_type, file_path) 430 | sys.exit(-1) 431 | else: 432 | target_name = f"{load_library_name}.{file_type}.so" 433 | if file_path.name == target_name: 434 | logger.debug( 435 | "Uploading Frida %s file: %s", file_type, file_path.name 436 | ) 437 | else: 438 | logger.debug( 439 | "Renaming and uploading Frida %s file: %s -> %s", 440 | file_type, 441 | file_path.name, 442 | target_name, 443 | ) 444 | shutil.copy(file_path, lib_arch_dir.joinpath(target_name)) 445 | 446 | return insert_loadlibary(decompiled_path, main_activity, load_library_name) 447 | 448 | 449 | def sign_apk(apk_path: str, ks: str = None, ks_alias: str = None, ks_key_pass: str = None, ks_pass: str = None): 450 | """Run uber apk signer with option 451 | 452 | Args: 453 | apk_path (str): path of apk file 454 | ks (str): keystore file path 455 | ks_alias (str): keystore alias 456 | ks_key_pass (str): key password 457 | ks_pass (str): keystore password 458 | """ 459 | signer_path = download_signer() # Download apk signer 460 | 461 | pipe = subprocess.PIPE 462 | cmd = ["java", "-jar", signer_path, "--apks", apk_path] 463 | 464 | if ks: 465 | cmd.append("--ks") 466 | cmd.append(ks) 467 | if ks_alias: 468 | cmd.append("--ksAlias") 469 | cmd.append(ks_alias) 470 | if ks_key_pass: 471 | cmd.append("--ksKeyPass") 472 | cmd.append(ks_key_pass) 473 | if ks_pass: 474 | cmd.append("--ksPass") 475 | cmd.append(ks_pass) 476 | 477 | with subprocess.Popen( 478 | cmd, stdin=pipe, stdout=subprocess.PIPE, stderr=sys.stderr 479 | ) as process: 480 | stdout, _ = process.communicate(b"\n") 481 | if process.returncode != 0: 482 | logger.error("The APK signing process failed.") 483 | raise subprocess.CalledProcessError( 484 | process.returncode, cmd, sys.stdout, sys.stderr 485 | ) 486 | 487 | output = stdout.decode() 488 | print(output) 489 | if "VERIFY" in output: 490 | verify_message = output.split("VERIFY")[1] 491 | if "file:" in verify_message: 492 | apk_path = verify_message.split("file:")[1].split("\n")[0].strip() 493 | logger.info("APK signing finished: %s", apk_path) 494 | 495 | 496 | def detect_adb_arch(): 497 | """Detect the architecture of the currently connected device via ADB. 498 | 499 | This function communicates with a connected Android device over ADB 500 | to determine its CPU architecture (e.g., arm64-v8a, armeabi-v7a, x86). 501 | 502 | Returns: 503 | str: The detected architecture of the connected device. 504 | Defaults to 'arm64' if detection fails. 505 | """ 506 | pipe = subprocess.PIPE 507 | cmd = ["adb", "shell", "getprop", "ro.product.cpu.abi"] 508 | default_arch = "arm64" 509 | 510 | try: 511 | with subprocess.Popen( 512 | cmd, stdin=pipe, stdout=subprocess.PIPE, stderr=subprocess.PIPE 513 | ) as process: 514 | stdout, stderr = process.communicate() 515 | if process.returncode != 0: 516 | logger.warning( 517 | "Failed to execute ADB command. Error: %s", stderr.decode().strip() 518 | ) 519 | logger.warning("Falling back to default architecture: %s", default_arch) 520 | return default_arch 521 | 522 | arch = stdout.decode().strip() 523 | if not arch: 524 | logger.warning( 525 | "Architecture detection failed: no output received. Falling back to default: %s", 526 | default_arch, 527 | ) 528 | return default_arch 529 | 530 | if arch == "arm64-v8a": 531 | arch = "arm64" 532 | elif arch == "armeabi-v7a": 533 | arch = "arm" 534 | 535 | logger.info("Auto-detected architecture via ADB: %s", arch) 536 | return arch 537 | 538 | except FileNotFoundError: 539 | logger.warning( 540 | "ADB is not installed or not found in the system PATH. Falling back to default: %s", 541 | default_arch, 542 | ) 543 | return default_arch 544 | except Exception as e: 545 | logger.warning( 546 | "An unexpected error occurred during architecture detection: %s. Falling back to default: %s", 547 | str(e), 548 | default_arch, 549 | ) 550 | return default_arch 551 | 552 | 553 | def print_version(ctx, _, value): 554 | """Print version and exit""" 555 | if not value or ctx.resilient_parsing: 556 | return 557 | print(f"frida-gadget version {__version__}") 558 | ctx.exit() 559 | 560 | 561 | def wrap_js_with_timeout(js_content: str, delay: int) -> str: 562 | """Wrap JavaScript content with setTimeout 563 | 564 | Args: 565 | js_content (str): Original JavaScript content 566 | delay (int): Seconds to wait before executing 567 | 568 | Returns: 569 | str: Wrapped JavaScript content 570 | """ 571 | return f"""setTimeout(function() {{ 572 | {js_content} 573 | }}, {delay * 1000});""" 574 | 575 | 576 | # pylint: disable=too-many-arguments 577 | @click.command() 578 | @click.option( 579 | "--arch", 580 | default=None, 581 | help="Specify the target architecture of the device. (options: arm64, x86_64, arm, x86, multi-arch)", 582 | ) 583 | @click.option("--config", help="Specify the Frida configuration file.") 584 | @click.option("--js", default=None, help="Specify the Frida gadget JavaScript file.") 585 | @click.option( 586 | "--js-delay", 587 | type=int, 588 | help="Specify seconds to wait before executing the JavaScript file.", 589 | ) 590 | @click.option( 591 | "--force-manifest", 592 | is_flag=True, 593 | help="Force modify AndroidManifest.xml even if it already has required permissions.", 594 | ) 595 | @click.option( 596 | "--custom-gadget-name", 597 | default=None, 598 | help="Specify a custom name for the Frida gadget.", 599 | ) 600 | @click.option("--no-res", is_flag=True, help="Skip decoding resources.") 601 | @click.option( 602 | "--main-activity", default=None, help="Specify the main activity if known." 603 | ) 604 | @click.option( 605 | "--sign", is_flag=True, help="Automatically sign the APK using uber-apk-signer." 606 | ) 607 | @click.option("--skip-decompile", is_flag=True, help="Skip the decompilation step.") 608 | @click.option("--skip-recompile", is_flag=True, help="Skip the recompilation step.") 609 | @click.option( 610 | "--use-aapt2", 611 | is_flag=True, 612 | help="Use aapt2 instead of aapt for resource processing.", 613 | ) 614 | @click.option( 615 | "--decompile-opts", 616 | default=None, 617 | help="Specify additional options for apktool decompile.", 618 | ) 619 | @click.option( 620 | "--recompile-opts", 621 | default=None, 622 | help="Specify additional options for apktool recompile.", 623 | ) 624 | @click.option( 625 | "--apktool-path", default=None, help="Specify the path or command to run apktool." 626 | ) 627 | @click.option("--frida-version", default=None, help="Specify the Frida version to use.") 628 | @click.option("--ks", default=None, help="The keystore file. If not provided, will use debug keystore.") 629 | @click.option("--ks-alias", default=None, help="The alias of the used key in the keystore.") 630 | @click.option("--ks-key-pass", default=None, help="The password for the key.") 631 | @click.option("--ks-pass", default=None, help="The password for the keystore.") 632 | @click.option( 633 | "--version", 634 | is_flag=True, 635 | callback=print_version, 636 | expose_value=False, 637 | is_eager=True, 638 | help="Show the version and exit.", 639 | ) 640 | @click.argument("apk_path", type=click.Path(exists=True), required=True) 641 | def run( 642 | apk_path: str, 643 | arch: str, 644 | config: str, 645 | no_res: bool, 646 | main_activity: str, 647 | sign: bool, 648 | custom_gadget_name: str, 649 | js: str, 650 | js_delay: int, 651 | force_manifest: bool, 652 | skip_decompile: bool, 653 | skip_recompile: bool, 654 | use_aapt2: bool, 655 | decompile_opts: str, 656 | recompile_opts: str, 657 | apktool_path: str, 658 | frida_version: str, 659 | ks: str, 660 | ks_alias: str, 661 | ks_key_pass: str, 662 | ks_pass: str, 663 | ): 664 | """Patch an APK with the Frida gadget library""" 665 | apk_path = Path(apk_path) 666 | 667 | logger.info("APK: '%s'", apk_path) 668 | if arch is None: 669 | arch = detect_adb_arch() 670 | elif arch.lower() == "multi-arch": 671 | if skip_decompile: 672 | logger.warning( 673 | "The 'multi-arch' option requires decompiling the APK first to detect architectures" 674 | ) 675 | arch = "multi-arch" 676 | elif arch == "arm64-v8a": 677 | arch = "arm64" 678 | elif arch == "armeabi-v7a": 679 | arch = "arm" 680 | 681 | # Validate js-delay option 682 | if js_delay is not None: 683 | if js is None: 684 | logger.error("The --js-delay option requires --js option to be specified.") 685 | sys.exit(-1) 686 | if js_delay < 0: 687 | logger.error("Delay value must be a positive number.") 688 | sys.exit(-1) 689 | logger.info("JavaScript execution will be delayed by %d seconds", js_delay) 690 | 691 | # Process JavaScript file with delay if specified 692 | if js and js_delay is not None: 693 | js_path = Path(js) 694 | if not js_path.exists(): 695 | logger.error("The specified JavaScript file does not exist: %s", js) 696 | sys.exit(-1) 697 | 698 | try: 699 | original_content = js_path.read_text() 700 | wrapped_content = wrap_js_with_timeout(original_content, js_delay) 701 | 702 | # Create a temporary file with wrapped content 703 | temp_js = js_path.parent / f"{js_path.stem}_wrapped{js_path.suffix}" 704 | temp_js.write_text(wrapped_content) 705 | js = str(temp_js) 706 | logger.debug("Created wrapped JavaScript file: %s", js) 707 | except Exception as e: 708 | logger.error("Failed to process JavaScript file: %s", str(e)) 709 | sys.exit(-1) 710 | 711 | global APKTOOL 712 | if apktool_path: 713 | APKTOOL = apktool_path 714 | apktool_parts = APKTOOL.split() 715 | apktool_binary = apktool_parts[-1] 716 | if not Path(apktool_binary).exists(): 717 | logger.error( 718 | "The specified apktool path does not exist: %s", apktool_binary 719 | ) 720 | sys.exit(-1) 721 | 722 | if len(apktool_parts) > 1: 723 | logger.info("Using custom apktool command: '%s'", APKTOOL) 724 | else: 725 | logger.info("Using custom apktool path: '%s'", APKTOOL) 726 | else: 727 | if not APKTOOL: 728 | raise FileNotFoundError( 729 | "apktool not found. Please install apktool and add it to your PATH environment.\n" 730 | "For macOS: brew install apktool\n" 731 | "For Windows: Download from https://ibotpeaches.github.io/Apktool/install/\n" 732 | "For Linux: sudo apt-get install apktool\n" 733 | "After installation, you may need to restart your terminal." 734 | ) 735 | 736 | if arch != "multi-arch": 737 | logger.info( 738 | "Gadget Architecture(--arch): %s%s", 739 | arch, 740 | "(default)" if arch == "arm64" else "", 741 | ) 742 | else: 743 | logger.info( 744 | "Gadget Architecture(--arch): %s (will inject for all architectures found in APK)", 745 | arch, 746 | ) 747 | 748 | if js and not Path(js).exists(): 749 | logger.error("The specified JavaScript file does not exist: %s", js) 750 | sys.exit(-1) 751 | 752 | if config and not Path(config).exists(): 753 | logger.error("The specified configuration file does not exist: %s", config) 754 | sys.exit(-1) 755 | elif config: 756 | try: 757 | with open(config, "r") as f: 758 | json.load(f) 759 | except json.JSONDecodeError: 760 | logger.error( 761 | "The specified configuration file is not a valid JSON: %s", config 762 | ) 763 | sys.exit(-1) 764 | 765 | if arch != "multi-arch": 766 | arch = arch.lower() 767 | supported_archs = ["arm", "arm64", "x86", "x86_64"] 768 | if arch not in supported_archs: 769 | logger.error( 770 | "The --arch option only supports the following architectures: %s, multi-arch", 771 | ", ".join(supported_archs), 772 | ) 773 | sys.exit(-1) 774 | 775 | # Make temp directory for decompile 776 | decompiled_path = TEMP_DIR.joinpath(str(apk_path.resolve())[:-4]) 777 | if not skip_decompile: 778 | logger.debug('Decompiling the target APK using apktool\n"%s"', decompiled_path) 779 | if decompiled_path.exists(): 780 | shutil.rmtree(decompiled_path) 781 | decompiled_path.mkdir() 782 | 783 | # APK decompile with apktool 784 | decompile_option = ["d", "-o", str(decompiled_path.resolve()), "--force"] 785 | if force_manifest: 786 | decompile_option += ["--force-manifest"] 787 | if no_res: 788 | decompile_option += ["--no-res"] 789 | if decompile_opts: 790 | if "--no-res" in decompile_opts: 791 | if no_res: 792 | # remove no-res option if it's already in the list 793 | decompile_option.remove("--no-res") 794 | no_res = True 795 | decompile_option += decompile_opts.split() 796 | 797 | run_apktool(decompile_option, str(apk_path.resolve())) 798 | else: 799 | if not decompiled_path.exists(): 800 | logger.error("Decompiled directory not found: %s", decompiled_path) 801 | sys.exit(-1) 802 | 803 | # Process if decompile is success 804 | modified_dex_number = inject_gadget_into_apk( 805 | apk_path, 806 | arch, 807 | decompiled_path, 808 | no_res, 809 | force_manifest, 810 | main_activity, 811 | config, 812 | js, 813 | custom_gadget_name, 814 | frida_version, 815 | ) 816 | 817 | # Rebuild with apktool, print apk_path if process is success 818 | if not skip_recompile: 819 | logger.debug('Recompiling the new APK using apktool "%s"', decompiled_path) 820 | 821 | recompile_option = ["b"] 822 | if use_aapt2: 823 | recompile_option += ["--use-aapt2"] 824 | if recompile_opts: 825 | recompile_option += recompile_opts.split() 826 | 827 | run_apktool(recompile_option, str(decompiled_path.resolve())) 828 | recompiled_apk_path = decompiled_path.joinpath("dist", apk_path.name) 829 | if not recompiled_apk_path.exists(): 830 | logger.error("APK not found: %s", recompiled_apk_path) 831 | else: 832 | logger.info("Frida gadget injected into APK: %s", recompiled_apk_path) 833 | 834 | # Clean up wrapped JavaScript file if it exists 835 | if js and js_delay is not None: 836 | temp_js = Path(js) 837 | if temp_js.exists() and temp_js.name.endswith("_wrapped.js"): 838 | try: 839 | temp_js.unlink() 840 | logger.debug("Cleaned up wrapped JavaScript file: %s", temp_js) 841 | except Exception as e: 842 | logger.warning( 843 | "Failed to clean up wrapped JavaScript file: %s", str(e) 844 | ) 845 | 846 | # Copy original dex files except the modified one to the recompiled APK 847 | logger.debug(f"Copying original dex files (except modified one {modified_dex_number}) to the recompiled APK") 848 | try: 849 | # Create a temporary directory for extraction 850 | with tempfile.TemporaryDirectory() as temp_dir: 851 | temp_dir_path = Path(temp_dir) 852 | 853 | # Extract original APK 854 | original_apk_zip = zipfile.ZipFile(apk_path, 'r') 855 | original_apk_zip.extractall(temp_dir_path) 856 | original_apk_zip.close() 857 | 858 | # Create a temporary file for the recompiled APK 859 | with tempfile.NamedTemporaryFile(delete=False) as temp_file: 860 | temp_file_path = Path(temp_file.name) 861 | 862 | # Open recompiled APK for reading 863 | recompiled_apk = zipfile.ZipFile(recompiled_apk_path, 'r') 864 | 865 | # Create a new ZIP file 866 | new_apk = zipfile.ZipFile(temp_file_path, 'w') 867 | 868 | # Copy all files from recompiled APK except dex files 869 | modified_dex_filename = f"classes{modified_dex_number}.dex" \ 870 | if modified_dex_number and modified_dex_number > 1 else "classes.dex" 871 | 872 | for item in recompiled_apk.infolist(): 873 | if item.filename == modified_dex_filename: 874 | logger.debug(f"Copying {item.filename} from recompiled APK") 875 | new_apk.writestr(item, recompiled_apk.read(item.filename)) 876 | elif item.filename.startswith('classes') and item.filename.endswith('.dex'): 877 | dex_file = temp_dir_path.joinpath(item.filename) 878 | new_apk.write(str(dex_file), dex_file.name) 879 | else: 880 | new_apk.writestr(item, recompiled_apk.read(item.filename)) 881 | 882 | # Close all zip files 883 | recompiled_apk.close() 884 | new_apk.close() 885 | 886 | # Replace the recompiled APK with the new one 887 | shutil.move(temp_file_path, recompiled_apk_path) 888 | logger.info("Successfully replaced dex files in the recompiled APK") 889 | except Exception as e: 890 | logger.error(f"Failed to copy original dex files: {str(e)}") 891 | 892 | if sign: 893 | logger.debug("Starting APK signing using uber-apk-signer") 894 | sign_apk(str(recompiled_apk_path), ks, ks_alias, ks_key_pass, ks_pass) 895 | return 896 | else: 897 | logger.info(apk_path) 898 | logger.warning( 899 | "The APK is not signed. Use the --sign option to sign it automatically, " 900 | "or sign the APK manually before installing it." 901 | ) 902 | 903 | 904 | if __name__ == "__main__": 905 | # pylint: disable=no-value-for-parameter 906 | run() 907 | --------------------------------------------------------------------------------