From 6356a4ff87cda5c598295cf47242a042a3118e78 Mon Sep 17 00:00:00 2001 From: tek Date: Mon, 31 Jul 2023 23:43:20 +0200 Subject: [PATCH] Refactor code of DumpsysDBInfo --- mvt/android/artifacts/dumpsys_dbinfo.py | 82 +++++++++++++++++++ mvt/android/modules/adb/dumpsys_dbinfo.py | 19 +---- mvt/android/modules/androidqf/__init__.py | 2 + .../modules/androidqf/dumpsys_dbinfo.py | 46 +++++++++++ mvt/android/modules/bugreport/dbinfo.py | 43 ++-------- mvt/android/parsers/__init__.py | 1 - mvt/android/parsers/dumpsys.py | 61 -------------- tests/android/test_artifact_dumpsys_dbinfo.py | 42 ++++++++++ .../android_androidqf/test_dumpsys_dbinfo.py | 24 ++++++ .../artifacts/android_data/dumpsys_dbinfo.txt | 23 ++++++ tests/artifacts/androidqf/dumpsys.txt | 25 ++++++ tests/common/test_utils.py | 2 +- 12 files changed, 256 insertions(+), 114 deletions(-) create mode 100644 mvt/android/artifacts/dumpsys_dbinfo.py create mode 100644 mvt/android/modules/androidqf/dumpsys_dbinfo.py create mode 100644 tests/android/test_artifact_dumpsys_dbinfo.py create mode 100644 tests/android_androidqf/test_dumpsys_dbinfo.py create mode 100644 tests/artifacts/android_data/dumpsys_dbinfo.txt diff --git a/mvt/android/artifacts/dumpsys_dbinfo.py b/mvt/android/artifacts/dumpsys_dbinfo.py new file mode 100644 index 0000000..f6d1ad5 --- /dev/null +++ b/mvt/android/artifacts/dumpsys_dbinfo.py @@ -0,0 +1,82 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# 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 + +from .artifact import AndroidArtifact + + +class DumpsysDBInfo(AndroidArtifact): + """ + Parser for dumpsys DBInfo service + """ + + def check_indicators(self) -> None: + if not self.indicators: + return + + for result in self.results: + path = result.get("path", "") + for part in path.split("/"): + ioc = self.indicators.check_app_id(part) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def parse(self, output: str) -> None: + 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\=\"(.+?)\"" + ) # pylint: disable=line-too-long + rxp_no_pid = re.compile( + r".*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"" + ) # pylint: disable=line-too-long + + pool = None + in_operations = False + for line in output.splitlines(): + if line.startswith("Connection pool for "): + pool = line.replace("Connection pool for ", "").rstrip(":") + + if not pool: + continue + + if line.strip() == "Most recently executed operations:": + in_operations = True + continue + + if not in_operations: + continue + + if not line.startswith(" "): + in_operations = False + pool = None + continue + + matches = rxp.findall(line) + if not matches: + matches = rxp_no_pid.findall(line) + if not matches: + continue + + match = matches[0] + self.results.append( + { + "isodate": match[0], + "action": match[1], + "sql": match[2], + "path": pool, + } + ) + else: + match = matches[0] + self.results.append( + { + "isodate": match[0], + "pid": match[1], + "action": match[2], + "sql": match[3], + "path": pool, + } + ) diff --git a/mvt/android/modules/adb/dumpsys_dbinfo.py b/mvt/android/modules/adb/dumpsys_dbinfo.py index fc32697..2255ed6 100644 --- a/mvt/android/modules/adb/dumpsys_dbinfo.py +++ b/mvt/android/modules/adb/dumpsys_dbinfo.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_dbinfo +from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfo as DBI from .base import AndroidExtraction -class DumpsysDBInfo(AndroidExtraction): +class DumpsysDBInfo(DBI, AndroidExtraction): """This module extracts records from battery daily updates.""" slug = "dumpsys_dbinfo" @@ -34,25 +34,12 @@ class DumpsysDBInfo(AndroidExtraction): results=results, ) - def check_indicators(self) -> None: - if not self.indicators: - return - - for result in self.results: - path = result.get("path", "") - for part in path.split("/"): - ioc = self.indicators.check_app_id(part) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - continue - def run(self) -> None: self._adb_connect() output = self._adb_command("dumpsys dbinfo") self._adb_disconnect() - self.results = parse_dumpsys_dbinfo(output) + self.parse(output) self.log.info( "Extracted a total of %d records from database information", diff --git a/mvt/android/modules/androidqf/__init__.py b/mvt/android/modules/androidqf/__init__.py index ca749e2..bce6318 100644 --- a/mvt/android/modules/androidqf/__init__.py +++ b/mvt/android/modules/androidqf/__init__.py @@ -6,6 +6,7 @@ from .dumpsys_accessibility import DumpsysAccessibility from .dumpsys_activities import DumpsysActivities from .dumpsys_appops import DumpsysAppops +from .dumpsys_dbinfo import DumpsysDBInfo from .dumpsys_packages import DumpsysPackages from .dumpsys_receivers import DumpsysReceivers from .getprop import Getprop @@ -18,6 +19,7 @@ ANDROIDQF_MODULES = [ DumpsysReceivers, DumpsysAccessibility, DumpsysAppops, + DumpsysDBInfo, Processes, Getprop, Settings, diff --git a/mvt/android/modules/androidqf/dumpsys_dbinfo.py b/mvt/android/modules/androidqf/dumpsys_dbinfo.py new file mode 100644 index 0000000..7af91ea --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_dbinfo.py @@ -0,0 +1,46 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging +from typing import Optional + +from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfo as DBI + +from .base import AndroidQFModule + + +class DumpsysDBInfo(DBI, AndroidQFModule): + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + module_options: Optional[dict] = None, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None, + ) -> None: + super().__init__( + file_path=file_path, + target_path=target_path, + results_path=results_path, + module_options=module_options, + log=log, + results=results, + ) + + def run(self) -> None: + dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") + if not dumpsys_file: + return + + # Extract dumpsys DBInfo section + data = self._get_file_content(dumpsys_file[0]) + section = self.extract_dumpsys_section( + data.decode("utf-8", errors="replace"), "DUMP OF SERVICE dbinfo:" + ) + + # Parse it + self.parse(section) + self.log.info("Identified %d DB Info entries", len(self.results)) diff --git a/mvt/android/modules/bugreport/dbinfo.py b/mvt/android/modules/bugreport/dbinfo.py index dc37a6f..40502ee 100644 --- a/mvt/android/modules/bugreport/dbinfo.py +++ b/mvt/android/modules/bugreport/dbinfo.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_dbinfo +from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfo from .base import BugReportModule -class DBInfo(BugReportModule): +class DBInfo(DumpsysDBInfo, BugReportModule): """This module extracts records from battery daily updates.""" slug = "dbinfo" @@ -34,47 +34,20 @@ class DBInfo(BugReportModule): results=results, ) - def check_indicators(self) -> None: - if not self.indicators: - return - - for result in self.results: - path = result.get("path", "") - for part in path.split("/"): - ioc = self.indicators.check_app_id(part) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - continue - def run(self) -> None: - content = self._get_dumpstate_file() - if not content: + data = self._get_dumpstate_file() + if not data: self.log.error( "Unable to find dumpstate file. " "Did you provide a valid bug report archive?" ) return - in_dbinfo = False - lines = [] - for line in content.decode(errors="ignore").splitlines(): - if line.strip() == "DUMP OF SERVICE dbinfo:": - in_dbinfo = True - continue - - if not in_dbinfo: - continue - - if line.strip().startswith( - "------------------------------------------------------------------------------" - ): # pylint: disable=line-too-long - break - - lines.append(line) - - self.results = parse_dumpsys_dbinfo("\n".join(lines)) + section = self.extract_dumpsys_section( + data.decode("utf-8", errors="ignore"), "DUMP OF SERVICE dbinfo:" + ) + self.parse(section) self.log.info( "Extracted a total of %d database connection pool records", len(self.results), diff --git a/mvt/android/parsers/__init__.py b/mvt/android/parsers/__init__.py index 787210c..9e48ae9 100644 --- a/mvt/android/parsers/__init__.py +++ b/mvt/android/parsers/__init__.py @@ -7,6 +7,5 @@ from .dumpsys import ( parse_dumpsys_appops, parse_dumpsys_battery_daily, parse_dumpsys_battery_history, - parse_dumpsys_dbinfo, parse_dumpsys_receiver_resolver_table, ) diff --git a/mvt/android/parsers/dumpsys.py b/mvt/android/parsers/dumpsys.py index eeeffbb..8720194 100644 --- a/mvt/android/parsers/dumpsys.py +++ b/mvt/android/parsers/dumpsys.py @@ -118,67 +118,6 @@ def parse_dumpsys_battery_history(output: str) -> List[Dict[str, Any]]: return results -def parse_dumpsys_dbinfo(output: str) -> List[Dict[str, Any]]: - 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\=\"(.+?)\"" - ) # pylint: disable=line-too-long - rxp_no_pid = re.compile( - r".*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"" - ) # pylint: disable=line-too-long - - pool = None - in_operations = False - for line in output.splitlines(): - if line.startswith("Connection pool for "): - pool = line.replace("Connection pool for ", "").rstrip(":") - - if not pool: - continue - - if line.strip() == "Most recently executed operations:": - in_operations = True - continue - - if not in_operations: - continue - - if not line.startswith(" "): - in_operations = False - pool = None - continue - - matches = rxp.findall(line) - if not matches: - matches = rxp_no_pid.findall(line) - if not matches: - continue - - match = matches[0] - results.append( - { - "isodate": match[0], - "action": match[1], - "sql": match[2], - "path": pool, - } - ) - else: - match = matches[0] - results.append( - { - "isodate": match[0], - "pid": match[1], - "action": match[2], - "sql": match[3], - "path": pool, - } - ) - - return results - - def parse_dumpsys_receiver_resolver_table(output: str) -> Dict[str, Any]: results = {} diff --git a/tests/android/test_artifact_dumpsys_dbinfo.py b/tests/android/test_artifact_dumpsys_dbinfo.py new file mode 100644 index 0000000..f0398a5 --- /dev/null +++ b/tests/android/test_artifact_dumpsys_dbinfo.py @@ -0,0 +1,42 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ +import logging + +from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfo +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysDBinfoArtifact: + def test_parsing(self): + dbi = DumpsysDBInfo() + file = get_artifact("android_data/dumpsys_dbinfo.txt") + with open(file) as f: + data = f.read() + + assert len(dbi.results) == 0 + dbi.parse(data) + assert len(dbi.results) == 5 + assert dbi.results[0]["action"] == "executeForCursorWindow" + assert dbi.results[0]["sql"] == "PRAGMA database_list;" + assert ( + dbi.results[0]["path"] == "/data/user/0/com.wssyncmldm/databases/idmsdk.db" + ) + + def test_ioc_check(self, indicator_file): + dbi = DumpsysDBInfo() + file = get_artifact("android_data/dumpsys_dbinfo.txt") + with open(file) as f: + data = f.read() + dbi.parse(data) + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["app_ids"].append("com.wssyncmldm") + dbi.indicators = ind + assert len(dbi.detected) == 0 + dbi.check_indicators() + assert len(dbi.detected) == 5 diff --git a/tests/android_androidqf/test_dumpsys_dbinfo.py b/tests/android_androidqf/test_dumpsys_dbinfo.py new file mode 100644 index 0000000..71d6cca --- /dev/null +++ b/tests/android_androidqf/test_dumpsys_dbinfo.py @@ -0,0 +1,24 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +from pathlib import Path + +from mvt.android.modules.androidqf.dumpsys_dbinfo import DumpsysDBInfo +from mvt.common.module import run_module + +from ..utils import get_android_androidqf, list_files + + +class TestDumpsysDBInfoModule: + def test_parsing(self): + data_path = get_android_androidqf() + m = DumpsysDBInfo(target_path=data_path) + files = list_files(data_path) + parent_path = Path(data_path).absolute().parent.as_posix() + m.from_folder(parent_path, files) + run_module(m) + assert len(m.results) == 6 + assert len(m.timeline) == 0 + assert len(m.detected) == 0 diff --git a/tests/artifacts/android_data/dumpsys_dbinfo.txt b/tests/artifacts/android_data/dumpsys_dbinfo.txt new file mode 100644 index 0000000..2178ec2 --- /dev/null +++ b/tests/artifacts/android_data/dumpsys_dbinfo.txt @@ -0,0 +1,23 @@ +DUMP OF SERVICE dbinfo: +Applications Database Info: + +** Database info for pid 8817 [com.wssyncmldm] ** + + Attached db: false +Connection pool for /data/user/0/com.wssyncmldm/databases/idmsdk.db: + Open: true + Max connections: 1 + Total execution time: 46 + Configuration: openFlags=268435456, isLegacyCompatibilityWalEnabled=false, journalMode=, syncMode= + Secure db: false + Available primary connection: + Connection #0: + isPrimaryConnection: true + onlyAllowReadOnlyOperations: true + Most recently executed operations: + 0: [2023-07-27 12:21:44.097] [Pid:(0)]executeForCursorWindow took 2ms - succeeded, sql="PRAGMA database_list;", path=/data/user/0/com.wssyncmldm/databases/idmsdk.db + 1: [2023-07-27 12:21:44.096] [Pid:(0)]executeForLong took 0ms - succeeded, sql="PRAGMA page_size;", path=/data/user/0/com.wssyncmldm/databases/idmsdk.db + 2: [2023-07-27 12:21:44.092] [Pid:(0)]executeForLong took 4ms - succeeded, sql="PRAGMA page_count;", path=/data/user/0/com.wssyncmldm/databases/idmsdk.db + 3: [2023-07-26 19:27:41.386] [Pid:(8817)]executeForCursorWindow took 2ms - succeeded, sql="SELECT path, name, acl, scope, format, type, depth, value FROM x6g1q14r75 WHERE (path = './DMAcc/x6g1q14r75/AppAuth/client/AAuthName') ORDER BY depth ASC", path=/data/user/0/com.wssyncmldm/databases/idmsdk.db + 4: [2023-07-26 19:27:41.385] [Pid:(8817)]prepare took 0ms - succeeded, sql="SELECT path, name, acl, scope, format, type, depth, value FROM x6g1q14r75 WHERE (path = './DMAcc/x6g1q14r75/AppAuth/client/AAuthName') ORDER BY depth ASC", path=/data/user/0/com.wssyncmldm/databases/idmsdk.db + diff --git a/tests/artifacts/androidqf/dumpsys.txt b/tests/artifacts/androidqf/dumpsys.txt index 005b539..0b409fc 100644 --- a/tests/artifacts/androidqf/dumpsys.txt +++ b/tests/artifacts/androidqf/dumpsys.txt @@ -250,5 +250,30 @@ Current AppOps Service state: WRITE_EXTERNAL_STORAGE (allow): +------------------------------------------------------------------------------- +DUMP OF SERVICE dbinfo: +Applications Database Info: + +** Database info for pid 5748 [com.sec.android.inputmethod] ** + + Attached db: false +Connection pool for /data/user/0/com.sec.android.inputmethod/databases/StickerRecentList: + Open: true + Max connections: 4 + Total execution time: 61 + Configuration: openFlags=805306368, isLegacyCompatibilityWalEnabled=false, journalMode=, syncMode= + Secure db: false + Use WAL mode. + Available primary connection: + Connection #0: + isPrimaryConnection: true + onlyAllowReadOnlyOperations: false + Most recently executed operations: + 0: [2023-07-27 12:21:44.458] [Pid:(0)]executeForCursorWindow took 1ms - succeeded, sql="PRAGMA database_list;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList + 1: [2023-07-27 12:21:44.456] [Pid:(0)]executeForLong took 0ms - succeeded, sql="PRAGMA page_size;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList + 2: [2023-07-27 12:21:44.455] [Pid:(0)]executeForLong took 2ms - succeeded, sql="PRAGMA page_count;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList + 3: [2023-07-26 16:50:25.321] [Pid:(0)]executeForCursorWindow took 0ms - succeeded, sql="PRAGMA database_list;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList + 4: [2023-07-26 16:50:25.320] [Pid:(0)]executeForLong took 0ms - succeeded, sql="PRAGMA page_size;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList + 5: [2023-07-26 16:50:25.318] [Pid:(0)]executeForLong took 2ms - succeeded, sql="PRAGMA page_count;", path=/data/user/0/com.sec.android.inputmethod/databases/StickerRecentList diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index 64ba4aa..9e323cc 100644 --- a/tests/common/test_utils.py +++ b/tests/common/test_utils.py @@ -62,5 +62,5 @@ class TestHashes: assert hashes[1]["file_path"] == os.path.join(path, "dumpsys.txt") assert ( hashes[1]["sha256"] - == "bac858001784657a43c7cfa771fd1fc4a49428eb6b7c458a1ebf2fdeef78dd86" + == "c6be3ada77674f5bb9750d24e84b9b7ccf8db0cd4a896d9c17f9456eeab4bd0b" )