diff --git a/mvt/android/artifacts/dumpsys_accessibility.py b/mvt/android/artifacts/dumpsys_accessibility.py index 4a3d630..f1eacb1 100644 --- a/mvt/android/artifacts/dumpsys_accessibility.py +++ b/mvt/android/artifacts/dumpsys_accessibility.py @@ -18,7 +18,7 @@ class DumpsysAccessibilityArtifact(AndroidArtifact): self.detected.append(result) continue - def parse(self, content: str): + def parse(self, content: str) -> None: """ Parse the Dumpsys Accessibility section/ Adds results to self.results (List[Dict[str, str]]) diff --git a/mvt/android/artifacts/dumpsys_appops.py b/mvt/android/artifacts/dumpsys_appops.py index 05b00fd..c5542d1 100644 --- a/mvt/android/artifacts/dumpsys_appops.py +++ b/mvt/android/artifacts/dumpsys_appops.py @@ -55,7 +55,7 @@ class DumpsysAppopsArtifact(AndroidArtifact): result["package_name"], ) - def parse(self, output: str) -> List[Dict[str, Any]]: + def parse(self, output: str) -> None: self.results: List[Dict[str, Any]] = [] perm = {} package = {} diff --git a/mvt/android/artifacts/dumpsys_receivers.py b/mvt/android/artifacts/dumpsys_receivers.py new file mode 100644 index 0000000..331d1db --- /dev/null +++ b/mvt/android/artifacts/dumpsys_receivers.py @@ -0,0 +1,116 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 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 .artifact import AndroidArtifact + +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 DumpsysReceiversArtifact(AndroidArtifact): + """ + Parser for dumpsys receivers in the package section + """ + + def check_indicators(self) -> None: + 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"], + ) + + if not self.indicators: + continue + + ioc = self.indicators.check_app_id(receiver["package_name"]) + if ioc: + receiver["matched_indicator"] = ioc + self.detected.append({intent: receiver}) + continue + + def parse(self, output: str) -> None: + self.results = {} + + in_receiver_resolver_table = False + in_non_data_actions = False + intent = None + for line in output.splitlines(): + if line.startswith("Receiver Resolver Table:"): + in_receiver_resolver_table = True + continue + + if not in_receiver_resolver_table: + continue + + if line.startswith(" Non-Data Actions:"): + in_non_data_actions = True + continue + + if not in_non_data_actions: + continue + + # If we hit an empty line, the Non-Data Actions section should be + # finished. + if line.strip() == "": + break + + # We detect the action name. + if ( + line.startswith(" " * 6) + and not line.startswith(" " * 8) + and ":" in line + ): + intent = line.strip().replace(":", "") + self.results[intent] = [] + continue + + # If we are not in an intent block yet, skip. + if not intent: + continue + + # If we are in a block but the line does not start with 8 spaces + # it means the block ended a new one started, so we reset and + # continue. + if not line.startswith(" " * 8): + intent = None + continue + + # If we got this far, we are processing receivers for the + # activities we are interested in. + receiver = line.strip().split(" ")[1] + package_name = receiver.split("/")[0] + + self.results[intent].append( + { + "package_name": package_name, + "receiver": receiver, + } + ) diff --git a/mvt/android/modules/adb/dumpsys_receivers.py b/mvt/android/modules/adb/dumpsys_receivers.py index 4529b38..3eaf802 100644 --- a/mvt/android/modules/adb/dumpsys_receivers.py +++ b/mvt/android/modules/adb/dumpsys_receivers.py @@ -6,18 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_receiver_resolver_table +from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact from .base import AndroidExtraction -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 DumpsysReceivers(AndroidExtraction): +class DumpsysReceivers(DumpsysReceiversArtifact, AndroidExtraction): """This module extracts details on receivers for risky activities.""" def __init__( @@ -40,49 +34,11 @@ class DumpsysReceivers(AndroidExtraction): 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}) - continue - def run(self) -> None: self._adb_connect() output = self._adb_command("dumpsys package") - self.results = parse_dumpsys_receiver_resolver_table(output) + self.parse(output) self._adb_disconnect() + self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/mvt/android/modules/androidqf/dumpsys_receivers.py b/mvt/android/modules/androidqf/dumpsys_receivers.py index d60c6d7..c01ecd3 100644 --- a/mvt/android/modules/androidqf/dumpsys_receivers.py +++ b/mvt/android/modules/androidqf/dumpsys_receivers.py @@ -6,19 +6,12 @@ import logging from typing import Any, Dict, List, Optional, Union -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 mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact from .base import AndroidQFModule -class DumpsysReceivers(AndroidQFModule): +class DumpsysReceivers(DumpsysReceiversArtifact, AndroidQFModule): """This module analyse dumpsys receivers""" def __init__( @@ -41,67 +34,16 @@ class DumpsysReceivers(AndroidQFModule): 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 = [] data = self._get_file_content(dumpsys_file[0]) - for line in data.decode("utf-8").split("\n"): - if line.strip() == "DUMP OF SERVICE package:": - in_receivers = True - continue - if not in_receivers: - continue + dumpsys_section = self.extract_dumpsys_section( + data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:" + ) - 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.parse(dumpsys_section) self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/mvt/android/modules/bugreport/receivers.py b/mvt/android/modules/bugreport/receivers.py index 79e80e6..73d099b 100644 --- a/mvt/android/modules/bugreport/receivers.py +++ b/mvt/android/modules/bugreport/receivers.py @@ -6,18 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_receiver_resolver_table +from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact from .base import BugReportModule -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): +class Receivers(DumpsysReceiversArtifact, BugReportModule): """This module extracts details on receivers for risky activities.""" def __init__( @@ -40,45 +34,6 @@ class Receivers(BugReportModule): 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}) - continue - def run(self) -> None: content = self._get_dumpstate_file() if not content: @@ -88,23 +43,9 @@ class Receivers(BugReportModule): ) return - in_receivers = False - lines = [] - for line in content.decode(errors="ignore").splitlines(): - 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) - - self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines)) + dumpsys_section = self.extract_dumpsys_section( + content.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:" + ) + self.parse(dumpsys_section) self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/mvt/android/parsers/__init__.py b/mvt/android/parsers/__init__.py index 5271cba..9968a57 100644 --- a/mvt/android/parsers/__init__.py +++ b/mvt/android/parsers/__init__.py @@ -2,5 +2,3 @@ # Copyright (c) 2021-2023 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 import parse_dumpsys_receiver_resolver_table diff --git a/mvt/android/parsers/dumpsys.py b/mvt/android/parsers/dumpsys.py index 422b3c8..602307f 100644 --- a/mvt/android/parsers/dumpsys.py +++ b/mvt/android/parsers/dumpsys.py @@ -7,64 +7,6 @@ import re from typing import Any, Dict, List -def parse_dumpsys_receiver_resolver_table(output: str) -> Dict[str, Any]: - results = {} - - in_receiver_resolver_table = False - in_non_data_actions = False - intent = None - for line in output.splitlines(): - if line.startswith("Receiver Resolver Table:"): - in_receiver_resolver_table = True - continue - - if not in_receiver_resolver_table: - continue - - if line.startswith(" Non-Data Actions:"): - in_non_data_actions = True - continue - - if not in_non_data_actions: - continue - - # If we hit an empty line, the Non-Data Actions section should be - # finished. - if line.strip() == "": - break - - # We detect the action name. - if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line: - intent = line.strip().replace(":", "") - results[intent] = [] - continue - - # If we are not in an intent block yet, skip. - if not intent: - continue - - # If we are in a block but the line does not start with 8 spaces - # it means the block ended a new one started, so we reset and - # continue. - if not line.startswith(" " * 8): - intent = None - continue - - # If we got this far, we are processing receivers for the - # activities we are interested in. - receiver = line.strip().split(" ")[1] - package_name = receiver.split("/")[0] - - results[intent].append( - { - "package_name": package_name, - "receiver": receiver, - } - ) - - return results - - def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]: """ Parse one entry of a dumpsys package information diff --git a/tests/android/test_artifact_dumpsys_receivers.py b/tests/android/test_artifact_dumpsys_receivers.py new file mode 100644 index 0000000..5dd966b --- /dev/null +++ b/tests/android/test_artifact_dumpsys_receivers.py @@ -0,0 +1,47 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 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 mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysReceiversArtifact: + def test_parsing(self): + dr = DumpsysReceiversArtifact() + file = get_artifact("android_data/dumpsys_packages.txt") + with open(file) as f: + data = f.read() + + assert len(dr.results) == 0 + dr.parse(data) + assert len(dr.results) == 4 + assert ( + list(dr.results.keys())[0] + == "com.android.storagemanager.automatic.SHOW_NOTIFICATION" + ) + assert ( + dr.results["com.android.storagemanager.automatic.SHOW_NOTIFICATION"][0][ + "package_name" + ] + == "com.android.storagemanager" + ) + + def test_ioc_check(self, indicator_file): + dr = DumpsysReceiversArtifact() + file = get_artifact("android_data/dumpsys_packages.txt") + with open(file) as f: + data = f.read() + dr.parse(data) + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["app_ids"].append("com.android.storagemanager") + dr.indicators = ind + assert len(dr.detected) == 0 + dr.check_indicators() + assert len(dr.detected) == 1 diff --git a/tests/android/test_dumpsys_parser.py b/tests/android/test_dumpsys_parser.py deleted file mode 100644 index 42a5afe..0000000 --- a/tests/android/test_dumpsys_parser.py +++ /dev/null @@ -1,25 +0,0 @@ -# Mobile Verification Toolkit (MVT) -# Copyright (c) 2021-2023 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 mvt.android.parsers.dumpsys import parse_dumpsys_packages - -from ..utils import get_artifact - - -class TestDumpsysParsing: - def test_packages_parsing(self): - file = get_artifact("android_data/dumpsys_packages.txt") - with open(file) as f: - data = f.read() - - res = parse_dumpsys_packages(data) - - assert len(res) == 2 - assert res[0]["package_name"] == "com.samsung.android.provider.filterprovider" - assert res[1]["package_name"] == "com.sec.android.app.DataCreate" - assert len(res[0]["permissions"]) == 4 - assert len(res[0]["requested_permissions"]) == 0 - assert len(res[1]["permissions"]) == 34 - assert len(res[1]["requested_permissions"]) == 11 diff --git a/tests/artifacts/android_data/dumpsys_packages.txt b/tests/artifacts/android_data/dumpsys_packages.txt index e80bf8c..e0f9b9d 100644 --- a/tests/artifacts/android_data/dumpsys_packages.txt +++ b/tests/artifacts/android_data/dumpsys_packages.txt @@ -150,3 +150,25 @@ Packages: + +Receiver Resolver Table: + Full MIME Types: + application/gmail-ls: + 231cb24 com.google.android.gm/.GmailReceiver + 3441042 com.google.android.gm/.widget.GoogleMailWidgetProvider (3 filters) + cf0a68d com.google.android.gm/.widget.GmailWidgetProvider (3 filters) + vnd.android.cursor.dir/voicemails: + 39596b6 com.samsung.android.app.telephonyui/com.android.voicemail.impl.sync.VoicemailProviderChangeReceiver + application/*: + 8e50848 com.android.vending/com.google.android.finsky.recoverymode.download.impl.RecoveryModeDownloadBroadcastReceiver + + Non-Data Actions: + com.android.storagemanager.automatic.SHOW_NOTIFICATION: + 417c3b3 com.android.storagemanager/.automatic.NotificationController + android.provider.action.EXTERNAL_PROVIDER_CHANGE: + b310d70 com.samsung.android.messaging/.service.syncservice.ExtProviderChangeReceiver + com.google.android.projection.gearhead.RESET_USB_FUNCTION: + da90126 com.google.android.projection.gearhead/com.google.android.apps.auto.components.connectivity.reset.ConnectionResetReceiver + com.samsung.cmh.action.CMH_SYNC: + d5c0372 com.samsung.cmh/.core.SystemBroadcastReceiver$CMHBroadcastReceiver +