From a863209abbb2d56e4614d3edd452490e109428a3 Mon Sep 17 00:00:00 2001 From: Nex Date: Mon, 5 Sep 2022 12:12:36 +0200 Subject: [PATCH] Added check-androidqf command --- mvt/android/cli.py | 43 ++++++- mvt/android/cmd_check_androidqf.py | 32 ++++++ mvt/android/modules/adb/processes.py | 18 ++- mvt/android/modules/androidqf/__init__.py | 15 +++ mvt/android/modules/androidqf/base.py | 38 ++++++ .../androidqf/dumpsys_accessibility.py | 68 +++++++++++ .../modules/androidqf/dumpsys_activities.py | 66 +++++++++++ .../modules/androidqf/dumpsys_appops.py | 83 ++++++++++++++ .../modules/androidqf/dumpsys_packages.py | 108 ++++++++++++++++++ .../modules/androidqf/dumpsys_receivers.py | 86 ++++++++++++++ mvt/android/modules/androidqf/getprop.py | 66 +++++++++++ mvt/android/modules/androidqf/processes.py | 92 +++++++++++++++ mvt/android/modules/androidqf/settings.py | 58 ++++++++++ mvt/common/command.py | 4 + mvt/ios/cli.py | 8 +- 15 files changed, 773 insertions(+), 12 deletions(-) create mode 100644 mvt/android/cmd_check_androidqf.py create mode 100644 mvt/android/modules/androidqf/__init__.py create mode 100644 mvt/android/modules/androidqf/base.py create mode 100644 mvt/android/modules/androidqf/dumpsys_accessibility.py create mode 100644 mvt/android/modules/androidqf/dumpsys_activities.py create mode 100644 mvt/android/modules/androidqf/dumpsys_appops.py create mode 100644 mvt/android/modules/androidqf/dumpsys_packages.py create mode 100644 mvt/android/modules/androidqf/dumpsys_receivers.py create mode 100644 mvt/android/modules/androidqf/getprop.py create mode 100644 mvt/android/modules/androidqf/processes.py create mode 100644 mvt/android/modules/androidqf/settings.py diff --git a/mvt/android/cli.py b/mvt/android/cli.py index edd1ba7..3d3adb9 100644 --- a/mvt/android/cli.py +++ b/mvt/android/cli.py @@ -16,6 +16,7 @@ from mvt.common.logo import logo from mvt.common.updates import IndicatorsUpdates from .cmd_check_adb import CmdAndroidCheckADB +from .cmd_check_androidqf import CmdAndroidCheckAndroidQF from .cmd_check_backup import CmdAndroidCheckBackup from .cmd_check_bugreport import CmdAndroidCheckBugreport from .cmd_download_apks import DownloadAPKs @@ -121,9 +122,9 @@ def check_adb(ctx, serial, iocs, output, fast, list_modules, module): cmd.run() - if len(cmd.timeline_detected) > 0: + if cmd.detected_count > 0: log.warning("The analysis of the Android device produced %d detections!", - len(cmd.timeline_detected)) + cmd.detected_count) #============================================================================== @@ -151,9 +152,9 @@ def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path): cmd.run() - if len(cmd.timeline_detected) > 0: + if cmd.detected_count > 0: log.warning("The analysis of the Android bug report produced %d detections!", - len(cmd.timeline_detected)) + cmd.detected_count) #============================================================================== @@ -179,9 +180,39 @@ def check_backup(ctx, iocs, output, list_modules, backup_path): cmd.run() - if len(cmd.timeline_detected) > 0: + if cmd.detected_count > 0: log.warning("The analysis of the Android backup produced %d detections!", - len(cmd.timeline_detected)) + cmd.detected_count) + + +#============================================================================== +# Command: check-androidqf +#============================================================================== +@cli.command("check-androidqf", help="Check data collected with AndroidQF") +@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("ANDROIDQF_PATH", type=click.Path(exists=True)) +@click.pass_context +def check_androidqf(ctx, iocs, output, list_modules, module, androidqf_path): + cmd = CmdAndroidCheckAndroidQF(target_path=androidqf_path, + results_path=output, ioc_files=iocs, + module_name=module) + + if list_modules: + cmd.list_modules() + return + + log.info("Checking AndroidQF acquisition at path: %s", androidqf_path) + + cmd.run() + + if cmd.detected_count > 0: + log.warning("The analysis of the AndroidQF acquisition produced %d detections!", + cmd.detected_count) #============================================================================== diff --git a/mvt/android/cmd_check_androidqf.py b/mvt/android/cmd_check_androidqf.py new file mode 100644 index 0000000..633f973 --- /dev/null +++ b/mvt/android/cmd_check_androidqf.py @@ -0,0 +1,32 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from mvt.common.command import Command + +from .modules.androidqf import ANDROIDQF_MODULES + +log = logging.getLogger(__name__) + + +class CmdAndroidCheckAndroidQF(Command): + + def __init__( + self, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + ioc_files: Optional[list] = None, + module_name: Optional[str] = None, + serial: Optional[str] = None, + fast_mode: Optional[bool] = False, + ) -> None: + super().__init__(target_path=target_path, results_path=results_path, + ioc_files=ioc_files, module_name=module_name, + serial=serial, fast_mode=fast_mode, log=log) + + self.name = "check-androidqf" + self.modules = ANDROIDQF_MODULES diff --git a/mvt/android/modules/adb/processes.py b/mvt/android/modules/adb/processes.py index 0c9ffe7..ea92442 100644 --- a/mvt/android/modules/adb/processes.py +++ b/mvt/android/modules/adb/processes.py @@ -30,7 +30,21 @@ class Processes(AndroidExtraction): return for result in self.results: - ioc = self.indicators.check_app_id(result.get("name", "")) + proc_name = result.get("proc_name", "") + if not proc_name: + continue + + # Skipping this process because of false positives. + if result["proc_name"] == "gatekeeperd": + continue + + ioc = self.indicators.check_app_id(proc_name) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + ioc = self.indicators.check_process(proc_name) if ioc: result["matched_indicator"] = ioc self.detected.append(result) @@ -38,7 +52,7 @@ class Processes(AndroidExtraction): def run(self) -> None: self._adb_connect() - output = self._adb_command("ps -e") + output = self._adb_command("ps -A") for line in output.splitlines()[1:]: line = line.strip() diff --git a/mvt/android/modules/androidqf/__init__.py b/mvt/android/modules/androidqf/__init__.py new file mode 100644 index 0000000..bbbd341 --- /dev/null +++ b/mvt/android/modules/androidqf/__init__.py @@ -0,0 +1,15 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +from .dumpsys_accessibility import DumpsysAccessibility +from .dumpsys_activities import DumpsysActivities +from .dumpsys_appops import DumpsysAppops +from .dumpsys_receivers import DumpsysReceivers +from .getprop import Getprop +from .processes import Processes +from .settings import Settings + +ANDROIDQF_MODULES = [DumpsysActivities, DumpsysReceivers, DumpsysAccessibility, + DumpsysAppops, Processes, Getprop, Settings] diff --git a/mvt/android/modules/androidqf/base.py b/mvt/android/modules/androidqf/base.py new file mode 100644 index 0000000..6d685ab --- /dev/null +++ b/mvt/android/modules/androidqf/base.py @@ -0,0 +1,38 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import fnmatch +import logging +import os +from typing import Optional + +from mvt.common.module import MVTModule + + +class AndroidQFModule(MVTModule): + """This class provides a base for all Android Data analysis modules.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + self._path = target_path + self._files = [] + + for root, dirs, files in os.walk(target_path): + for name in files: + self._files.append(os.path.join(root, name)) + + def _get_files_by_pattern(self, pattern): + return fnmatch.filter(self._files, pattern) diff --git a/mvt/android/modules/androidqf/dumpsys_accessibility.py b/mvt/android/modules/androidqf/dumpsys_accessibility.py new file mode 100644 index 0000000..d78b777 --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_accessibility.py @@ -0,0 +1,68 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from mvt.android.parsers import parse_dumpsys_accessibility + +from .base import AndroidQFModule + + +class DumpsysAccessibility(AndroidQFModule): + """This module analyse dumpsys accessbility""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + def check_indicators(self) -> None: + 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) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + lines = [] + in_accessibility = False + with open(dumpsys_file[0]) as handle: + for line in handle: + if line.strip().startswith("DUMP OF SERVICE accessibility:"): + in_accessibility = True + continue + + if not in_accessibility: + continue + + if line.strip().startswith("-------------------------------------------------------------------------------"): # pylint: disable=line-too-long + break + + lines.append(line.rstrip()) + + self.results = parse_dumpsys_accessibility("\n".join(lines)) + + for result in self.results: + self.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/androidqf/dumpsys_activities.py b/mvt/android/modules/androidqf/dumpsys_activities.py new file mode 100644 index 0000000..76f30cf --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_activities.py @@ -0,0 +1,66 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from mvt.android.parsers import parse_dumpsys_activity_resolver_table + +from .base import AndroidQFModule + + +class DumpsysActivities(AndroidQFModule): + """This module extracts details on receivers for risky activities.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + self.results = results if results else {} + + def check_indicators(self) -> None: + 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}) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + lines = [] + in_package = False + with open(dumpsys_file[0]) as handle: + for line in handle: + if line.strip() == "DUMP OF SERVICE package:": + in_package = True + continue + + if not in_package: + continue + + if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long + break + + lines.append(line.rstrip()) + + self.results = parse_dumpsys_activity_resolver_table("\n".join(lines)) + + self.log.info("Extracted activities for %d intents", len(self.results)) diff --git a/mvt/android/modules/androidqf/dumpsys_appops.py b/mvt/android/modules/androidqf/dumpsys_appops.py new file mode 100644 index 0000000..9177034 --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_appops.py @@ -0,0 +1,83 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional, Union + +from mvt.android.parsers import parse_dumpsys_appops + +from .base import AndroidQFModule + + +class DumpsysAppops(AndroidQFModule): + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + def serialize(self, record: dict) -> Union[dict, list]: + records = [] + for perm in record["permissions"]: + if "entries" not in perm: + continue + + for entry in perm["entries"]: + if "timestamp" in entry: + records.append({ + "timestamp": entry["timestamp"], + "module": self.__class__.__name__, + "event": entry["access"], + "data": f"{record['package_name']} access to " + f"{perm['name']} : {entry['access']}", + }) + + return records + + def check_indicators(self) -> None: + for result in self.results: + if self.indicators: + ioc = self.indicators.check_app_id(result.get("package_name")) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + for perm in result["permissions"]: + if (perm["name"] == "REQUEST_INSTALL_PACKAGES" + and perm["access"] == "allow"): + self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission", + result["package_name"]) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + lines = [] + in_package = False + with open(dumpsys_file[0]) as handle: + for line in handle: + if line.startswith("DUMP OF SERVICE appops:"): + in_package = True + continue + + if in_package: + if line.startswith("-------------------------------------------------------------------------------"): # pylint: disable=line-too-long + break + + lines.append(line.rstrip()) + + self.results = parse_dumpsys_appops("\n".join(lines)) + self.log.info("Identified %d applications in AppOps Manager", + len(self.results)) diff --git a/mvt/android/modules/androidqf/dumpsys_packages.py b/mvt/android/modules/androidqf/dumpsys_packages.py new file mode 100644 index 0000000..76d909d --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_packages.py @@ -0,0 +1,108 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 datetime import datetime +from typing import Optional, Union + +from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS, + DANGEROUS_PERMISSIONS_THRESHOLD, + ROOT_PACKAGES) +from mvt.android.parsers.dumpsys import parse_dumpsys_packages +from mvt.common.utils import convert_datetime_to_iso + +from .base import AndroidQFModule + + +class DumpsysPackages(AndroidQFModule): + """This module analyse dumpsys packages""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + def serialize(self, record: dict) -> Union[dict, list]: + entries = [] + for entry in ["timestamp", "first_install_time", "last_update_time"]: + if entry in record: + entries.append({ + "timestamp": record[entry], + "module": self.__class__.__name__, + "event": entry, + "data": f"Package {record['package_name']} " + f"({record['uid']})", + }) + + return entries + + def check_indicators(self) -> None: + for result in self.results: + if result["package_name"] in ROOT_PACKAGES: + self.log.warning("Found an installed package related to " + "rooting/jailbreaking: \"%s\"", + result["package_name"]) + self.detected.append(result) + continue + + if not self.indicators: + continue + + ioc = self.indicators.check_app_id(result.get("package_name", "")) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if len(dumpsys_file) != 1: + self.log.info("Dumpsys file not found") + return + + with open(dumpsys_file[0]) as handle: + data = handle.read().split("\n") + + package = [] + in_service = False + in_package_list = False + for line in data: + if line.strip().startswith("DUMP OF SERVICE package:"): + in_service = True + continue + + if in_service and line.startswith("Packages:"): + in_package_list = True + continue + + if not in_service or not in_package_list: + continue + + if line.strip() == "": + break + + package.append(line) + + self.results = parse_dumpsys_packages("\n".join(package)) + + for result in self.results: + dangerous_permissions_count = 0 + for perm in result["permissions"]: + if perm["name"] in DANGEROUS_PERMISSIONS: + dangerous_permissions_count += 1 + + if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD: + self.log.info("Found package \"%s\" requested %d potentially dangerous permissions", + result["package_name"], + dangerous_permissions_count) + + self.log.info("Extracted details on %d packages", len(self.results)) diff --git a/mvt/android/modules/androidqf/dumpsys_receivers.py b/mvt/android/modules/androidqf/dumpsys_receivers.py new file mode 100644 index 0000000..009f979 --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_receivers.py @@ -0,0 +1,86 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from mvt.android.modules.adb.dumpsys_receivers import ( + INTENT_DATA_SMS_RECEIVED, INTENT_NEW_OUTGOING_CALL, + INTENT_NEW_OUTGOING_SMS, INTENT_PHONE_STATE, INTENT_SMS_RECEIVED) +from mvt.android.parsers import parse_dumpsys_receiver_resolver_table + +from .base import AndroidQFModule + + +class DumpsysReceivers(AndroidQFModule): + """This module analyse dumpsys receivers""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + self.results = results if results else {} + + def check_indicators(self) -> None: + 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}) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + in_receivers = False + lines = [] + with open(dumpsys_file[0]) as handle: + for line in handle: + if line.strip() == "DUMP OF SERVICE package:": + in_receivers = True + continue + + if not in_receivers: + continue + + if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long + break + + lines.append(line.rstrip()) + + self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines)) + + self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/mvt/android/modules/androidqf/getprop.py b/mvt/android/modules/androidqf/getprop.py new file mode 100644 index 0000000..9a500b7 --- /dev/null +++ b/mvt/android/modules/androidqf/getprop.py @@ -0,0 +1,66 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 datetime import datetime, timedelta +from typing import Optional + +from mvt.android.parsers import getprop + +from .base import AndroidQFModule + +INTERESTING_PROPERTIES = [ + "gsm.sim.operator.alpha", + "gsm.sim.operator.iso-country", + "persist.sys.timezone", + "ro.boot.serialno", + "ro.build.version.sdk", + "ro.build.version.security_patch", + "ro.product.cpu.abi", + "ro.product.locale", + "ro.product.vendor.manufacturer", + "ro.product.vendor.model", + "ro.product.vendor.name" +] + + +class Getprop(AndroidQFModule): + """This module extracts data from get properties.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + self.results = {} + + def run(self) -> None: + getprop_files = self._get_files_by_pattern("*/getprop.txt") + if not getprop_files: + self.log.info("getprop.txt file not found") + return + + with open(getprop_files[0]) as f: + data = f.read() + + self.results = getprop.parse_getprop(data) + for entry in self.results: + if entry in INTERESTING_PROPERTIES: + self.log.info("%s: %s", entry, self.results[entry]) + if entry == "ro.build.version.security_patch": + last_patch = datetime.strptime(self.results[entry], "%Y-%m-%d") + if (datetime.now() - last_patch) > timedelta(days=6*31): + self.log.warning("This phone has not received security " + "updates for more than six months " + "(last update: %s)", self.results[entry]) + + self.log.info("Extracted a total of %d properties", len(self.results)) diff --git a/mvt/android/modules/androidqf/processes.py b/mvt/android/modules/androidqf/processes.py new file mode 100644 index 0000000..14b4d80 --- /dev/null +++ b/mvt/android/modules/androidqf/processes.py @@ -0,0 +1,92 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from .base import AndroidQFModule + + +class Processes(AndroidQFModule): + """This module analyse running processes""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + + def check_indicators(self) -> None: + if not self.indicators: + return + + for result in self.results: + proc_name = result.get("proc_name", "") + if not proc_name: + continue + + # Skipping this process because of false positives. + if result["proc_name"] == "gatekeeperd": + continue + + ioc = self.indicators.check_app_id(proc_name) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + ioc = self.indicators.check_process(proc_name) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + + def _parse_ps(self, data): + for line in data.split("\n")[1:]: + proc = line.split() + + # Sometimes WCHAN is empty. + if len(proc) == 8: + proc = proc[:5] + [''] + proc[5:] + + # Sometimes there is the security label. + if proc[0].startswith("u:r"): + label = proc[0] + proc = proc[1:] + else: + label = "" + + # Sometimes there is no WCHAN. + if len(proc) < 9: + proc = proc[:5] + [""] + proc[5:] + + self.results.append({ + "user": proc[0], + "pid": int(proc[1]), + "ppid": int(proc[2]), + "virtual_memory_size": int(proc[3]), + "resident_set_size": int(proc[4]), + "wchan": proc[5], + "aprocress": proc[6], + "stat": proc[7], + "proc_name": proc[8].strip("[]"), + "label": label, + }) + + def run(self) -> None: + ps_files = self._get_files_by_pattern("*/ps.txt") + if not ps_files: + return + + with open(ps_files[0]) as handle: + self._parse_ps(handle.read()) + + self.log.info("Identified %d running processes", len(self.results)) diff --git a/mvt/android/modules/androidqf/settings.py b/mvt/android/modules/androidqf/settings.py new file mode 100644 index 0000000..bcb1e11 --- /dev/null +++ b/mvt/android/modules/androidqf/settings.py @@ -0,0 +1,58 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 Claudio Guarnieri. +# 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 typing import Optional + +from mvt.android.modules.adb.settings import ANDROID_DANGEROUS_SETTINGS + +from .base import AndroidQFModule + + +class Settings(AndroidQFModule): + """This module analyse setting files""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + self.results = {} + + def run(self) -> None: + for setting_file in self._get_files_by_pattern("*/settings_*.txt"): + namespace = setting_file[setting_file.rfind("_")+1:-4] + + self.results[namespace] = {} + + with open(setting_file) as handle: + for line in handle: + line = line.strip() + try: + key, value = line.split("=", 1) + except ValueError: + continue + + try: + self.results[namespace][key] = value + except IndexError: + continue + + for danger in ANDROID_DANGEROUS_SETTINGS: + if (danger["key"] == key + and danger["safe_value"] != value): + self.log.warning("Found suspicious setting \"%s = %s\" (%s)", + key, value, danger["description"]) + break + + self.log.info("Identified %d settings", + sum([len(val) for val in self.results.values()])) diff --git a/mvt/common/command.py b/mvt/common/command.py index 1494b5d..a645cd7 100644 --- a/mvt/common/command.py +++ b/mvt/common/command.py @@ -47,6 +47,8 @@ class Command: # We can use this to reference e.g. self.executed[0].results. self.executed = [] + self.detected_count = 0 + self.timeline = [] self.timeline_detected = [] @@ -196,6 +198,8 @@ class Command: self.executed.append(m) + self.detected_count += len(m.detected) + self.timeline.extend(m.timeline) self.timeline_detected.extend(m.timeline_detected) diff --git a/mvt/ios/cli.py b/mvt/ios/cli.py index 90283f8..12c1b8f 100644 --- a/mvt/ios/cli.py +++ b/mvt/ios/cli.py @@ -162,9 +162,9 @@ def check_backup(ctx, iocs, output, fast, list_modules, module, backup_path): cmd.run() - if len(cmd.timeline_detected) > 0: + if cmd.detected_count > 0: log.warning("The analysis of the backup produced %d detections!", - len(cmd.timeline_detected)) + cmd.detected_count) #============================================================================== @@ -192,9 +192,9 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, dump_path): cmd.run() - if len(cmd.timeline_detected) > 0: + if cmd.detected_count > 0: log.warning("The analysis of the iOS filesystem produced %d detections!", - len(cmd.timeline_detected)) + cmd.detected_count) #==============================================================================