diff --git a/mvt/android/modules/adb/dumpsys_accessibility.py b/mvt/android/modules/adb/dumpsys_accessibility.py index f5053c2..53c7f8e 100644 --- a/mvt/android/modules/adb/dumpsys_accessibility.py +++ b/mvt/android/modules/adb/dumpsys_accessibility.py @@ -5,6 +5,8 @@ import logging +from mvt.android.parsers import parse_dumpsys_accessibility + from .base import AndroidExtraction log = logging.getLogger(__name__) @@ -30,36 +32,11 @@ class DumpsysAccessibility(AndroidExtraction): self.detected.append(result) continue - @staticmethod - def parse_accessibility(output): - results = [] - - in_services = False - for line in output.splitlines(): - if line.strip().startswith("installed services:"): - in_services = True - continue - - if not in_services: - continue - - if line.strip() == "}": - break - - service = line.split(":")[1].strip() - - results.append({ - "package_name": service.split("/")[0], - "service": service, - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys accessibility") - self.results = self.parse_accessibility(output) + self.results = parse_dumpsys_accessibility(output) for result in self.results: log.info("Found installed accessibility service \"%s\"", result.get("service")) diff --git a/mvt/android/modules/adb/dumpsys_activities.py b/mvt/android/modules/adb/dumpsys_activities.py index f392036..f9b8b2b 100644 --- a/mvt/android/modules/adb/dumpsys_activities.py +++ b/mvt/android/modules/adb/dumpsys_activities.py @@ -7,6 +7,8 @@ import logging from .base import AndroidExtraction +from mvt.android.parsers import parse_dumpsys_activity_resolver_table + log = logging.getLogger(__name__) @@ -33,66 +35,10 @@ class DumpsysActivities(AndroidExtraction): self.detected.append({intent: activity}) continue - @staticmethod - def parse_activity_resolver_table(output): - results = {} - - in_activity_resolver_table = False - in_non_data_actions = False - intent = None - for line in output.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(":", "") - results[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. - activity = line.strip().split(" ")[1] - package_name = activity.split("/")[0] - - results[intent].append({ - "package_name": package_name, - "activity": activity, - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys package") - self.results = self.parse_activity_resolver_table(output) + self.results = parse_dumpsys_activity_resolver_table(output) self._adb_disconnect() diff --git a/mvt/android/modules/adb/dumpsys_battery_daily.py b/mvt/android/modules/adb/dumpsys_battery_daily.py index 86adc2d..819680d 100644 --- a/mvt/android/modules/adb/dumpsys_battery_daily.py +++ b/mvt/android/modules/adb/dumpsys_battery_daily.py @@ -7,6 +7,8 @@ import logging from .base import AndroidExtraction +from mvt.android.parsers import parse_dumpsys_battery_daily + log = logging.getLogger(__name__) @@ -38,54 +40,11 @@ class DumpsysBatteryDaily(AndroidExtraction): self.detected.append(result) continue - @staticmethod - def parse_battery_daily(output): - results = [] - daily = None - daily_updates = [] - for line in output.splitlines()[1:]: - if line.startswith(" Daily from "): - if len(daily_updates) > 0: - results.extend(daily_updates) - daily_updates = [] - - timeframe = line[13:].strip() - date_from, date_to = timeframe.strip(":").split(" to ", 1) - daily = {"from": date_from[0:10], "to": date_to[0:10]} - continue - - if not daily: - continue - - if not line.strip().startswith("Update "): - continue - - line = line.strip().replace("Update ", "") - package_name, vers = line.split(" ", 1) - vers_nr = vers.split("=", 1)[1] - - already_seen = False - for update in daily_updates: - if package_name == update["package_name"] and vers_nr == update["vers"]: - already_seen = True - break - - if not already_seen: - daily_updates.append({ - "action": "update", - "from": daily["from"], - "to": daily["to"], - "package_name": package_name, - "vers": vers_nr, - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys batterystats --daily") - self.results = self.parse_battery_daily(output) + self.results = parse_dumpsys_battery_daily(output) self.log.info("Extracted %d records from battery daily stats", len(self.results)) diff --git a/mvt/android/modules/adb/dumpsys_battery_history.py b/mvt/android/modules/adb/dumpsys_battery_history.py index dbad9b0..0ce521e 100644 --- a/mvt/android/modules/adb/dumpsys_battery_history.py +++ b/mvt/android/modules/adb/dumpsys_battery_history.py @@ -6,6 +6,7 @@ import logging from .base import AndroidExtraction +from mvt.android.parsers import parse_dumpsys_battery_history log = logging.getLogger(__name__) @@ -30,61 +31,11 @@ class DumpsysBatteryHistory(AndroidExtraction): self.detected.append(result) continue - @staticmethod - def parse_battery_history(output): - results = [] - - for line in output.splitlines()[1:]: - if line.strip() == "": - break - - time_elapsed, rest = line.strip().split(" ", 1) - - start = line.find(" 100 ") - if start == -1: - continue - - line = line[start+5:] - - event = "" - if line.startswith("+job"): - event = "start_job" - elif line.startswith("-job"): - event = "end_job" - elif line.startswith("+running +wake_lock="): - event = "wake" - else: - continue - - if event in ["start_job", "end_job"]: - uid = line[line.find("=")+1:line.find(":")] - service = line[line.find(":")+1:].strip('"') - package_name = service.split("/")[0] - elif event == "wake": - uid = line[line.find("=")+1:line.find(":")] - service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip() - if service == "" or "/" not in service: - continue - - package_name = service.split("/")[0] - else: - continue - - results.append({ - "time_elapsed": time_elapsed, - "event": event, - "uid": uid, - "package_name": package_name, - "service": service, - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys batterystats --history") - self.results = self.parse_battery_history(output) + self.results = parse_dumpsys_battery_history(output) self.log.info("Extracted %d records from battery history", len(self.results)) diff --git a/mvt/android/modules/adb/dumpsys_dbinfo.py b/mvt/android/modules/adb/dumpsys_dbinfo.py index 55b79ac..34730e9 100644 --- a/mvt/android/modules/adb/dumpsys_dbinfo.py +++ b/mvt/android/modules/adb/dumpsys_dbinfo.py @@ -7,6 +7,7 @@ import logging import re from .base import AndroidExtraction +from mvt.android.parsers import parse_dumpsys_dbinfo log = logging.getLogger(__name__) @@ -35,45 +36,11 @@ class DumpsysDBInfo(AndroidExtraction): self.detected.append(result) continue - @staticmethod - def parse_dbinfo(output): - results = [] - - rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\".*path\=(.*?$)') - - in_operations = False - for line in output.splitlines(): - if line.strip() == "Most recently executed operations:": - in_operations = True - continue - - if not in_operations: - continue - - if not line.startswith(" "): - in_operations = False - continue - - matches = rxp.findall(line) - if not matches: - continue - - match = matches[0] - results.append({ - "isodate": match[0], - "pid": match[1], - "action": match[2], - "sql": match[3], - "path": match[4], - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys dbinfo") - self.results = self.parse_dbinfo(output) + self.results = parse_dumpsys_dbinfo(output) self.log.info("Extracted a total of %d records from database information", len(self.results)) diff --git a/mvt/android/modules/adb/dumpsys_receivers.py b/mvt/android/modules/adb/dumpsys_receivers.py index 1e5ce8a..fbebbdf 100644 --- a/mvt/android/modules/adb/dumpsys_receivers.py +++ b/mvt/android/modules/adb/dumpsys_receivers.py @@ -6,6 +6,7 @@ import logging from .base import AndroidExtraction +from mvt.android.parsers import parse_dumpsys_receiver_resolver_table log = logging.getLogger(__name__) @@ -55,66 +56,10 @@ class DumpsysReceivers(AndroidExtraction): self.detected.append({intent: receiver}) continue - @staticmethod - def parse_receiver_resolver_table(output): - results = {} - - 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(":", "") - results[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 = line.strip().split(" ")[1] - package_name = receiver.split("/")[0] - - results[intent].append({ - "package_name": package_name, - "receiver": receiver, - }) - - return results - def run(self): self._adb_connect() output = self._adb_command("dumpsys package") - self.results = self.parse_receiver_resolver_table(output) + self.results = parse_dumpsys_receiver_resolver_table(output) self._adb_disconnect() diff --git a/mvt/android/modules/bugreport/accessibility.py b/mvt/android/modules/bugreport/accessibility.py index 1b52a2d..0bf082f 100644 --- a/mvt/android/modules/bugreport/accessibility.py +++ b/mvt/android/modules/bugreport/accessibility.py @@ -5,8 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_accessibility import DumpsysAccessibility - +from mvt.android.parsers import parse_dumpsys_accessibility from .base import BugReportModule log = logging.getLogger(__name__) @@ -56,7 +55,7 @@ class Accessibility(BugReportModule): lines.append(line) - self.results = DumpsysAccessibility.parse_accessibility("\n".join(lines)) + self.results = parse_dumpsys_accessibility("\n".join(lines)) for result in self.results: log.info("Found installed accessibility service \"%s\"", result.get("service")) diff --git a/mvt/android/modules/bugreport/activities.py b/mvt/android/modules/bugreport/activities.py index 9fdac53..fb9c861 100644 --- a/mvt/android/modules/bugreport/activities.py +++ b/mvt/android/modules/bugreport/activities.py @@ -5,8 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_activities import DumpsysActivities - +from mvt.android.parsers import parse_dumpsys_activity_resolver_table from .base import BugReportModule log = logging.getLogger(__name__) @@ -59,4 +58,4 @@ class Activities(BugReportModule): lines.append(line) - self.results = DumpsysActivities.parse_activity_resolver_table("\n".join(lines)) + self.results = parse_dumpsys_activity_resolver_table("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/battery_daily.py b/mvt/android/modules/bugreport/battery_daily.py index d243dbf..ab6249a 100644 --- a/mvt/android/modules/bugreport/battery_daily.py +++ b/mvt/android/modules/bugreport/battery_daily.py @@ -5,8 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_battery_daily import DumpsysBatteryDaily - +from mvt.android.parsers import parse_dumpsys_battery_daily from .base import BugReportModule log = logging.getLogger(__name__) @@ -73,4 +72,4 @@ class BatteryDaily(BugReportModule): lines.append(line) - self.results = DumpsysBatteryDaily.parse_battery_daily("\n".join(lines)) + self.results = parse_dumpsys_battery_daily("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/battery_history.py b/mvt/android/modules/bugreport/battery_history.py index 5ed68c9..dc8b8c1 100644 --- a/mvt/android/modules/bugreport/battery_history.py +++ b/mvt/android/modules/bugreport/battery_history.py @@ -5,9 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_battery_history import \ - DumpsysBatteryHistory - +from mvt.android.parsers import parse_dumpsys_battery_history from .base import BugReportModule log = logging.getLogger(__name__) @@ -66,4 +64,4 @@ class BatteryHistory(BugReportModule): lines.append(line) - self.results = DumpsysBatteryHistory.parse_battery_history("\n".join(lines)) + self.results = parse_dumpsys_battery_history("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/dbinfo.py b/mvt/android/modules/bugreport/dbinfo.py index a6edb89..058b266 100644 --- a/mvt/android/modules/bugreport/dbinfo.py +++ b/mvt/android/modules/bugreport/dbinfo.py @@ -5,8 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_dbinfo import DumpsysDBInfo - +from mvt.android.parsers import parse_dumpsys_dbinfo from .base import BugReportModule log = logging.getLogger(__name__) @@ -60,4 +59,4 @@ class DBInfo(BugReportModule): lines.append(line) - self.results = DumpsysDBInfo.parse_dbinfo("\n".join(lines)) + self.results = parse_dumpsys_dbinfo("\n".join(lines)) diff --git a/mvt/android/modules/bugreport/receivers.py b/mvt/android/modules/bugreport/receivers.py index 62dc9bc..8e1271d 100644 --- a/mvt/android/modules/bugreport/receivers.py +++ b/mvt/android/modules/bugreport/receivers.py @@ -5,8 +5,7 @@ import logging -from mvt.android.modules.adb.dumpsys_receivers import DumpsysReceivers - +from mvt.android.parsers import parse_dumpsys_receiver_resolver_table from .base import BugReportModule log = logging.getLogger(__name__) @@ -81,4 +80,4 @@ class Receivers(BugReportModule): lines.append(line) - self.results = DumpsysReceivers.parse_receiver_resolver_table("\n".join(lines)) + self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines)) diff --git a/mvt/android/parsers/__init__.py b/mvt/android/parsers/__init__.py new file mode 100644 index 0000000..e809b24 --- /dev/null +++ b/mvt/android/parsers/__init__.py @@ -0,0 +1,266 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2022 The MVT Project 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 + +def parse_dumpsys_accessibility(output): + results = [] + + in_services = False + for line in output.splitlines(): + if line.strip().startswith("installed services:"): + in_services = True + continue + + if not in_services: + continue + + if line.strip() == "}": + break + + service = line.split(":")[1].strip() + + results.append({ + "package_name": service.split("/")[0], + "service": service, + }) + + return results + + +def parse_dumpsys_activity_resolver_table(output): + results = {} + + in_activity_resolver_table = False + in_non_data_actions = False + intent = None + for line in output.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(":", "") + results[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. + activity = line.strip().split(" ")[1] + package_name = activity.split("/")[0] + + results[intent].append({ + "package_name": package_name, + "activity": activity, + }) + + return results + +def parse_dumpsys_battery_daily(output): + results = [] + daily = None + daily_updates = [] + for line in output.splitlines()[1:]: + if line.startswith(" Daily from "): + if len(daily_updates) > 0: + results.extend(daily_updates) + daily_updates = [] + + timeframe = line[13:].strip() + date_from, date_to = timeframe.strip(":").split(" to ", 1) + daily = {"from": date_from[0:10], "to": date_to[0:10]} + continue + + if not daily: + continue + + if not line.strip().startswith("Update "): + continue + + line = line.strip().replace("Update ", "") + package_name, vers = line.split(" ", 1) + vers_nr = vers.split("=", 1)[1] + + already_seen = False + for update in daily_updates: + if package_name == update["package_name"] and vers_nr == update["vers"]: + already_seen = True + break + + if not already_seen: + daily_updates.append({ + "action": "update", + "from": daily["from"], + "to": daily["to"], + "package_name": package_name, + "vers": vers_nr, + }) + + return results + +def parse_dumpsys_battery_history(output): + results = [] + + for line in output.splitlines()[1:]: + if line.strip() == "": + break + + time_elapsed, rest = line.strip().split(" ", 1) + + start = line.find(" 100 ") + if start == -1: + continue + + line = line[start+5:] + + event = "" + if line.startswith("+job"): + event = "start_job" + elif line.startswith("-job"): + event = "end_job" + elif line.startswith("+running +wake_lock="): + event = "wake" + else: + continue + + if event in ["start_job", "end_job"]: + uid = line[line.find("=")+1:line.find(":")] + service = line[line.find(":")+1:].strip('"') + package_name = service.split("/")[0] + elif event == "wake": + uid = line[line.find("=")+1:line.find(":")] + service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip() + if service == "" or "/" not in service: + continue + + package_name = service.split("/")[0] + else: + continue + + results.append({ + "time_elapsed": time_elapsed, + "event": event, + "uid": uid, + "package_name": package_name, + "service": service, + }) + + return results + + +def parse_dumpsys_dbinfo(output): + results = [] + + rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\".*path\=(.*?$)') + + in_operations = False + for line in output.splitlines(): + if line.strip() == "Most recently executed operations:": + in_operations = True + continue + + if not in_operations: + continue + + if not line.startswith(" "): + in_operations = False + continue + + matches = rxp.findall(line) + if not matches: + continue + + match = matches[0] + results.append({ + "isodate": match[0], + "pid": match[1], + "action": match[2], + "sql": match[3], + "path": match[4], + }) + + return results + +def parse_dumpsys_receiver_resolver_table(output): + results = {} + + 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(":", "") + results[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 = line.strip().split(" ")[1] + package_name = receiver.split("/")[0] + + results[intent].append({ + "package_name": package_name, + "receiver": receiver, + }) + + return results