Compare commits

..

2 Commits

Author SHA1 Message Date
Janik Besendorf
6342e3261b Fix ruff F821: use self.log instead of undefined log 2026-04-12 10:05:43 +02:00
Janik Besendorf
642add21b0 Abort analysis and warn user when backup is encrypted
When `check-backup` is run against an encrypted backup, Manifest.db
cannot be opened as a plain SQLite database. Previously this caused
a flood of confusing "file is not a database" errors across all modules.

Now the Manifest module detects the sqlite3.DatabaseError on its first
query and raises a new EncryptedBackupError. This exception propagates
out of run_module() and is caught in Command.run(), which logs a clear
critical message instructing the user to decrypt the backup first with
`mvt-ios decrypt-backup`, then stops the analysis immediately.

Fixes #769
2026-04-12 10:03:57 +02:00
7 changed files with 42 additions and 114 deletions

View File

@@ -22,13 +22,13 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
def parse(self, content: str) -> None:
"""
Parse the Dumpsys Accessibility section.
Adds results to self.results (List[Dict[str, Any]])
Parse the Dumpsys Accessibility section/
Adds results to self.results (List[Dict[str, str]])
:param content: content of the accessibility section (string)
"""
# Parse installed services
# "Old" syntax
in_services = False
for line in content.splitlines():
if line.strip().startswith("installed services:"):
@@ -39,6 +39,7 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
continue
if line.strip() == "}":
# At end of installed services
break
service = line.split(":")[1].strip()
@@ -47,66 +48,21 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
{
"package_name": service.split("/")[0],
"service": service,
"enabled": False,
}
)
# 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()
# "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}}
in_enabled = False
for line in content.splitlines():
stripped = line.strip()
if line.strip().startswith("Enabled services:"):
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:
enabled_services.add(match.group(1).strip())
# Each match is in format: <package_name>/<service>
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(
{
"package_name": package_name,
"service": service,
"enabled": True,
}
)
self.results.append(
{"package_name": package_name, "service": service}
)

View File

@@ -49,14 +49,9 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
for result in self.results:
self.log.info(
'Found installed accessibility service "%s" (enabled: %s)',
result.get("service"),
result.get("enabled", False),
'Found installed accessibility service "%s"', result.get("service")
)
enabled_count = sum(1 for r in self.results if r.get("enabled"))
self.log.info(
"Identified a total of %d accessibility services, %d enabled",
len(self.results),
enabled_count,
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -11,7 +11,7 @@ from datetime import datetime
from typing import Optional
from mvt.common.indicators import Indicators
from mvt.common.module import MVTModule, run_module, save_timeline
from mvt.common.module import EncryptedBackupError, MVTModule, run_module, save_timeline
from mvt.common.utils import (
convert_datetime_to_iso,
generate_hashes_from_path,
@@ -244,7 +244,14 @@ class Command:
except NotImplementedError:
pass
run_module(m)
try:
run_module(m)
except EncryptedBackupError:
self.log.critical(
"The backup appears to be encrypted. "
"Please decrypt it first using `mvt-ios decrypt-backup`."
)
return
self.executed.append(m)

View File

@@ -21,6 +21,10 @@ class DatabaseCorruptedError(Exception):
pass
class EncryptedBackupError(Exception):
pass
class InsufficientPrivileges(Exception):
pass
@@ -169,6 +173,8 @@ def run_module(module: MVTModule) -> None:
try:
exec_or_profile("module.run()", globals(), locals())
except EncryptedBackupError:
raise
except NotImplementedError:
module.log.exception(
"The run() procedure of module %s was not implemented yet!",

View File

@@ -8,9 +8,10 @@ import io
import logging
import os
import plistlib
import sqlite3
from typing import Optional
from mvt.common.module import DatabaseNotFoundError
from mvt.common.module import DatabaseNotFoundError, EncryptedBackupError
from mvt.common.url import URL
from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso
@@ -127,7 +128,14 @@ class Manifest(IOSExtraction):
conn = self._open_sqlite_db(manifest_db_path)
cur = conn.cursor()
cur.execute("SELECT * FROM Files;")
try:
cur.execute("SELECT * FROM Files;")
except sqlite3.DatabaseError:
conn.close()
raise EncryptedBackupError(
"Manifest.db is not a valid SQLite database. "
"The backup may be encrypted."
)
names = [description[0] for description in cur.description]
for file_entry in cur:

View File

@@ -25,9 +25,6 @@ class TestDumpsysAccessibilityArtifact:
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()
@@ -39,32 +36,7 @@ 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.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
assert da.results[0]["service"] == "com.malware.service.malwareservice"
def test_ioc_check(self, indicator_file):
da = DumpsysAccessibilityArtifact()

View File

@@ -1,16 +0,0 @@
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:{}