diff --git a/mvt/android/cli.py b/mvt/android/cli.py index 790fc6e..7c477d6 100644 --- a/mvt/android/cli.py +++ b/mvt/android/cli.py @@ -5,6 +5,7 @@ import logging import os +from zipfile import ZipFile import click from rich.logging import RichHandler @@ -21,6 +22,7 @@ from .lookups.koodous import koodous_lookup from .lookups.virustotal import virustotal_lookup from .modules.adb import ADB_MODULES from .modules.backup import BACKUP_MODULES +from .modules.bugreport import BUGREPORT_MODULES # Setup logging using Rich. LOG_FORMAT = "[%(name)s] %(message)s" @@ -46,7 +48,7 @@ def version(): #============================================================================== -# Download APKs +# Command: download-apks #============================================================================== @cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device") @click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL) @@ -99,7 +101,7 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f #============================================================================== -# Checks through ADB +# Command: check-adb #============================================================================== @cli.command("check-adb", help="Check an Android device over adb") @click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL) @@ -157,7 +159,69 @@ def check_adb(ctx, iocs, output, fast, list_modules, module, serial): #============================================================================== -# Check ADB backup +# Command: check-bugreport +#============================================================================== +@cli.command("check-bugreport", help="Check an Android Bug Report") +@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True, + default=[], help=HELP_MSG_IOC) +@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT) +@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES) +@click.option("--module", "-m", help=HELP_MSG_MODULE) +@click.argument("BUGREPORT_PATH", type=click.Path(exists=True)) +@click.pass_context +def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path): + if list_modules: + log.info("Following is the list of available check-bugreport modules:") + for adb_module in BUGREPORT_MODULES: + log.info(" - %s", adb_module.__name__) + + return + + log.info("Checking an Android Bug Report located at: %s", bugreport_path) + + if output and not os.path.exists(output): + try: + os.makedirs(output) + except Exception as e: + log.critical("Unable to create output folder %s: %s", output, e) + ctx.exit(1) + + indicators = Indicators(log=log) + indicators.load_indicators_files(iocs) + + zip_archive = ZipFile(bugreport_path) + zip_files = [] + for file_name in zip_archive.namelist(): + zip_files.append(file_name) + + timeline = [] + timeline_detected = [] + for bugreport_module in BUGREPORT_MODULES: + if module and bugreport_module.__name__ != module: + continue + + m = bugreport_module(base_folder=bugreport_path, output_folder=output, + log=logging.getLogger(bugreport_module.__module__)) + + m.from_zip(zip_archive, zip_files) + + if indicators.total_ioc_count: + m.indicators = indicators + m.indicators.log = m.log + + run_module(m) + timeline.extend(m.timeline) + timeline_detected.extend(m.timeline_detected) + + if output: + if len(timeline) > 0: + save_timeline(timeline, os.path.join(output, "timeline.csv")) + if len(timeline_detected) > 0: + save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv")) + + +#============================================================================== +# Command: check-backup #============================================================================== @cli.command("check-backup", help="Check an Android Backup") @click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL) diff --git a/mvt/android/modules/adb/dumpsys_accessibility.py b/mvt/android/modules/adb/dumpsys_accessibility.py index e3bae4e..511f310 100644 --- a/mvt/android/modules/adb/dumpsys_accessibility.py +++ b/mvt/android/modules/adb/dumpsys_accessibility.py @@ -47,7 +47,6 @@ class DumpsysAccessibility(AndroidExtraction): break service = line.split(":")[1].strip() - log.info("Found installed accessibility service \"%s\"", service) results.append({ "package_name": service.split("/")[0], @@ -62,6 +61,9 @@ class DumpsysAccessibility(AndroidExtraction): output = self._adb_command("dumpsys accessibility") self.results = self.parse_accessibility(output) + for result in self.results: + log.info("Found installed accessibility service \"%s\"", result.get("service")) + self.log.info("Identified a total of %d accessibility services", len(self.results)) self._adb_disconnect() diff --git a/mvt/android/modules/adb/dumpsys_battery_daily.py b/mvt/android/modules/adb/dumpsys_battery_daily.py index 934f87c..c1abc7d 100644 --- a/mvt/android/modules/adb/dumpsys_battery_daily.py +++ b/mvt/android/modules/adb/dumpsys_battery_daily.py @@ -39,23 +39,22 @@ class DumpsysBatteryDaily(AndroidExtraction): continue @staticmethod - def parse_battery_history(output): + def parse_battery_daily(output): results = [] daily = None daily_updates = [] for line in output.split("\n")[1:]: if line.startswith(" Daily from "): + if len(daily_updates) > 0: + results.extend(daily_updates) + daily_updates = [] + timeframe = line[13:].strip() date_from, date_to = timeframe.strip(":").split(" to ", 1) daily = {"from": date_from[0:10], "to": date_to[0:10]} - - if not daily: continue - if line.strip() == "": - results.extend(daily_updates) - daily = None - daily_updates = [] + if not daily: continue if not line.strip().startswith("Update "): @@ -86,7 +85,7 @@ class DumpsysBatteryDaily(AndroidExtraction): self._adb_connect() output = self._adb_command("dumpsys batterystats --daily") - self.results = self.parse_battery_history(output) + self.results = self.parse_battery_daily(output) self.log.info("Extracted %d records from battery daily stats", len(self.results)) diff --git a/mvt/android/modules/adb/packages.py b/mvt/android/modules/adb/packages.py index 3875e71..cb356de 100644 --- a/mvt/android/modules/adb/packages.py +++ b/mvt/android/modules/adb/packages.py @@ -39,6 +39,7 @@ DANGEROUS_PERMISSIONS = [ "com.android.browser.permission.READ_HISTORY_BOOKMARKS", ] + class Packages(AndroidExtraction): """This module extracts the list of installed packages.""" diff --git a/mvt/android/modules/bugreport/__init__.py b/mvt/android/modules/bugreport/__init__.py new file mode 100644 index 0000000..cea3fc6 --- /dev/null +++ b/mvt/android/modules/bugreport/__init__.py @@ -0,0 +1,14 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +from .accessibility import Accessibility +from .activities import Activities +from .battery_daily import BatteryDaily +from .battery_history import BatteryHistory +from .dbinfo import DBInfo +from .receivers import Receivers + +BUGREPORT_MODULES = [Accessibility, Activities, BatteryDaily, BatteryHistory, + DBInfo, Receivers] diff --git a/mvt/android/modules/bugreport/accessibility.py b/mvt/android/modules/bugreport/accessibility.py new file mode 100644 index 0000000..909e896 --- /dev/null +++ b/mvt/android/modules/bugreport/accessibility.py @@ -0,0 +1,63 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_accessibility import DumpsysAccessibility + +from .base import BugReportModule + +log = logging.getLogger(__name__) + + +class Accessibility(BugReportModule): + """This module extracts stats on accessibility.""" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def check_indicators(self): + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_app_id(result["package_name"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_accessibility = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "DUMP OF SERVICE accessibility:": + in_accessibility = True + continue + + if not in_accessibility: + continue + + if line.strip() == "------------------------------------------------------------------------------": + break + + lines.append(line) + + self.results = DumpsysAccessibility.parse_accessibility("\n".join(lines)) + for result in self.results: + log.info("Found installed accessibility service \"%s\"", result.get("service")) + + self.log.info("Identified a total of %d accessibility services", len(self.results)) diff --git a/mvt/android/modules/bugreport/activities.py b/mvt/android/modules/bugreport/activities.py new file mode 100644 index 0000000..57938e1 --- /dev/null +++ b/mvt/android/modules/bugreport/activities.py @@ -0,0 +1,62 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_activities import DumpsysActivities + +from .base import BugReportModule + +log = logging.getLogger(__name__) + + +class Activities(BugReportModule): + """This module extracts details on receivers for risky activities.""" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + self.results = results if results else {} + + def check_indicators(self): + if not self.indicators: + return + + for intent, activities in self.results.items(): + for activity in activities: + ioc = self.indicators.check_app_id(activity["package_name"]) + if ioc: + activity["matched_indicator"] = ioc + self.detected.append({intent: activity}) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_activities = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "DUMP OF SERVICE package:": + in_activities = True + continue + + if not in_activities: + continue + + if line.strip() == "------------------------------------------------------------------------------": + break + + lines.append(line) + + self.results = DumpsysActivities.parse_activity_resolver_table("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/base.py b/mvt/android/modules/bugreport/base.py new file mode 100644 index 0000000..405d88f --- /dev/null +++ b/mvt/android/modules/bugreport/base.py @@ -0,0 +1,46 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# See the file 'LICENSE' for usage and copying permissions, or find a copy at +# https://github.com/mvt-project/mvt/blob/main/LICENSE + +import fnmatch +import logging +import os + +from mvt.common.module import MVTModule + +log = logging.getLogger(__name__) + + +class BugReportModule(MVTModule): + """This class provides a base for all Android Bug Report modules.""" + + zip_archive = None + + def from_folder(self, extract_path): + self.extract_path = extract_path + + def from_zip(self, zip_archive, zip_files): + self.zip_archive = zip_archive + self.zip_files = zip_files + + def _get_files_by_pattern(self, pattern): + file_names = [] + if self.zip_archive: + for zip_file in self.zip_files: + file_names.append(zip_file) + else: + file_names = self.files + + return fnmatch.filter(file_names, pattern) + + def _get_file_content(self, file_path): + if self.zip_archive: + handle = self.zip_archive.open(file_path) + else: + handle = open(os.path.join(self.parent_path, file_path), "rb") + + data = handle.read() + handle.close() + + return data diff --git a/mvt/android/modules/bugreport/battery_daily.py b/mvt/android/modules/bugreport/battery_daily.py new file mode 100644 index 0000000..9c5d5ab --- /dev/null +++ b/mvt/android/modules/bugreport/battery_daily.py @@ -0,0 +1,76 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_battery_daily import DumpsysBatteryDaily + +from .base import BugReportModule + +log = logging.getLogger(__name__) + + +class BatteryDaily(BugReportModule): + """This module extracts records from battery daily updates.""" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def serialize(self, record): + return { + "timestamp": record["from"], + "module": self.__class__.__name__, + "event": "battery_daily", + "data": f"Recorded update of package {record['package_name']} with vers {record['vers']}" + } + + def check_indicators(self): + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_app_id(result["package_name"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_batterystats = False + in_daily = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "DUMP OF SERVICE batterystats:": + in_batterystats = True + continue + + if not in_batterystats: + continue + + if line.strip() == "Daily stats:": + lines.append(line) + in_daily = True + continue + + if not in_daily: + continue + + if line.strip() == "": + break + + lines.append(line) + + self.results = DumpsysBatteryDaily.parse_battery_daily("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/battery_history.py b/mvt/android/modules/bugreport/battery_history.py new file mode 100644 index 0000000..04609c3 --- /dev/null +++ b/mvt/android/modules/bugreport/battery_history.py @@ -0,0 +1,69 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_battery_history import \ + DumpsysBatteryHistory + +from .base import BugReportModule + +log = logging.getLogger(__name__) + + +class BatteryHistory(BugReportModule): + """This module extracts records from battery daily updates.""" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def check_indicators(self): + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_app_id(result["package_name"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_batterystats = False + in_history = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "********** Print latest newbatterystats **********": + in_batterystats = True + continue + + if not in_batterystats: + continue + + if line.strip().startswith("Battery History "): + lines.append(line) + in_history = True + continue + + if not in_history: + continue + + if line.strip() == "": + break + + lines.append(line) + + self.results = DumpsysBatteryHistory.parse_battery_history("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/dbinfo.py b/mvt/android/modules/bugreport/dbinfo.py new file mode 100644 index 0000000..c125b52 --- /dev/null +++ b/mvt/android/modules/bugreport/dbinfo.py @@ -0,0 +1,63 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_dbinfo import DumpsysDBInfo + +from .base import BugReportModule + +log = logging.getLogger(__name__) + + +class DBInfo(BugReportModule): + """This module extracts records from battery daily updates.""" + + slug = "dbinfo" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def check_indicators(self): + if not self.indicators: + return + + for result in self.results: + path = result.get("path", "") + for part in path.split("/"): + ioc = self.indicators.check_app_id(part) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_dbinfo = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "DUMP OF SERVICE dbinfo:": + in_dbinfo = True + continue + + if not in_dbinfo: + continue + + if line.strip() == "------------------------------------------------------------------------------": + break + + lines.append(line) + + self.results = DumpsysDBInfo.parse_dbinfo("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/receivers.py b/mvt/android/modules/bugreport/receivers.py new file mode 100644 index 0000000..4954c9a --- /dev/null +++ b/mvt/android/modules/bugreport/receivers.py @@ -0,0 +1,84 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.android.modules.adb.dumpsys_receivers import DumpsysReceivers + +from .base import BugReportModule + +log = logging.getLogger(__name__) + +INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS" +INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED" +INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED" +INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE" +INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL" + + +class Receivers(BugReportModule): + """This module extracts details on receivers for risky activities.""" + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + serial=None, fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + self.results = results if results else {} + + def check_indicators(self): + if not self.indicators: + return + + for intent, receivers in self.results.items(): + for receiver in receivers: + if intent == INTENT_NEW_OUTGOING_SMS: + self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"", + receiver["receiver"]) + elif intent == INTENT_SMS_RECEIVED: + self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"", + receiver["receiver"]) + elif intent == INTENT_DATA_SMS_RECEIVED: + self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"", + receiver["receiver"]) + elif intent == INTENT_PHONE_STATE: + self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"", + receiver["receiver"]) + elif intent == INTENT_NEW_OUTGOING_CALL: + self.log.info("Found a receiver monitoring outgoing calls: \"%s\"", + receiver["receiver"]) + + ioc = self.indicators.check_app_id(receiver["package_name"]) + if ioc: + receiver["matched_indicator"] = ioc + self.detected.append({intent: receiver}) + continue + + def run(self): + dumpstate_files = self._get_files_by_pattern("dumpstate-*") + if not dumpstate_files: + return + + content = self._get_file_content(dumpstate_files[0]) + if not content: + return + + in_receivers = False + lines = [] + for line in content.decode().split("\n"): + if line.strip() == "DUMP OF SERVICE package:": + in_receivers = True + continue + + if not in_receivers: + continue + + if line.strip() == "------------------------------------------------------------------------------": + break + + lines.append(line) + + self.results = DumpsysReceivers.parse_receiver_resolver_table("\n".join(lines)) diff --git a/mvt/common/utils.py b/mvt/common/utils.py index 30eb1c4..202323a 100644 --- a/mvt/common/utils.py +++ b/mvt/common/utils.py @@ -73,7 +73,7 @@ def check_for_links(text): :returns: Search results. """ - return re.findall("(?Phttps?://[^\s]+)", text, re.IGNORECASE) + return re.findall(r"(?Phttps?://[^\s]+)", text, re.IGNORECASE) def get_sha256_from_file_path(file_path):