Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 60 additions & 16 deletions src/mvt/android/artifacts/dumpsys_accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ def check_indicators(self) -> None:

def parse(self, content: str) -> None:
"""
Parse the Dumpsys Accessibility section/
Adds results to self.results (List[Dict[str, str]])
Parse the Dumpsys Accessibility section.
Adds results to self.results (List[Dict[str, Any]])

:param content: content of the accessibility section (string)
"""

# "Old" syntax
# Parse installed services
in_services = False
for line in content.splitlines():
if line.strip().startswith("installed services:"):
Expand All @@ -40,7 +40,6 @@ def parse(self, content: str) -> None:
continue

if line.strip() == "}":
# At end of installed services
break

service = line.split(":")[1].strip()
Expand All @@ -49,21 +48,66 @@ def parse(self, content: str) -> None:
{
"package_name": service.split("/")[0],
"service": service,
"enabled": False,
}
)

# "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}}

# Parse enabled services from both old and new formats.
#
# 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():
if line.strip().startswith("Enabled services:"):
matches = re.finditer(r"{([^{]+?)}", line)
stripped = line.strip()

if in_enabled:
if stripped == "}":
in_enabled = False
continue
service = line.split(":")[1].strip()
enabled_services.add(service)
continue

for match in matches:
# Each match is in format: <package_name>/<service>
package_name, _, service = match.group(1).partition("/")
if re.match(r"enabled services:\s*\{\s*$", stripped, re.IGNORECASE):
# Old multi-line format: "enabled services: {"
in_enabled = True
continue

self.results.append(
{"package_name": package_name, "service": service}
)
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:
enabled_services.add(match.group(1).strip())

# 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(
{
"package_name": package_name,
"service": service,
"enabled": True,
}
)
9 changes: 7 additions & 2 deletions src/mvt/android/modules/bugreport/dumpsys_accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ def run(self) -> None:

for result in self.results:
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(
"Identified a total of %d accessibility services", len(self.results)
"Identified a total of %d accessibility services, %d enabled",
len(self.results),
enabled_count,
)
30 changes: 29 additions & 1 deletion tests/android/test_artifact_dumpsys_accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def test_parsing(self):
da.results[0]["service"]
== "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):
da = DumpsysAccessibilityArtifact()
Expand All @@ -36,7 +39,32 @@ def test_parsing_v14_aosp_format(self):
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"]
== "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):
da = DumpsysAccessibilityArtifact()
Expand Down
16 changes: 16 additions & 0 deletions tests/artifacts/android_data/dumpsys_accessibility_enabled.txt
Original file line number Diff line number Diff line change
@@ -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:{}
Loading