From 008844b440e4c7a31dd569703ab6d73de01c4e74 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Sat, 22 Aug 2026 14:16:23 +0200 Subject: [PATCH] Parse accessibility service states per user --- .../artifacts/dumpsys_accessibility.py | 101 +++++++++++------- .../bugreport/dumpsys_accessibility.py | 4 +- .../test_artifact_dumpsys_accessibility.py | 22 +++- 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/src/mvt/android/artifacts/dumpsys_accessibility.py b/src/mvt/android/artifacts/dumpsys_accessibility.py index 3d8fa6c..da46043 100644 --- a/src/mvt/android/artifacts/dumpsys_accessibility.py +++ b/src/mvt/android/artifacts/dumpsys_accessibility.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ import re +from typing import Any from .artifact import AndroidArtifact @@ -20,10 +21,10 @@ class DumpsysAccessibilityArtifact(AndroidArtifact): continue self.alertstore.medium( - f'Found accessibility service: "{result["service"]}"', + f'Found accessibility service: "{result["component"]}"', "", result, - ) + ) def parse(self, content: str) -> None: """ @@ -33,41 +34,69 @@ class DumpsysAccessibilityArtifact(AndroidArtifact): :param content: content of the accessibility section (string) """ - # "Old" syntax - in_services = False + self.results: list[dict[str, Any]] = [] + services: dict[tuple[int | None, str], dict] = {} + user_id: int | None = None + state: str | None = None + for line in content.splitlines(): - if line.strip().startswith("installed services:"): - in_services = True - continue + user_match = re.search(r"attributes:\{id=(\d+)", line) + if user_match: + user_id = int(user_match.group(1)) - if not in_services: - continue - - if line.strip() == "}": - # At end of installed services - break - - service = line.split(":")[1].strip() - - self.results.append( - { - "package_name": service.split("/")[0], - "service": service, - } + stripped = line.strip() + state_match = re.match( + r"(?i)(installed|enabled|binding|bound|crashed) services\s*:\s*\{(.*)", + stripped, ) - - # "New" syntax - AOSP >= 14 (?) - # Looks like: - # Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}} - - for line in content.splitlines(): - if line.strip().startswith("Enabled services:"): - matches = re.finditer(r"{([^{]+?)}", line) - - for match in matches: - # Each match is in format: / - package_name, _, service = match.group(1).partition("/") - - self.results.append( - {"package_name": package_name, "service": service} + if state_match: + state = state_match.group(1).lower() + inline = state_match.group(2) + for component in re.findall( + r"\{?([\w.$-]+/[\w.$-]+)(?:\s+\(A11yTool\))?\}?", inline + ): + service = services.setdefault( + (user_id, component), self._new_service(component, user_id) ) + service[self._state_field(state)] = True + service["accessibility_tool"] = "(A11yTool)" in inline + continue + + if not state: + continue + if stripped == "}" or stripped.startswith("AccessibilityInputFilter"): + state = None + continue + component_match = re.search( + r"(?:\d+\s*:\s*)?([\w.$-]+/[\w.$-]+)(?:\s+\(A11yTool\))?", + stripped, + ) + if component_match: + component = component_match.group(1) + service = services.setdefault( + (user_id, component), self._new_service(component, user_id) + ) + service[self._state_field(state)] = True + service["accessibility_tool"] = "(A11yTool)" in stripped + + self.results.extend(services.values()) + + @staticmethod + def _state_field(state: str) -> str: + return {"binding": "binding", "bound": "bound"}.get(state, state) + + @staticmethod + def _new_service(component: str, user_id: int | None) -> dict: + package_name, service_name = component.split("/", 1) + return { + "user_id": user_id, + "component": component, + "package_name": package_name, + "service_name": service_name, + "installed": False, + "enabled": False, + "binding": False, + "bound": False, + "crashed": False, + "accessibility_tool": False, + } diff --git a/src/mvt/android/modules/bugreport/dumpsys_accessibility.py b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py index 72208c0..02d85a3 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_accessibility.py +++ b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py @@ -49,9 +49,7 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule): self.parse(content) for result in self.results: - self.log.info( - 'Found installed accessibility service "%s"', result.get("service") - ) + self.log.info('Found accessibility service "%s"', result.get("component")) self.log.info( "Identified a total of %d accessibility services", len(self.results) diff --git a/tests/android/test_artifact_dumpsys_accessibility.py b/tests/android/test_artifact_dumpsys_accessibility.py index 1f0b234..c727c57 100644 --- a/tests/android/test_artifact_dumpsys_accessibility.py +++ b/tests/android/test_artifact_dumpsys_accessibility.py @@ -23,7 +23,7 @@ class TestDumpsysAccessibilityArtifact: assert len(da.results) == 4 assert da.results[0]["package_name"] == "com.android.settings" assert ( - da.results[0]["service"] + da.results[0]["component"] == "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor" ) @@ -37,7 +37,9 @@ class TestDumpsysAccessibilityArtifact: da.parse(data) assert len(da.results) == 1 assert da.results[0]["package_name"] == "com.malware.accessibility" - assert da.results[0]["service"] == "com.malware.service.malwareservice" + assert da.results[0]["service_name"] == "com.malware.service.malwareservice" + assert da.results[0]["enabled"] is True + assert da.results[0]["installed"] is False def test_accessibility_service_alert(self): da = DumpsysAccessibilityArtifact() @@ -52,6 +54,22 @@ class TestDumpsysAccessibilityArtifact: assert da.alertstore.alerts[0].level == AlertLevel.MEDIUM assert da.alertstore.alerts[0].event == da.results[0] + def test_same_component_is_kept_for_each_user(self): + da = DumpsysAccessibilityArtifact() + da.parse( + """User state[attributes:{id=0 + installed services: { + 0 : com.example/.Service + } +User state[attributes:{id=10 + installed services: { + 0 : com.example/.Service + } +""" + ) + + assert [result["user_id"] for result in da.results] == [0, 10] + def test_ioc_check(self, indicator_file): da = DumpsysAccessibilityArtifact() file = get_artifact("android_data/dumpsys_accessibility.txt")