mirror of
https://github.com/mvt-project/mvt.git
synced 2026-09-03 00:21:07 +02:00
Parse all package resolver categories
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
from .package_resolvers import parse_resolver_table
|
||||
|
||||
|
||||
class DumpsysPackageActivitiesArtifact(AndroidArtifact):
|
||||
@@ -19,67 +20,11 @@ class DumpsysPackageActivitiesArtifact(AndroidArtifact):
|
||||
)
|
||||
continue
|
||||
|
||||
def parse(self, content: str):
|
||||
def parse(self, content: str) -> None:
|
||||
"""
|
||||
Parse the Dumpsys Package section for activities
|
||||
Adds results to self.results
|
||||
|
||||
:param content: content of the package section (string)
|
||||
"""
|
||||
self.results = []
|
||||
|
||||
in_activity_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in content.splitlines():
|
||||
if line.startswith("Activity Resolver Table:"):
|
||||
in_activity_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_activity_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if (
|
||||
line.startswith(" " * 6)
|
||||
and not line.startswith(" " * 8)
|
||||
and ":" in line
|
||||
):
|
||||
intent = line.strip().replace(":", "")
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
activity = line.strip().split(" ")[1]
|
||||
package_name = activity.split("/")[0]
|
||||
|
||||
self.results.append(
|
||||
{
|
||||
"intent": intent,
|
||||
"package_name": package_name,
|
||||
"activity": activity,
|
||||
}
|
||||
)
|
||||
self.results = parse_resolver_table(content, "Activity")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
from .package_resolvers import parse_resolver_table
|
||||
|
||||
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||
@@ -18,115 +19,45 @@ class DumpsysReceiversArtifact(AndroidArtifact):
|
||||
"""
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept outgoing SMS messages: "%s"',
|
||||
receiver["receiver"],
|
||||
)
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept incoming SMS messages: "%s"',
|
||||
receiver["receiver"],
|
||||
)
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept incoming data SMS message: "%s"',
|
||||
receiver["receiver"],
|
||||
)
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info(
|
||||
"Found a receiver monitoring "
|
||||
'telephony state/incoming calls: "%s"',
|
||||
receiver["receiver"],
|
||||
)
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info(
|
||||
'Found a receiver monitoring outgoing calls: "%s"',
|
||||
receiver["receiver"],
|
||||
)
|
||||
for receiver in self.results:
|
||||
intent = receiver["key"]
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept outgoing SMS messages: "%s"',
|
||||
receiver["component"],
|
||||
)
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept incoming SMS messages: "%s"',
|
||||
receiver["component"],
|
||||
)
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info(
|
||||
'Found a receiver to intercept incoming data SMS message: "%s"',
|
||||
receiver["component"],
|
||||
)
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info(
|
||||
'Found a receiver monitoring telephony state/incoming calls: "%s"',
|
||||
receiver["component"],
|
||||
)
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info(
|
||||
'Found a receiver monitoring outgoing calls: "%s"',
|
||||
receiver["component"],
|
||||
)
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc_match = self.indicators.check_app_id(receiver["package_name"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
"",
|
||||
{intent: receiver},
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
continue
|
||||
ioc_match = self.indicators.check_app_id(receiver["package_name"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
"",
|
||||
receiver,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
def parse(self, output: str) -> None:
|
||||
self.results: dict[str, list[dict[str, str]]] = {}
|
||||
|
||||
in_receiver_resolver_table = False
|
||||
in_non_data_actions = False
|
||||
intent = None
|
||||
for line in output.splitlines():
|
||||
if line.startswith("Receiver Resolver Table:"):
|
||||
in_receiver_resolver_table = True
|
||||
continue
|
||||
|
||||
if not in_receiver_resolver_table:
|
||||
continue
|
||||
|
||||
if line.startswith(" Non-Data Actions:"):
|
||||
in_non_data_actions = True
|
||||
continue
|
||||
|
||||
if not in_non_data_actions:
|
||||
continue
|
||||
|
||||
# If we hit an empty line, the Non-Data Actions section should be
|
||||
# finished.
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
# We detect the action name.
|
||||
if (
|
||||
line.startswith(" " * 6)
|
||||
and not line.startswith(" " * 8)
|
||||
and ":" in line
|
||||
):
|
||||
intent = line.strip().replace(":", "")
|
||||
self.results[intent] = []
|
||||
continue
|
||||
|
||||
parts = line.strip().split(" ")
|
||||
if len(parts) < 2:
|
||||
# A single-token line here is not a receiver. Real dumpstate
|
||||
# output can print an action header mis-indented (observed with
|
||||
# 15 leading spaces instead of 6), which used to raise
|
||||
# IndexError and abort the whole module. Treat a trailing-colon
|
||||
# token as the next action, skip anything else.
|
||||
if parts[0].endswith(":"):
|
||||
intent = parts[0][:-1]
|
||||
self.results.setdefault(intent, [])
|
||||
continue
|
||||
|
||||
# If we are not in an intent block yet, skip.
|
||||
if not intent:
|
||||
continue
|
||||
|
||||
# If we are in a block but the line does not start with 8 spaces
|
||||
# it means the block ended a new one started, so we reset and
|
||||
# continue.
|
||||
if not line.startswith(" " * 8):
|
||||
intent = None
|
||||
continue
|
||||
|
||||
# If we got this far, we are processing receivers for the
|
||||
# activities we are interested in.
|
||||
receiver = parts[1]
|
||||
package_name = receiver.split("/")[0]
|
||||
|
||||
self.results[intent].append(
|
||||
{
|
||||
"package_name": package_name,
|
||||
"receiver": receiver,
|
||||
}
|
||||
)
|
||||
self.results = parse_resolver_table(output, "Receiver")
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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/
|
||||
|
||||
import re
|
||||
|
||||
|
||||
RESOLVER_TYPES = {
|
||||
"Full MIME Types": "full_mime_type",
|
||||
"Base MIME Types": "base_mime_type",
|
||||
"Wild MIME Types": "wild_mime_type",
|
||||
"Schemes": "scheme",
|
||||
"Non-Data Actions": "non_data_action",
|
||||
"MIME Typed Actions": "mime_typed_action",
|
||||
}
|
||||
|
||||
ENTRY_RE = re.compile(
|
||||
r"^\s{8,}[0-9a-fA-F]+\s+(?P<component>\S+)"
|
||||
r"(?:\s+\((?P<filters>\d+)\s+filters?\))?\s*$"
|
||||
)
|
||||
|
||||
|
||||
def parse_resolver_table(content: str, table_name: str) -> list[dict]:
|
||||
"""Parse every resolver category from a dumpsys package table."""
|
||||
results: list[dict] = []
|
||||
in_table = False
|
||||
resolver_type: str | None = None
|
||||
key: str | None = None
|
||||
|
||||
for line in content.splitlines():
|
||||
if line.startswith(f"{table_name} Resolver Table:"):
|
||||
in_table = True
|
||||
continue
|
||||
if not in_table:
|
||||
continue
|
||||
|
||||
if line and not line.startswith(" "):
|
||||
break
|
||||
|
||||
section_match = re.match(r"^ {2}([^ ].*):\s*$", line)
|
||||
if section_match:
|
||||
resolver_type = RESOLVER_TYPES.get(section_match.group(1))
|
||||
key = None
|
||||
continue
|
||||
if resolver_type is None:
|
||||
continue
|
||||
|
||||
key_match = re.match(r"^ {6,}([^ ].*):\s*$", line)
|
||||
if key_match:
|
||||
key = key_match.group(1)
|
||||
continue
|
||||
if key is None:
|
||||
continue
|
||||
|
||||
entry_match = ENTRY_RE.match(line)
|
||||
if not entry_match:
|
||||
continue
|
||||
component = entry_match.group("component")
|
||||
results.append(
|
||||
{
|
||||
"resolver_type": resolver_type,
|
||||
"key": key,
|
||||
"package_name": component.split("/", 1)[0],
|
||||
"component": component,
|
||||
"filter_count": int(entry_match.group("filters") or 1),
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -33,7 +33,7 @@ class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
|
||||
results=results,
|
||||
)
|
||||
|
||||
self.results = results if results else {}
|
||||
self.results = results if results else []
|
||||
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
@@ -49,4 +49,4 @@ class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
|
||||
)
|
||||
|
||||
self.parse(dumpsys_section)
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
self.log.info("Extracted %d package receivers", len(self.results))
|
||||
|
||||
@@ -21,12 +21,16 @@ class TestDumpsysPackageActivitiesArtifact:
|
||||
|
||||
assert len(dpa.results) == 0
|
||||
dpa.parse(data)
|
||||
assert len(dpa.results) == 4
|
||||
assert dpa.results[0]["package_name"] == "com.samsung.android.app.social"
|
||||
assert len(dpa.results) == 10
|
||||
assert dpa.results[0]["package_name"] == "com.samsung.android.messaging"
|
||||
assert (
|
||||
dpa.results[0]["activity"]
|
||||
== "com.samsung.android.app.social/.feed.FeedsActivity"
|
||||
dpa.results[0]["component"]
|
||||
== "com.samsung.android.messaging/.ui.RcsTransferContent"
|
||||
)
|
||||
assert {result["resolver_type"] for result in dpa.results} == {
|
||||
"full_mime_type",
|
||||
"non_data_action",
|
||||
}
|
||||
|
||||
def test_ioc_check(self, indicator_file):
|
||||
dpa = DumpsysPackageActivitiesArtifact()
|
||||
@@ -41,4 +45,4 @@ class TestDumpsysPackageActivitiesArtifact:
|
||||
dpa.indicators = ind
|
||||
assert len(dpa.alertstore.alerts) == 0
|
||||
dpa.check_indicators()
|
||||
assert len(dpa.alertstore.alerts) == 1
|
||||
assert len(dpa.alertstore.alerts) == 2
|
||||
|
||||
@@ -19,17 +19,14 @@ class TestDumpsysReceiversArtifact:
|
||||
|
||||
assert len(dr.results) == 0
|
||||
dr.parse(data)
|
||||
assert len(dr.results) == 4
|
||||
assert (
|
||||
list(dr.results.keys())[0]
|
||||
== "com.android.storagemanager.automatic.SHOW_NOTIFICATION"
|
||||
)
|
||||
assert (
|
||||
dr.results["com.android.storagemanager.automatic.SHOW_NOTIFICATION"][0][
|
||||
"package_name"
|
||||
]
|
||||
== "com.android.storagemanager"
|
||||
assert len(dr.results) == 9
|
||||
assert dr.results[0]["resolver_type"] == "full_mime_type"
|
||||
storage_manager = next(
|
||||
result
|
||||
for result in dr.results
|
||||
if result["key"] == "com.android.storagemanager.automatic.SHOW_NOTIFICATION"
|
||||
)
|
||||
assert storage_manager["package_name"] == "com.android.storagemanager"
|
||||
|
||||
def test_parsing_misindented_action(self):
|
||||
dr = DumpsysReceiversArtifact()
|
||||
@@ -44,12 +41,8 @@ Receiver Resolver Table:
|
||||
|
||||
dr.parse(data)
|
||||
|
||||
assert (
|
||||
dr.results["android.intent.action.MY_PACKAGE_REPLACED"][0][
|
||||
"package_name"
|
||||
]
|
||||
== "com.psycatgames.nhiegame"
|
||||
)
|
||||
assert dr.results[1]["key"] == "android.intent.action.MY_PACKAGE_REPLACED"
|
||||
assert dr.results[1]["package_name"] == "com.psycatgames.nhiegame"
|
||||
|
||||
def test_parsing_misindented_first_action(self):
|
||||
dr = DumpsysReceiversArtifact()
|
||||
@@ -62,12 +55,8 @@ Receiver Resolver Table:
|
||||
|
||||
dr.parse(data)
|
||||
|
||||
assert (
|
||||
dr.results["android.intent.action.MY_PACKAGE_REPLACED"][0][
|
||||
"package_name"
|
||||
]
|
||||
== "com.psycatgames.nhiegame"
|
||||
)
|
||||
assert dr.results[0]["key"] == "android.intent.action.MY_PACKAGE_REPLACED"
|
||||
assert dr.results[0]["package_name"] == "com.psycatgames.nhiegame"
|
||||
|
||||
def test_ioc_check(self, indicator_file):
|
||||
dr = DumpsysReceiversArtifact()
|
||||
|
||||
Reference in New Issue
Block a user