Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Few enhancements to gadget injection into APK (patchapk) #586

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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