-
Notifications
You must be signed in to change notification settings - Fork 844
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
base: master
Are you sure you want to change the base?
Changes from all commits
0954bae
6994ada
8b4f122
7a678f8
3d6161b
77b9b35
6669696
c58766e
a93855c
21e282d
c3aef7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
|
@@ -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. | ||
|
||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ requests | |
Flask>=3.0.0 | ||
Pygments>=2.0.0 | ||
litecli>=1.3.0 | ||
lief>=0.14.0 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.