diff --git a/src/mvt/android/artifacts/dumpsys_platform_compat.py b/src/mvt/android/artifacts/dumpsys_platform_compat.py new file mode 100644 index 000000000..e1037f01c --- /dev/null +++ b/src/mvt/android/artifacts/dumpsys_platform_compat.py @@ -0,0 +1,42 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT 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 .artifact import AndroidArtifact + + +class DumpsysPlatformCompatArtifact(AndroidArtifact): + """ + Parser for uninstalled apps listed in platform_compat section. + """ + + 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) + continue + + def parse(self, data: str) -> None: + for line in data.splitlines(): + if not line.startswith("ChangeId(168419799; name=DOWNSCALED;"): + continue + + if line.strip() == "": + break + + # Look for rawOverrides field + if "rawOverrides={" in line: + # Extract the content inside the braces for rawOverrides + overrides_field = line.split("rawOverrides={", 1)[1].split("};", 1)[0] + + for entry in overrides_field.split(", "): + # Extract app name + uninstall_app = entry.split("=")[0].strip() + + self.results.append({"package_name": uninstall_app}) diff --git a/src/mvt/android/modules/androidqf/__init__.py b/src/mvt/android/modules/androidqf/__init__.py index c1c7548ce..cdb0af8be 100644 --- a/src/mvt/android/modules/androidqf/__init__.py +++ b/src/mvt/android/modules/androidqf/__init__.py @@ -14,6 +14,7 @@ from .dumpsys_adb import DumpsysADBState from .getprop import Getprop from .packages import Packages +from .dumpsys_platform_compat import DumpsysPlatformCompat from .processes import Processes from .settings import Settings from .sms import SMS @@ -29,6 +30,7 @@ DumpsysBatteryHistory, DumpsysADBState, Packages, + DumpsysPlatformCompat, Processes, Getprop, Settings, diff --git a/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py b/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py new file mode 100644 index 000000000..869c476f5 --- /dev/null +++ b/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py @@ -0,0 +1,44 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT 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 typing import Optional + +from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact + +from .base import AndroidQFModule + + +class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, AndroidQFModule): + """This module extracts details on uninstalled apps.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + module_options: Optional[dict] = None, + 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, + module_options=module_options, + log=log, + results=results, + ) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") + content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:") + self.parse(content) + + self.log.info("Found %d uninstalled apps", len(self.results)) diff --git a/src/mvt/android/modules/bugreport/__init__.py b/src/mvt/android/modules/bugreport/__init__.py index 119c958d0..73dd852b0 100644 --- a/src/mvt/android/modules/bugreport/__init__.py +++ b/src/mvt/android/modules/bugreport/__init__.py @@ -11,6 +11,7 @@ from .dbinfo import DBInfo from .getprop import Getprop from .packages import Packages +from .platform_compat import PlatformCompat from .receivers import Receivers from .adb_state import DumpsysADBState @@ -23,6 +24,7 @@ DBInfo, Getprop, Packages, + PlatformCompat, Receivers, DumpsysADBState, ] diff --git a/src/mvt/android/modules/bugreport/platform_compat.py b/src/mvt/android/modules/bugreport/platform_compat.py new file mode 100644 index 000000000..fadac9216 --- /dev/null +++ b/src/mvt/android/modules/bugreport/platform_compat.py @@ -0,0 +1,48 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT 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 typing import Optional + +from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact + +from mvt.android.modules.bugreport.base import BugReportModule + + +class PlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule): + """This module extracts details on uninstalled apps.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + module_options: Optional[dict] = None, + 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, + module_options=module_options, + log=log, + results=results, + ) + + def run(self) -> None: + data = self._get_dumpstate_file() + if not data: + self.log.error( + "Unable to find dumpstate file. " + "Did you provide a valid bug report archive?" + ) + return + + data = data.decode("utf-8", errors="replace") + content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:") + self.parse(content) + + self.log.info("Found %d uninstalled apps", len(self.results)) diff --git a/tests/android/test_artifact_dumpsys_platform_compat.py b/tests/android/test_artifact_dumpsys_platform_compat.py new file mode 100644 index 000000000..e2321a4a3 --- /dev/null +++ b/tests/android/test_artifact_dumpsys_platform_compat.py @@ -0,0 +1,40 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT 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.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysPlatformCompatArtifact: + def test_parsing(self): + dbi = DumpsysPlatformCompatArtifact() + file = get_artifact("android_data/dumpsys_platform_compat.txt") + with open(file) as f: + data = f.read() + + assert len(dbi.results) == 0 + dbi.parse(data) + assert len(dbi.results) == 2 + assert dbi.results[0]["package_name"] == "org.torproject.torbrowser" + assert dbi.results[1]["package_name"] == "org.article19.circulo.next" + + def test_ioc_check(self, indicator_file): + dbi = DumpsysPlatformCompatArtifact() + file = get_artifact("android_data/dumpsys_platform_compat.txt") + with open(file) as f: + data = f.read() + dbi.parse(data) + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["app_ids"].append("org.torproject.torbrowser") + ind.ioc_collections[0]["app_ids"].append("org.article19.circulo.next") + dbi.indicators = ind + assert len(dbi.detected) == 0 + dbi.check_indicators() + assert len(dbi.detected) == 2 diff --git a/tests/android_androidqf/test_dumpsys_platform_compat.py b/tests/android_androidqf/test_dumpsys_platform_compat.py new file mode 100644 index 000000000..812343291 --- /dev/null +++ b/tests/android_androidqf/test_dumpsys_platform_compat.py @@ -0,0 +1,23 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT 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 pathlib import Path + +from mvt.android.modules.androidqf.dumpsys_platform_compat import DumpsysPlatformCompat +from mvt.common.module import run_module + +from ..utils import get_android_androidqf, list_files + + +class TestDumpsysPlatformCompatModule: + def test_parsing(self): + data_path = get_android_androidqf() + m = DumpsysPlatformCompat(target_path=data_path) + files = list_files(data_path) + parent_path = Path(data_path).absolute().parent.as_posix() + m.from_folder(parent_path, files) + run_module(m) + assert len(m.results) == 2 + assert len(m.detected) == 0 diff --git a/tests/artifacts/android_data/bugreport/dumpstate.txt b/tests/artifacts/android_data/bugreport/dumpstate.txt index b30d30b50..c888ed5cc 100644 --- a/tests/artifacts/android_data/bugreport/dumpstate.txt +++ b/tests/artifacts/android_data/bugreport/dumpstate.txt @@ -246,6 +246,23 @@ Packages: com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivity com.instagram.share.handleractivity.ClipsShareHandlerActivity com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivityInterop - +--------- 0.053s was the duration of dumpsys appops, ending at: 2022-03-29 23:14:27 +------------------------------------------------------------------------------- +DUMP OF SERVICE platform_compat: +ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable) +ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable) +ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30) +ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30) +ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled) +ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35) +ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33) +ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35) +ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33) +ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31) +ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable) +ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled) +ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34) +ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled) +ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable) diff --git a/tests/artifacts/android_data/dumpsys_platform_compat.txt b/tests/artifacts/android_data/dumpsys_platform_compat.txt new file mode 100644 index 000000000..13cc17c2d --- /dev/null +++ b/tests/artifacts/android_data/dumpsys_platform_compat.txt @@ -0,0 +1,16 @@ +DUMP OF SERVICE platform_compat: +ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable) +ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable) +ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30) +ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30) +ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled) +ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35) +ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33) +ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35) +ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33) +ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31) +ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable) +ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled) +ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34) +ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled) +ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable) \ No newline at end of file diff --git a/tests/artifacts/androidqf/dumpsys.txt b/tests/artifacts/androidqf/dumpsys.txt index 2f21ffa95..0deb4cfee 100644 --- a/tests/artifacts/androidqf/dumpsys.txt +++ b/tests/artifacts/androidqf/dumpsys.txt @@ -379,4 +379,22 @@ Daily stats: Update com.google.android.projection.gearhead vers=99632623 Update com.google.android.projection.gearhead vers=99632623 Update com.google.android.projection.gearhead vers=99632623 +--------- 0.053s was the duration of dumpsys batterystats, ending at: 2024-03-21 11:07:22 +------------------------------------------------------------------------------- +DUMP OF SERVICE platform_compat: +ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable) +ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable) +ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30) +ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30) +ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled) +ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35) +ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33) +ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35) +ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33) +ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31) +ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable) +ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled) +ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34) +ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled) +ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable) diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index c97b9c0da..d1058e59d 100644 --- a/tests/common/test_utils.py +++ b/tests/common/test_utils.py @@ -75,7 +75,7 @@ def test_hash_from_folder(self): # This needs to be updated when we add or edit files in AndroidQF folder assert ( hashes[1]["sha256"] - == "1bd255f656a7f9d5647a730f0f0cc47053115576f11532d41bf28c16635b193d" + == "9fb6396b64cfff30e2a459a64496d3c1386926d09edd68be2d878de45fa7b3a9" )