From ef84ab07889fd5b871b41dd70f3038cf3859b208 Mon Sep 17 00:00:00 2001 From: Isak Van Der Walt Date: Fri, 13 Sep 2024 11:51:55 +0200 Subject: [PATCH 1/3] Added functionality to analyse detect and analyse implicit intents --- agent/src/android/intent.ts | 36 +++++ agent/src/android/lib/intentUtils.ts | 59 +++++++++ agent/src/android/lib/types.ts | 4 + agent/src/rpc/android.ts | 1 + objection/commands/android/intents.py | 7 + objection/commands/filemanager.py | 124 ++++++++++++++++-- objection/console/commands.py | 11 +- .../android.intent.implicit_intents.txt | 8 ++ tests/commands/android/test_intents.py | 8 +- 9 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 agent/src/android/lib/intentUtils.ts create mode 100644 objection/console/helpfiles/android.intent.implicit_intents.txt diff --git a/agent/src/android/intent.ts b/agent/src/android/intent.ts index 598e6b4f..6de1748b 100644 --- a/agent/src/android/intent.ts +++ b/agent/src/android/intent.ts @@ -63,3 +63,39 @@ export const startService = (serviceClass: string): Promise => { send(c.blackBright(`Service successfully asked to start.`)); }); }; + +// Analyzes and Detects Android Implicit Intents +// https://developer.android.com/guide/components/intents-filters#Types +export const analyzeImplicits = (): Promise => { + + + return wrapJavaPerform(() => { + const classesToHook = [ + { className: "android.app.Activity", methodName: "startActivityForResult" }, + { className: "android.app.Activity", methodName: "onActivityResult" }, + { className: "androidx.activity.ComponentActivity", methodName: "onActivityResult" }, + { className: "android.content.Context", methodName: "startActivity"}, + { className: "android.content.BroadcastReceiver", methodName: "onReceive"} + // Add other classes and methods as needed + ]; + + classesToHook.forEach(hook => { + try { + const clazz = Java.use(hook.className); + const method = clazz[hook.methodName]; + method.overloads.forEach((overload: FridaOverload) => { + overload.implementation = function (...args: any[]): any { + args.forEach(arg => { + if (arg && arg.$className === "android.content.Intent") { + analyseIntent(`${hook.className}::${hook.methodName}`, arg); + } + }); + return overload.apply(this, args); + }; + }); + } catch (e) { + send(`[-] Error hooking ${c.redBright(`${hook.className}.${hook.methodName}: ${e}`)}`); + } + }); + }); +}; \ No newline at end of file diff --git a/agent/src/android/lib/intentUtils.ts b/agent/src/android/lib/intentUtils.ts new file mode 100644 index 00000000..a9daf592 --- /dev/null +++ b/agent/src/android/lib/intentUtils.ts @@ -0,0 +1,59 @@ +import { colors as c } from "../../lib/color.js"; + +export const analyseIntent = (methodName: string, intent: Java.Wrapper): void => { + try { + send(`\nAnalyzing Intent from: ${c.green(`${methodName}`)}`); + + // Get Component + const component = intent.getComponent(); + if (component) { + send(`[-] ${c.green('Intent Type: Explicit Intent')}`); + } else { + send(`[+] ${c.redBright('Intent Type: Implicit Intent Detected!')}`); + + // Log intent details + send(`[+] Action: ${ `${c.green(`${intent.getAction()}`)}` || `${c.redBright(`[None]`)}` }`); + send(`[+] Data URI: ${ `${c.green(`${intent.getDataString()}`)}` || `${c.redBright(`[None]`)}` }`); + send(`[+] Type: ${ `${c.green(`${intent.getType()}`)}` || `${c.redBright(`[None]`)}` }`); + send(`[+] Flags: ${c.green(`0x${intent.getFlags().toString(16)}`)}`); + + // Categories + const categories = intent.getCategories(); + if (categories) { + send("\n[+] Categories:"); + const iterator = categories.iterator(); + while (iterator.hasNext()) { + send(`[+] Category: ${c.green(`${iterator.next()}`)} `); + } + } else { + send(`[-] Category: ${`${c.redBright(`[None]`)}`}`); + } + + // Extras + const extras = intent.getExtras(); + if (extras) { + send(`[+] Extras: ${c.green(`${extras}`)}`); + } else { + send(`[-] Extras: ${`${c.redBright(`[None]`)}`}`); + } + + // Resolving implicit intents + const activityContext = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext(); + if (activityContext) { + const packageManager = activityContext.getPackageManager(); + const resolveInfoList = packageManager.queryIntentActivities(intent, Java.use("android.content.pm.PackageManager").MATCH_ALL.value); + + send("[+] Responding apps:"); + for (let i = 0; i < resolveInfoList.size(); i++) { + const resolveInfo = resolveInfoList.get(i); + send(`[*] Resolve Info List at positi on ${i}: ${c.green(`${resolveInfo.toString()}`)}`); + } + } else { + send("[-] No activity context available"); + } + + } + } catch (e) { + send(`[!] Error analyzing intent: ${e}`); + } +}; \ No newline at end of file diff --git a/agent/src/android/lib/types.ts b/agent/src/android/lib/types.ts index e53714f2..90b97488 100644 --- a/agent/src/android/lib/types.ts +++ b/agent/src/android/lib/types.ts @@ -31,3 +31,7 @@ export type ActivityClientRecord = JavaClass | any; export type Bitmap = JavaClass | any; export type ByteArrayOutputStream = JavaClass | any; export type CompressFormat = JavaClass | any; +export type FridaOverload = { + implementation: (...args: any[]) => any; + apply: (thisArg: any, args: any[]) => any; +}; \ No newline at end of file diff --git a/agent/src/rpc/android.ts b/agent/src/rpc/android.ts index 82d6797c..1340ab2a 100644 --- a/agent/src/rpc/android.ts +++ b/agent/src/rpc/android.ts @@ -71,6 +71,7 @@ export const android = { // android intents androidIntentStartActivity: (activityClass: string): Promise => intent.startActivity(activityClass), androidIntentStartService: (serviceClass: string): Promise => intent.startService(serviceClass), + androidIntentAnalyze: (): Promise => intent.analyzeImplicits(), // android keystore androidKeystoreClear: () => keystore.clear(), diff --git a/objection/commands/android/intents.py b/objection/commands/android/intents.py index ed17aae7..719956dc 100644 --- a/objection/commands/android/intents.py +++ b/objection/commands/android/intents.py @@ -3,6 +3,13 @@ from objection.state.connection import state_connection from objection.utils.helpers import clean_argument_flags +def analyze_implicit_intents(args: list) -> None: + """ + Analyzes implicit intents in hooked methods. + """ + api = state_connection.get_api() + api.android_intent_analyze() + click.secho('Started implicit intent analysis', bold=True) def launch_activity(args: list) -> None: """ diff --git a/objection/commands/filemanager.py b/objection/commands/filemanager.py index 89f99560..f0342084 100644 --- a/objection/commands/filemanager.py +++ b/objection/commands/filemanager.py @@ -16,6 +16,17 @@ _ls_cache = {} +def _should_download_folder(args: list) -> bool: + """ + Checks if --json is in the list of tokens received from the command line. + + :param args: + :return: + """ + + return len(args) > 0 and '--folder' in args + + def cd(args: list) -> None: """ Change the current working directory of the device. @@ -393,14 +404,16 @@ def download(args: list) -> None: source = args[0] destination = args[1] if len(args) > 1 else os.path.basename(source) + should_download_folder = _should_download_folder(args) + if device_state.platform == Ios: - _download_ios(source, destination) + _download_ios(source, destination, should_download_folder) if device_state.platform == Android: - _download_android(source, destination) + _download_android(source, destination, should_download_folder) -def _download_ios(path: str, destination: str) -> None: +def _download_ios(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from an iOS filesystem and store it locally. @@ -416,27 +429,55 @@ def _download_ios(path: str, destination: str) -> None: api = state_connection.get_api() - click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) + if path_root: + click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.ios_file_readable(path): click.secho('Unable to download file. File is not readable.', fg='red') return if not api.ios_file_path_is_file(path): - click.secho('Unable to download file. Target path is not a file.', fg='yellow') + if not should_download_folder: + click.secho('To download folders, specify --folder.', fg='yellow') + return + + if os.path.exists(destination): + click.secho('The target path already exists.', fg='yellow') + return + + os.makedirs(destination) + + if path_root: + if not click.confirm('Do you want to download the full directory?', default=True): + click.secho('Download aborted.', fg='yellow') + return + click.secho('Downloading directory recursively...', fg='green') + + data = api.ios_file_ls(path) + for name, _ in data['files'].items(): + sub_path = device_state.platform.path_separator.join([path, name]) + sub_destination = os.path.join(destination, name) + + _download_ios(sub_path, sub_destination, True, False) + if path_root: + click.secho('Recursive download finished.', fg='green') + return - click.secho('Streaming file from device...', dim=True) + if path_root: + click.secho('Streaming file from device...', dim=True) file_data = api.ios_file_download(path) - click.secho('Writing bytes to destination...', dim=True) + if path_root: + click.secho('Writing bytes to destination...', dim=True) + with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True) -def _download_android(path: str, destination: str) -> None: +def _download_android(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from the Android filesystem and store it locally. @@ -452,20 +493,47 @@ def _download_android(path: str, destination: str) -> None: api = state_connection.get_api() - click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) + if path_root: + click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.android_file_readable(path): click.secho('Unable to download file. Target path is not readable.', fg='red') return if not api.android_file_path_is_file(path): - click.secho('Unable to download file. Target path is not a file.', fg='yellow') + if not should_download_folder: + click.secho('To download folders, specify --folder.', fg='yellow') + return + + if os.path.exists(destination): + click.secho('The target path already exists.', fg='yellow') + return + + os.makedirs(destination) + + if path_root: + if not click.confirm('Do you want to download the full directory?', default=True): + click.secho('Download aborted.', fg='yellow') + return + click.secho('Downloading directory recursively...', fg='green') + + data = api.android_file_ls(path) + for name, _ in data['files'].items(): + sub_path = device_state.platform.path_separator.join([path, name]) + sub_destination = os.path.join(destination, name) + + _download_android(sub_path, sub_destination, True, False) + if path_root: + click.secho('Recursive download finished.', fg='green') return - click.secho('Streaming file from device...', dim=True) + if path_root: + click.secho('Streaming file from device...', dim=True) file_data = api.android_file_download(path) - click.secho('Writing bytes to destination...', dim=True) + if path_root: + click.secho('Writing bytes to destination...', dim=True) + with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) @@ -819,3 +887,35 @@ def list_files_in_current_fm_directory() -> dict: resp[file_name] = file_name return resp + + +def list_content_in_current_fm_directory() -> dict: + """ + Return folders and files in the current working directory of the + Frida attached device. + """ + + resp = {} + + # check for existence based on the runtime + if device_state.platform == Ios: + response = _get_short_ios_listing() + + elif device_state.platform == Android: + response = _get_short_android_listing() + + # looks like we landed in an unknown runtime. + # just return. + else: + return resp + + # loop the response to get entries. + for entry in response: + name, _ = entry + + if ' ' in name: + resp[f"'{name}'"] = name + else: + resp[name] = name + + return resp diff --git a/objection/console/commands.py b/objection/console/commands.py index 2702a1be..49a8da0b 100644 --- a/objection/console/commands.py +++ b/objection/console/commands.py @@ -119,7 +119,7 @@ 'exec': filemanager.pwd_print, }, - 'file': { + 'filesystem': { 'meta': 'Work with files on the remote filesystem', 'commands': { 'cat': { @@ -132,8 +132,9 @@ 'exec': filemanager.upload }, 'download': { - 'meta': 'Download a file', - 'dynamic': filemanager.list_files_in_current_fm_directory, + 'meta': 'Download a file or folder', + 'flags': ['--folder'], + 'dynamic': filemanager.list_content_in_current_fm_directory, 'exec': filemanager.download }, @@ -456,6 +457,10 @@ 'launch_service': { 'meta': 'Launch a Service class using an Intent', 'exec': intents.launch_service + }, + 'implicit_intents': { + 'meta': 'Analyze implicit intents', + 'exec': intents.analyze_implicit_intents } } }, diff --git a/objection/console/helpfiles/android.intent.implicit_intents.txt b/objection/console/helpfiles/android.intent.implicit_intents.txt new file mode 100644 index 00000000..59524d52 --- /dev/null +++ b/objection/console/helpfiles/android.intent.implicit_intents.txt @@ -0,0 +1,8 @@ +Command: android intent implicit_intents + +Usage: android intent implicit_intents + +Starts a hook to analyze implicit intents during runtime. + +Examples: + android intent implicit_intents diff --git a/tests/commands/android/test_intents.py b/tests/commands/android/test_intents.py index c77be0aa..5bb33195 100644 --- a/tests/commands/android/test_intents.py +++ b/tests/commands/android/test_intents.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from objection.commands.android.intents import launch_activity, launch_service +from objection.commands.android.intents import launch_activity, launch_service, analyze_implicit_intents from ...helpers import capture @@ -29,3 +29,9 @@ def test_launch_service(self, mock_api): launch_service(['com.foo.bar']) self.assertTrue(mock_api.return_value.android_intent_start_service.called) + + @mock.patch('objection.state.connection.state_connection.get_api') + def test_analyze_implicit_intents(self, mock_api): + analyze_implicit_intents([]) + + self.assertTrue(mock_api.return_value.android_intent_analyze.called) From 973cf2ecdac7d96c8040d774540d2e323d2edaa8 Mon Sep 17 00:00:00 2001 From: lehasa Date: Mon, 13 Jan 2025 10:26:09 +0200 Subject: [PATCH 2/3] Fixed Imports in Intents.ts to include /lib/IntentUtils.ts --- agent/src/android/intent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/src/android/intent.ts b/agent/src/android/intent.ts index 6de1748b..c3406eb3 100644 --- a/agent/src/android/intent.ts +++ b/agent/src/android/intent.ts @@ -3,7 +3,8 @@ import { getApplicationContext, wrapJavaPerform } from "./lib/libjava.js"; -import { Intent } from "./lib/types.js"; +import { Intent, FridaOverload } from "./lib/types.js"; +import { analyseIntent } from "./lib/intentUtils.js"; // https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK const FLAG_ACTIVITY_NEW_TASK = 0x10000000; From 3818176ecd3e28693a4d0374d70dc1310f552c6f Mon Sep 17 00:00:00 2001 From: lehasa Date: Tue, 14 Jan 2025 11:30:17 +0200 Subject: [PATCH 3/3] Included Jobs into the Implicit Intent Analyser --- agent/src/android/intent.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/agent/src/android/intent.ts b/agent/src/android/intent.ts index c3406eb3..c63dfb17 100644 --- a/agent/src/android/intent.ts +++ b/agent/src/android/intent.ts @@ -5,6 +5,9 @@ import { } from "./lib/libjava.js"; import { Intent, FridaOverload } from "./lib/types.js"; import { analyseIntent } from "./lib/intentUtils.js"; +import { IJob } from "../lib/interfaces.js"; +import * as jobs from "../lib/jobs.js"; + // https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK const FLAG_ACTIVITY_NEW_TASK = 0x10000000; @@ -69,6 +72,12 @@ export const startService = (serviceClass: string): Promise => { // https://developer.android.com/guide/components/intents-filters#Types export const analyzeImplicits = (): Promise => { + const job: IJob = { + identifier: jobs.identifier(), + implementations: [], + type: `implicit-intent-analyser`, + }; + jobs.add(job) return wrapJavaPerform(() => { const classesToHook = [ @@ -93,6 +102,7 @@ export const analyzeImplicits = (): Promise => { }); return overload.apply(this, args); }; + job.implementations.push(method.overload); }); } catch (e) { send(`[-] Error hooking ${c.redBright(`${hook.className}.${hook.methodName}: ${e}`)}`);