Skip to content
22 changes: 14 additions & 8 deletions objection/commands/mobile_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -111,18 +112,18 @@ 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:
:param skip_signing:

:return:
"""

github = Github(gadget_version=gadget_version)
android_gadget = AndroidGadget(github)

Expand Down Expand Up @@ -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():
Expand All @@ -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')
Expand All @@ -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()
Expand Down
39 changes: 25 additions & 14 deletions objection/console/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down
82 changes: 67 additions & 15 deletions objection/utils/patchers/android.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import contextlib
import functools
import lzma
import os
import re
Expand All @@ -8,6 +9,7 @@

import click
import delegator
import lief
import requests
import semver

Expand Down Expand Up @@ -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
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this is used for anything.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using it below in inject_load_library for injecting the library with the correct architecture.


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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have this right, I think we are going to be injecting into every native library here? I'm not sure that is what we want :) Not sure what the right approach would be here to choose a target. Maybe random?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I don't have any idea for a better strategy so far, happy to take feedbacks and ideas.

I'm basically replicating method 1 from https://fadeevab.com/frida-gadget-injection-on-android-no-root-2-methods/. Problem is that bundled native library in the APK may not be loaded upon APK initialization (they could be dynamically loaded later on at runtime). Therefore, if we inject in a random library, we might inject in an unused library?

This is quite old but I remember having apps exhibiting this behavior.

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())
Expand Down Expand Up @@ -853,32 +899,38 @@ 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.

: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):
click.secho('Creating library path: {0}'.format(libs_path), dim=True)
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.

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ requests
Flask>=3.0.0
Pygments>=2.0.0
litecli>=1.3.0
lief>=0.14.0