mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-02 05:59:39 +02:00
Two issues from review: * The set of printed state sections was global, so a section printed for one user decided the flags of another. A user with an installed list and no enabled section was marked enabled=False and got a LOW "installed, not enabled" alert. Sections are now tracked per user. * A count-only record was added only when a user had no named component at all. A dump stating installedServiceCount=2 and naming one service read as complete. The count-only record is now added whenever the stated count exceeds the distinct named components for that user, and carries the difference in a new unnamed_service_count field. The module summary adds up that remainder.
156 lines
6.0 KiB
Python
156 lines
6.0 KiB
Python
# Mobile Verification Toolkit (MVT)
|
|
# Copyright (c) 2021-2026 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/
|
|
"""Installed is not the same as enabled, and the dump says which.
|
|
|
|
Two things this guards:
|
|
|
|
* a section the dump never printed must read as None ("not stated"), not as
|
|
False ("not enabled");
|
|
* the alert must name the state, instead of firing identically on a device
|
|
where nothing is switched on and one where something is bound.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
|
|
from mvt.common.alerts import AlertLevel
|
|
|
|
from ..utils import get_artifact
|
|
|
|
NO_STATE_SECTIONS = """\
|
|
ACCESSIBILITY MANAGER (dumpsys accessibility)
|
|
User state[attributes:{id=0, currentUser=true}
|
|
installed services: {
|
|
0 : com.example.app/com.example.app.Service
|
|
}
|
|
"""
|
|
|
|
ENABLED_BLOCK = """\
|
|
ACCESSIBILITY MANAGER (dumpsys accessibility)
|
|
User state[attributes:{id=0, currentUser=true}
|
|
installed services: {
|
|
0 : com.example.app/com.example.app.Service
|
|
1 : com.other.app/.Helper
|
|
}
|
|
enabled services: {
|
|
0 : com.other.app/.Helper
|
|
}
|
|
bound services:{
|
|
0 : com.other.app/.Helper
|
|
}
|
|
"""
|
|
|
|
# User 0 prints only `installed services`, user 10 only `Enabled services`.
|
|
# Neither section speaks for the other user.
|
|
TWO_USERS_DIFFERENT_SECTIONS = """\
|
|
ACCESSIBILITY MANAGER (dumpsys accessibility)
|
|
User state[attributes:{id=0, currentUser=true}
|
|
installed services: {
|
|
0 : com.example.app/com.example.app.Service
|
|
}
|
|
User state[attributes:{id=10, currentUser=false}
|
|
Enabled services:{{com.other.app/.Helper}}
|
|
"""
|
|
|
|
|
|
class _IndicatorsMatching:
|
|
"""Minimal stand-in: matches one package id, like the STIX2 loader would."""
|
|
|
|
def __init__(self, package_name: str) -> None:
|
|
self.package_name = package_name
|
|
|
|
def check_app_id(self, app_id):
|
|
if app_id != self.package_name:
|
|
return None
|
|
return SimpleNamespace(
|
|
message=f"Found a known suspicious app: {app_id}", ioc={"value": app_id}
|
|
)
|
|
|
|
|
|
class TestAccessibilityServiceState:
|
|
def _parse(self, content):
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
artifact.parse(content)
|
|
return {r["component"]: r for r in artifact.results}
|
|
|
|
def test_absent_sections_leave_the_state_unknown(self):
|
|
# None, not False: a build that does not print the sections says
|
|
# nothing about what is enabled, and that must not read as "nothing".
|
|
state = self._parse(NO_STATE_SECTIONS)[
|
|
"com.example.app/com.example.app.Service"
|
|
]
|
|
assert state["installed"] is True
|
|
assert state["enabled"] is None
|
|
assert state["bound"] is None
|
|
|
|
def test_enabled_and_bound_are_attributed_per_service(self):
|
|
results = self._parse(ENABLED_BLOCK)
|
|
installed_only = results["com.example.app/com.example.app.Service"]
|
|
active = results["com.other.app/.Helper"]
|
|
assert (installed_only["enabled"], installed_only["bound"]) == (False, False)
|
|
assert (active["enabled"], active["bound"]) == (True, True)
|
|
|
|
def test_alert_message_carries_the_state(self):
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
with open(get_artifact("android_data/dumpsys_accessibility.txt")) as handle:
|
|
artifact.parse(handle.read())
|
|
artifact.check_indicators()
|
|
assert artifact.alertstore.alerts
|
|
assert all("installed" in alert.message for alert in artifact.alertstore.alerts)
|
|
|
|
def test_a_switched_off_service_is_low_and_a_running_one_medium(self):
|
|
# A service the dump says is OFF still reaches the analyst, but must not
|
|
# compete with one that is actually bound. "Not stated" is not "off".
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
artifact.parse(ENABLED_BLOCK)
|
|
artifact.check_indicators()
|
|
by_level = {}
|
|
for alert in artifact.alertstore.alerts:
|
|
by_level.setdefault(alert.level, []).append(alert.message)
|
|
assert artifact.alertstore.count(AlertLevel.LOW) == 1
|
|
assert artifact.alertstore.count(AlertLevel.MEDIUM) == 1
|
|
assert "com.example.app" in by_level[AlertLevel.LOW][0]
|
|
assert "com.other.app" in by_level[AlertLevel.MEDIUM][0]
|
|
|
|
def test_an_unstated_enabled_state_stays_medium(self):
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
artifact.parse(NO_STATE_SECTIONS)
|
|
artifact.check_indicators()
|
|
assert artifact.alertstore.count(AlertLevel.MEDIUM) == 1
|
|
assert artifact.alertstore.count(AlertLevel.LOW) == 0
|
|
|
|
def test_a_disabled_service_is_still_matched_against_indicators(self):
|
|
# The state decides the severity of an ordinary finding, never whether
|
|
# the package is compared with the IOC feeds.
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
artifact.parse(ENABLED_BLOCK)
|
|
artifact.indicators = _IndicatorsMatching("com.example.app")
|
|
artifact.check_indicators()
|
|
assert artifact.alertstore.count(AlertLevel.CRITICAL) == 1
|
|
assert artifact.alertstore.count(AlertLevel.LOW) == 0
|
|
|
|
def test_printed_sections_are_tracked_per_user(self):
|
|
artifact = DumpsysAccessibilityArtifact()
|
|
artifact.results = []
|
|
artifact.parse(TWO_USERS_DIFFERENT_SECTIONS)
|
|
by_user = {r["user_id"]: r for r in artifact.results}
|
|
# User 0's enabled state is not stated, so it is unknown, not off.
|
|
assert (by_user[0]["installed"], by_user[0]["enabled"]) == (True, None)
|
|
# User 10's installed state is not stated either.
|
|
assert (by_user[10]["installed"], by_user[10]["enabled"]) == (None, True)
|
|
|
|
artifact.check_indicators()
|
|
assert artifact.alertstore.count(AlertLevel.LOW) == 0
|
|
assert artifact.alertstore.count(AlertLevel.MEDIUM) == 2
|
|
assert not any(
|
|
"installed, not enabled" in alert.message
|
|
for alert in artifact.alertstore.alerts
|
|
)
|