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

Added Detection and Analysis of Android Implicit Intents #714

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion agent/src/android/intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ 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";
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;
Expand Down Expand Up @@ -63,3 +67,46 @@ export const startService = (serviceClass: string): Promise<void> => {
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<void> => {

const job: IJob = {
identifier: jobs.identifier(),
implementations: [],
type: `implicit-intent-analyser`,
};
jobs.add(job)

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);
};
job.implementations.push(method.overload);
});
} catch (e) {
send(`[-] Error hooking ${c.redBright(`${hook.className}.${hook.methodName}: ${e}`)}`);
}
});
});
};
59 changes: 59 additions & 0 deletions agent/src/android/lib/intentUtils.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
};
4 changes: 4 additions & 0 deletions agent/src/android/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
1 change: 1 addition & 0 deletions agent/src/rpc/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const android = {
// android intents
androidIntentStartActivity: (activityClass: string): Promise<void> => intent.startActivity(activityClass),
androidIntentStartService: (serviceClass: string): Promise<void> => intent.startService(serviceClass),
androidIntentAnalyze: (): Promise<void> => intent.analyzeImplicits(),

// android keystore
androidKeystoreClear: () => keystore.clear(),
Expand Down
7 changes: 7 additions & 0 deletions objection/commands/android/intents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
124 changes: 112 additions & 12 deletions objection/commands/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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']))

Expand Down Expand Up @@ -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
11 changes: 8 additions & 3 deletions objection/console/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
'exec': filemanager.pwd_print,
},

'file': {
'filesystem': {
'meta': 'Work with files on the remote filesystem',
'commands': {
'cat': {
Expand All @@ -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
},

Expand Down Expand Up @@ -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
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion tests/commands/android/test_intents.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)