diff --git a/objection/commands/mobile_packages.py b/objection/commands/mobile_packages.py index 59efd6d5..65d90734 100644 --- a/objection/commands/mobile_packages.py +++ b/objection/commands/mobile_packages.py @@ -97,9 +97,10 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, bin def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: bool = True, - enable_debug: bool = True, gadget_version: str = None, skip_resources: bool = False, + enable_debug: bool = True, gadget_version: str = None, decode_resources: bool = False, network_security_config: bool = False, target_class: str = None, - use_aapt2: bool = False, gadget_config: str = None, script_source: str = None, + use_aapt1: bool = False, gadget_name: str = 'libfrida-gadget.so', + gadget_config: str = None, script_source: str = None, ignore_nativelibs: bool = True, manifest: str = None, skip_signing: bool = False, only_main_classes: bool = False) -> None: """ Patches an Android APK by extracting, patching SMALI, repackaging @@ -111,10 +112,11 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: :param skip_cleanup: :param enable_debug: :param gadget_version: - :param skip_resources: + :param decode_resources: :param network_security_config: :param target_class: - :param use_aapt2: + :param use_aapt1: + :param gadget_name: :param gadget_config: :param script_source: :param manifest: @@ -122,7 +124,6 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: :return: """ - github = Github(gadget_version=gadget_version) android_gadget = AndroidGadget(github) @@ -176,7 +177,7 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green') - patcher = AndroidPatcher(skip_cleanup=skip_cleanup, skip_resources=skip_resources, manifest=manifest, only_main_classes=only_main_classes) + patcher = AndroidPatcher(skip_cleanup=skip_cleanup, decode_resources=decode_resources, manifest=manifest, only_main_classes=only_main_classes) # ensure that we have all of the commandline requirements if not patcher.are_requirements_met(): @@ -201,8 +202,13 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: if network_security_config: patcher.add_network_security_config() + patcher.add_gadget_to_apk( + architecture, + android_gadget.get_frida_library_path(), + gadget_config, + gadget_name + ) patcher.inject_load_library(target_class=target_class) - patcher.add_gadget_to_apk(architecture, android_gadget.get_frida_library_path(), gadget_config) if script_source: click.secho('Copying over a custom script to use with the gadget config.', fg='green') @@ -219,7 +225,7 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: input('Press ENTER to continue...') - patcher.build_new_apk(use_aapt2=use_aapt2) + patcher.build_new_apk(use_aapt1=use_aapt1) patcher.zipalign_apk() if not skip_signing: patcher.sign_apk() diff --git a/objection/console/cli.py b/objection/console/cli.py index 14e6c701..38dd12bf 100644 --- a/objection/console/cli.py +++ b/objection/console/cli.py @@ -54,7 +54,7 @@ def get_agent() -> Agent: @click.option('--debugger', required=False, default=False, is_flag=True, help='Enable the Chrome debug port.') @click.option('--uid', required=False, default=None, help='Specify the uid to run as (Android only).') def cli(network: bool, host: str, port: int, api_host: str, api_port: int, - name: str, serial: str, debug: bool, spawn: bool, no_pause: bool, + name: str, serial: str, debug: bool, spawn: bool, no_pause: bool, foremost: bool, debugger: bool, uid: int) -> None: """ \b @@ -261,14 +261,20 @@ def patchipa(source: str, gadget_version: str, codesign_signature: str, provisio help='Set the android:debuggable flag to true in the application manifest.', show_default=True) @click.option('--network-security-config', '-N', is_flag=True, default=False, help='Include a network_security_config.xml file allowing for user added CA\'s to be trusted on ' - 'Android 7 and up. This option can not be used with the --skip-resources flag.') -@click.option('--skip-resources', '-D', is_flag=True, default=False, - help='Skip resource decoding as part of the apktool processing.', show_default=False) + 'Android 7 and up. This option requires the --decode-resources flag.') +@click.option('--decode-resources', '-D', is_flag=True, default=False, + help='Decode resource as part of the apktool processing.', show_default=False) @click.option('--skip-signing', '-C', is_flag=True, default=False, help='Skip signing the apk file.', show_default=False) @click.option('--target-class', '-t', help='The target class to patch.', default=None) -@click.option('--use-aapt2', '-2', is_flag=True, default=False, - help='Use the aapt2 binary instead of aapt as part of the apktool processing.', show_default=False) +@click.option('--use-aapt1', '-1', is_flag=True, default=False, + help='Use the aapt binary instead of aapt2 as part of the apktool processing.', show_default=True) +@click.option('--gadget-name', '-g', default='libfrida-gadget.so', + help=( + 'Name of the gadget library. Can be named whatever you want to dodge anti-frida ' + 'detection schemes looking for loaded libraries with frida in the name.' + 'Refer to https://frida.re/docs/gadget/ for more information.'), + show_default=True) @click.option('--gadget-config', '-c', default=None, help=( 'The gadget configuration file to use. ' 'Refer to https://frida.re/docs/gadget/ for more information.'), show_default=False) @@ -280,25 +286,30 @@ def patchipa(source: str, gadget_version: str, codesign_signature: str, provisio @click.option('--manifest', '-m', help='A decoded AndroidManifest.xml file to read.', default=None) @click.option('--only-main-classes', help="Only patch classes that are in the main dex file.", is_flag=True, default=False) def patchapk(source: str, architecture: str, gadget_version: str, pause: bool, skip_cleanup: bool, - enable_debug: bool, skip_resources: bool, network_security_config: bool, target_class: str, - use_aapt2: bool, gadget_config: str, script_source: str, ignore_nativelibs: bool, manifest: str, skip_signing: bool, only_main_classes:bool = False) -> None: + enable_debug: bool, decode_resources: bool, network_security_config: bool, target_class: str, + use_aapt1: bool, gadget_name: str, gadget_config: str, script_source: str, ignore_nativelibs: bool, manifest: str, skip_signing: bool, only_main_classes:bool = False) -> None: """ Patch an APK with the frida-gadget.so. """ # ensure we decode resources if we have the network-security-config flag. - if network_security_config and skip_resources: - click.secho('The --network-security-config flag is incompatible with the --skip-resources flag.', fg='red') + if network_security_config and not decode_resources: + click.secho('The --network-security-config flag requires the --decode-resources flag.', fg='red') return # ensure we decode resources if we have the enable-debug flag. - if enable_debug and skip_resources: - click.secho('The --enable-debug flag is incompatible with the --skip-resources flag.', fg='red') + if enable_debug and not decode_resources: + click.secho('The --enable-debug flag is incompatible with the --decode-resources flag.', fg='red') return # ensure we decode resources if we do not have the --ignore-nativelibs flag. - if not ignore_nativelibs and skip_resources: - click.secho('The --ignore-nativelibs flag is required with the --skip-resources flag.', fg='red') + if not ignore_nativelibs and not decode_resources: + click.secho('The --ignore-nativelibs flag is required with the --decode-resources flag.', fg='red') + return + + # ensure provided gadget name is a valid android lib name + if not gadget_name.startswith('lib') or not gadget_name.endswith('.so'): + click.secho("Gadget name should start with 'lib' and end in '.so'", fg='red') return patch_android_apk(**locals()) diff --git a/objection/utils/patchers/android.py b/objection/utils/patchers/android.py index a843a9d3..71a8825f 100644 --- a/objection/utils/patchers/android.py +++ b/objection/utils/patchers/android.py @@ -1,4 +1,5 @@ import contextlib +import functools import lzma import os import re @@ -8,6 +9,7 @@ import click import delegator +import lief import requests import semver @@ -199,7 +201,7 @@ class AndroidPatcher(BasePlatformPatcher): } } - def __init__(self, skip_cleanup: bool = False, skip_resources: bool = False, manifest: str = None, only_main_classes: bool = False): + def __init__(self, skip_cleanup: bool = False, decode_resources: bool = False, manifest: str = None, only_main_classes: bool = False): super(AndroidPatcher, self).__init__() self.apk_source = None @@ -208,9 +210,11 @@ def __init__(self, skip_cleanup: bool = False, skip_resources: bool = False, man self.apk_temp_frida_patched_aligned = self.apk_temp_directory + '.aligned.objection.apk' self.aapt = None self.skip_cleanup = skip_cleanup - self.skip_resources = skip_resources + self.decode_resources = decode_resources self.manifest = manifest + self.architecture = None + self.keystore = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets', 'objection.jks') self.netsec_config = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets', 'network_security_config.xml') @@ -283,11 +287,11 @@ def _get_android_manifest(self) -> ElementTree: :return: """ - # error if --skip-resources was used because the manifest is encoded - if self.skip_resources is True and self.manifest is None: - click.secho('Cannot manually parse the AndroidManifest.xml when --skip-resources ' - 'is set, remove this and try again, or manually specify a manifest with --manifest.', fg='red') - raise Exception('Cannot --skip-resources when trying to manually parse the AndroidManifest.xml') + # error if --decode-resources was not used because the manifest is encoded + if not self.decode_resources is True and self.manifest is None: + click.secho('Cannot manually parse the AndroidManifest.xml when --decoode-resources ' + 'is not set, add this and try again, or manually specify a manifest with --manifest.', fg='red') + raise Exception('Cannot --decode-resources when trying to manually parse the AndroidManifest.xml') # use the android namespace ElementTree.register_namespace('android', 'http://schemas.android.com/apk/res/android') @@ -404,7 +408,7 @@ def unpack_apk(self): self.required_commands['apktool']['location'], 'decode', '-f', - '-r' if self.skip_resources else '', + '-r' if not self.decode_resources else '', '--only-main-classes' if self.only_main_classes else '', '-o', self.apk_temp_directory, @@ -802,6 +806,30 @@ def _h(): return patched_smali + @functools.cache + def _find_libs_path(self): + """ + Find the libraries path for the target architecture within the APK. + """ + base_libs_path = os.path.join(self.apk_temp_directory, 'lib') + available_libs_arch = os.listdir(base_libs_path) + if self.architecture in available_libs_arch: + # Exact match with arch + return os.path.join(base_libs_path, self.architecture) + else: + # Try to use prefix search + try: + matching_arch = next( + item for item in available_libs_arch if item.startswith(self.architecture) + ) + click.secho('Using matching architecture {0} from provided architecture {1}.'.format( + matching_arch, self.architecture + ), dim=True) + return os.path.join(base_libs_path, matching_arch) + except StopIteration: + # Might create the arch folder inside the APK tree + return os.path.join(base_libs_path, self.architecture) + def inject_load_library(self, target_class: str = None): """ Injects a loadLibrary call into a class. @@ -822,8 +850,26 @@ def inject_load_library(self, target_class: str = None): if target_class: click.secho('Using target class: {0} for patch'.format(target_class), fg='green', bold=True) else: - click.secho('Target class not specified, searching for launchable activity instead...', fg='green', + click.secho('Target class not specified, injecting through existing native libraries...', fg='green', bold=True) + # Inspired by https://fadeevab.com/frida-gadget-injection-on-android-no-root-2-methods/ + if not self.architecture or not self.libfridagadget_name: + raise Exception('Frida-gadget should have been copied prior to injecting!') + libs_path = self._find_libs_path() + existing_libs_in_apk = [ + lib + for lib in os.listdir(libs_path) + if lib not in [self.libfridagadget_name, self.libfridagadgetconfig_name] + ] + if existing_libs_in_apk: + for lib in existing_libs_in_apk: + libnative = lief.parse(os.path.join(libs_path, lib)) + libnative.add_library(self.libfridagadget_name) # Injection! + libnative.write(os.path.join(libs_path, lib)) + return + else: + click.secho('No native libraries found in APK, searching for launchable activity instead...', fg='green', + bold=True) activity_path = self._determine_smali_path_for_class( target_class if target_class else self._get_launchable_activity()) @@ -853,7 +899,9 @@ def inject_load_library(self, target_class: str = None): with open(activity_path, 'w') as f: f.write(''.join(patched_smali)) - def add_gadget_to_apk(self, architecture: str, gadget_source: str, gadget_config: str): + def add_gadget_to_apk(self, architecture: str, + gadget_source: str, gadget_config: str, + libfridagadget_name: str = 'libfrida-gadget.so'): """ Copies a frida gadget for a specific architecture to an extracted APK's lib path. @@ -861,10 +909,14 @@ def add_gadget_to_apk(self, architecture: str, gadget_source: str, gadget_config :param architecture: :param gadget_source: :param gadget_config: + :param libfridagadget_name: :return: """ + self.architecture = architecture + self.libfridagadget_name = libfridagadget_name + self.libfridagadgetconfig_name = libfridagadget_name.replace('.so', '.config.so') - libs_path = os.path.join(self.apk_temp_directory, 'lib', architecture) + libs_path = self._find_libs_path() # check if the libs path exists if not os.path.exists(libs_path): @@ -872,13 +924,13 @@ def add_gadget_to_apk(self, architecture: str, gadget_source: str, gadget_config os.makedirs(libs_path) click.secho('Copying Frida gadget to libs path...', fg='green', dim=True) - shutil.copyfile(gadget_source, os.path.join(libs_path, 'libfrida-gadget.so')) + shutil.copyfile(gadget_source, os.path.join(libs_path, self.libfridagadget_name)) if gadget_config: click.secho('Adding a gadget configuration file...', fg='green') - shutil.copyfile(gadget_config, os.path.join(libs_path, 'libfrida-gadget.config.so')) + shutil.copyfile(gadget_config, os.path.join(libs_path, self.libfridagadgetconfig_name)) - def build_new_apk(self, use_aapt2: bool = False): + def build_new_apk(self, use_aapt1: bool = False): """ Build a new .apk with the frida-gadget patched in. @@ -890,7 +942,7 @@ def build_new_apk(self, use_aapt2: bool = False): self.list2cmdline([self.required_commands['apktool']['location'], 'build', self.apk_temp_directory, - ] + (['--use-aapt2'] if use_aapt2 else []) + [ + ] + (['--use-aapt2'] if not use_aapt1 else []) + [ '-o', self.apk_temp_frida_patched ]), timeout=self.command_run_timeout) diff --git a/requirements.txt b/requirements.txt index 1f678cfa..5cc95715 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ requests Flask>=3.0.0 Pygments>=2.0.0 litecli>=1.3.0 +lief>=0.14.0