Compare commits

...

1 Commits

Author SHA1 Message Date
Janik Besendorf 9bcbaac3a2 Show which accessibility services are enabled
Parse both installed and enabled accessibility services, adding an
"enabled" field to each result. This lets users see at a glance
whether any installed accessibility service is actually active.

Fixes #744
2026-04-12 10:16:49 +02:00
4 changed files with 110 additions and 17 deletions
@@ -22,13 +22,13 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
def parse(self, content: str) -> None: def parse(self, content: str) -> None:
""" """
Parse the Dumpsys Accessibility section/ Parse the Dumpsys Accessibility section.
Adds results to self.results (List[Dict[str, str]]) Adds results to self.results (List[Dict[str, Any]])
:param content: content of the accessibility section (string) :param content: content of the accessibility section (string)
""" """
# "Old" syntax # Parse installed services
in_services = False in_services = False
for line in content.splitlines(): for line in content.splitlines():
if line.strip().startswith("installed services:"): if line.strip().startswith("installed services:"):
@@ -39,7 +39,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
continue continue
if line.strip() == "}": if line.strip() == "}":
# At end of installed services
break break
service = line.split(":")[1].strip() service = line.split(":")[1].strip()
@@ -48,21 +47,66 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
{ {
"package_name": service.split("/")[0], "package_name": service.split("/")[0],
"service": service, "service": service,
"enabled": False,
} }
) )
# "New" syntax - AOSP >= 14 (?) # Parse enabled services from both old and new formats.
# Looks like: #
# Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}} # Old format (multi-line block):
# enabled services: {
# 0 : com.example/.MyService
# }
#
# New format (single line, AOSP >= 14):
# Enabled services:{{com.example/com.example.MyService}, {com.other/com.other.Svc}}
enabled_services = set()
in_enabled = False
for line in content.splitlines(): for line in content.splitlines():
if line.strip().startswith("Enabled services:"): stripped = line.strip()
matches = re.finditer(r"{([^{]+?)}", line)
if in_enabled:
if stripped == "}":
in_enabled = False
continue
service = line.split(":")[1].strip()
enabled_services.add(service)
continue
if re.match(r"enabled services:\s*\{\s*$", stripped, re.IGNORECASE):
# Old multi-line format: "enabled services: {"
in_enabled = True
continue
if re.match(r"enabled services:\s*\{", stripped, re.IGNORECASE):
# New single-line format: "Enabled services:{{pkg/svc}, {pkg2/svc2}}"
matches = re.finditer(r"\{([^{}]+)\}", stripped)
for match in matches: for match in matches:
# Each match is in format: <package_name>/<service> enabled_services.add(match.group(1).strip())
package_name, _, service = match.group(1).partition("/")
# Mark installed services that are enabled.
# Installed service names may include trailing annotations like
# "(A11yTool)" that are absent from the enabled services list,
# so strip annotations before comparing.
def _strip_annotation(s: str) -> str:
return re.sub(r"\s+\(.*?\)\s*$", "", s)
installed_stripped = {
_strip_annotation(r["service"]): r for r in self.results
}
for enabled in enabled_services:
if enabled in installed_stripped:
installed_stripped[enabled]["enabled"] = True
# Add enabled services not found in the installed list
for service in enabled_services:
if service not in installed_stripped:
package_name, _, _ = service.partition("/")
self.results.append( self.results.append(
{"package_name": package_name, "service": service} {
"package_name": package_name,
"service": service,
"enabled": True,
}
) )
@@ -49,9 +49,14 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
for result in self.results: for result in self.results:
self.log.info( self.log.info(
'Found installed accessibility service "%s"', result.get("service") 'Found installed accessibility service "%s" (enabled: %s)',
result.get("service"),
result.get("enabled", False),
) )
enabled_count = sum(1 for r in self.results if r.get("enabled"))
self.log.info( self.log.info(
"Identified a total of %d accessibility services", len(self.results) "Identified a total of %d accessibility services, %d enabled",
len(self.results),
enabled_count,
) )
@@ -25,6 +25,9 @@ class TestDumpsysAccessibilityArtifact:
da.results[0]["service"] da.results[0]["service"]
== "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor" == "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor"
) )
# All services are installed but none enabled in this fixture
for result in da.results:
assert result["enabled"] is False
def test_parsing_v14_aosp_format(self): def test_parsing_v14_aosp_format(self):
da = DumpsysAccessibilityArtifact() da = DumpsysAccessibilityArtifact()
@@ -36,7 +39,32 @@ class TestDumpsysAccessibilityArtifact:
da.parse(data) da.parse(data)
assert len(da.results) == 1 assert len(da.results) == 1
assert da.results[0]["package_name"] == "com.malware.accessibility" assert da.results[0]["package_name"] == "com.malware.accessibility"
assert da.results[0]["service"] == "com.malware.service.malwareservice" assert (
da.results[0]["service"]
== "com.malware.accessibility/com.malware.service.malwareservice"
)
assert da.results[0]["enabled"] is True
def test_parsing_installed_and_enabled(self):
da = DumpsysAccessibilityArtifact()
file = get_artifact("android_data/dumpsys_accessibility_enabled.txt")
with open(file) as f:
data = f.read()
assert len(da.results) == 0
da.parse(data)
assert len(da.results) == 5
enabled = [r for r in da.results if r["enabled"]]
assert len(enabled) == 1
assert enabled[0]["package_name"] == "com.samsung.accessibility"
assert (
enabled[0]["service"]
== "com.samsung.accessibility/.universalswitch.UniversalSwitchService (A11yTool)"
)
not_enabled = [r for r in da.results if not r["enabled"]]
assert len(not_enabled) == 4
def test_ioc_check(self, indicator_file): def test_ioc_check(self, indicator_file):
da = DumpsysAccessibilityArtifact() da = DumpsysAccessibilityArtifact()
@@ -0,0 +1,16 @@
ACCESSIBILITY MANAGER (dumpsys accessibility)
currentUserId=0
User state[
attributes:{id=0, touchExplorationEnabled=false, installedServiceCount=5}
installed services: {
0 : com.google.android.apps.accessibility.voiceaccess/.JustSpeakService (A11yTool)
1 : com.microsoft.appmanager/com.microsoft.mmx.screenmirroringsrc.accessibility.ScreenMirroringAccessibilityService
2 : com.samsung.accessibility/.assistantmenu.serviceframework.AssistantMenuService (A11yTool)
3 : com.samsung.accessibility/.universalswitch.UniversalSwitchService (A11yTool)
4 : com.samsung.android.accessibility.talkback/com.samsung.android.marvin.talkback.TalkBackService (A11yTool)
}
Bound services:{}
Enabled services:{{com.samsung.accessibility/.universalswitch.UniversalSwitchService}}
Binding services:{}
Crashed services:{}