From e60e5fdc6e8c22dcc573285582769f63849bbc81 Mon Sep 17 00:00:00 2001 From: tek Date: Fri, 4 Aug 2023 19:20:14 +0200 Subject: [PATCH] Refactors DumpsysBatteryHistory and adds related androidqf module --- .../artifacts/dumpsys_battery_history.py | 78 +++++++++++++++++++ .../modules/adb/dumpsys_battery_history.py | 17 +--- mvt/android/modules/androidqf/__init__.py | 4 + .../androidqf/dumpsys_battery_history.py | 46 +++++++++++ .../modules/bugreport/battery_history.py | 36 ++------- mvt/android/parsers/__init__.py | 5 +- mvt/android/parsers/dumpsys.py | 60 -------------- .../test_artifact_dumpsys_battery_history.py | 44 +++++++++++ tests/android/test_dumpsys_parser.py | 21 +---- .../test_dumpsys_battery_history.py | 24 ++++++ tests/artifacts/androidqf/dumpsys.txt | 7 ++ tests/common/test_utils.py | 2 +- 12 files changed, 215 insertions(+), 129 deletions(-) create mode 100644 mvt/android/artifacts/dumpsys_battery_history.py create mode 100644 mvt/android/modules/androidqf/dumpsys_battery_history.py create mode 100644 tests/android/test_artifact_dumpsys_battery_history.py create mode 100644 tests/android_androidqf/test_dumpsys_battery_history.py diff --git a/mvt/android/artifacts/dumpsys_battery_history.py b/mvt/android/artifacts/dumpsys_battery_history.py new file mode 100644 index 0000000..a22e079 --- /dev/null +++ b/mvt/android/artifacts/dumpsys_battery_history.py @@ -0,0 +1,78 @@ +# 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 .artifact import AndroidArtifact + + +class DumpsysBatteryHistoryArtifact(AndroidArtifact): + """ + Parser for dumpsys dattery history events. + """ + + def check_indicators(self) -> None: + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_app_id(result["package_name"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + def parse(self, data: str) -> None: + for line in data.splitlines(): + if line.startswith("Battery History "): + continue + + if line.strip() == "": + break + + time_elapsed = line.strip().split(" ", 1)[0] + + event = "" + if line.find("+job") > 0: + event = "start_job" + uid = line[line.find("+job") + 5 : line.find(":")] + service = line[line.find(":") + 1 :].strip('"') + package_name = service.split("/")[0] + elif line.find("-job") > 0: + event = "end_job" + uid = line[line.find("-job") + 5 : line.find(":")] + service = line[line.find(":") + 1 :].strip('"') + package_name = service.split("/")[0] + elif line.find("+running +wake_lock=") > 0: + uid = line[line.find("+running +wake_lock=") + 21 : line.find(":")] + event = "wake" + service = ( + line[line.find("*walarm*:") + 9 :].split(" ")[0].strip('"').strip() + ) + if service == "" or "/" not in service: + continue + + package_name = service.split("/")[0] + elif (line.find("+top=") > 0) or (line.find("-top") > 0): + if line.find("+top=") > 0: + event = "start_top" + top_pos = line.find("+top=") + else: + event = "end_top" + top_pos = line.find("-top=") + colon_pos = top_pos + line[top_pos:].find(":") + uid = line[top_pos + 5 : colon_pos] + service = "" + package_name = line[colon_pos + 1 :].strip('"') + else: + continue + + self.results.append( + { + "time_elapsed": time_elapsed, + "event": event, + "uid": uid, + "package_name": package_name, + "service": service, + } + ) diff --git a/mvt/android/modules/adb/dumpsys_battery_history.py b/mvt/android/modules/adb/dumpsys_battery_history.py index 66bd3f4..51079b3 100644 --- a/mvt/android/modules/adb/dumpsys_battery_history.py +++ b/mvt/android/modules/adb/dumpsys_battery_history.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_battery_history +from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact from .base import AndroidExtraction -class DumpsysBatteryHistory(AndroidExtraction): +class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction): """This module extracts records from battery history events.""" def __init__( @@ -32,22 +32,11 @@ class DumpsysBatteryHistory(AndroidExtraction): results=results, ) - def check_indicators(self) -> None: - if not self.indicators: - return - - for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - continue - def run(self) -> None: self._adb_connect() output = self._adb_command("dumpsys batterystats --history") self._adb_disconnect() - self.results = parse_dumpsys_battery_history(output) + self.parse(output) self.log.info("Extracted %d records from battery history", len(self.results)) diff --git a/mvt/android/modules/androidqf/__init__.py b/mvt/android/modules/androidqf/__init__.py index bce6318..78657de 100644 --- a/mvt/android/modules/androidqf/__init__.py +++ b/mvt/android/modules/androidqf/__init__.py @@ -6,6 +6,8 @@ from .dumpsys_accessibility import DumpsysAccessibility from .dumpsys_activities import DumpsysActivities from .dumpsys_appops import DumpsysAppops +from .dumpsys_battery_daily import DumpsysBatteryDaily +from .dumpsys_battery_history import DumpsysBatteryHistory from .dumpsys_dbinfo import DumpsysDBInfo from .dumpsys_packages import DumpsysPackages from .dumpsys_receivers import DumpsysReceivers @@ -20,6 +22,8 @@ ANDROIDQF_MODULES = [ DumpsysAccessibility, DumpsysAppops, DumpsysDBInfo, + DumpsysBatteryDaily, + DumpsysBatteryHistory, Processes, Getprop, Settings, diff --git a/mvt/android/modules/androidqf/dumpsys_battery_history.py b/mvt/android/modules/androidqf/dumpsys_battery_history.py new file mode 100644 index 0000000..272bcdc --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_battery_history.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_battery_history import DumpsysBatteryHistoryArtifact + +from .base import AndroidQFModule + + +class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, 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 section + data = self._get_file_content(dumpsys_file[0]) + section = self.extract_dumpsys_section( + data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:" + ) + + # Parse it + self.parse(section) + self.log.info("Extracted a total of %d battery daily stats", len(self.results)) diff --git a/mvt/android/modules/bugreport/battery_history.py b/mvt/android/modules/bugreport/battery_history.py index 4cf02d6..3ffc7c4 100644 --- a/mvt/android/modules/bugreport/battery_history.py +++ b/mvt/android/modules/bugreport/battery_history.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.android.parsers import parse_dumpsys_battery_history +from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact from .base import BugReportModule -class BatteryHistory(BugReportModule): +class BatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule): """This module extracts records from battery daily updates.""" def __init__( @@ -32,17 +32,6 @@ class BatteryHistory(BugReportModule): results=results, ) - def check_indicators(self) -> None: - if not self.indicators: - return - - for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - continue - def run(self) -> None: content = self._get_dumpstate_file() if not content: @@ -52,23 +41,10 @@ class BatteryHistory(BugReportModule): ) return - lines = [] - in_history = False - for line in content.decode(errors="ignore").splitlines(): - if line.strip().startswith("Battery History "): - lines.append(line) - in_history = True - continue - - if not in_history: - continue - - if line.strip() == "": - break - - lines.append(line) - - self.results = parse_dumpsys_battery_history("\n".join(lines)) + dumpsys_section = self.extract_dumpsys_section( + content.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:" + ) + self.parse(dumpsys_section) self.log.info( "Extracted a total of %d battery history records", len(self.results) diff --git a/mvt/android/parsers/__init__.py b/mvt/android/parsers/__init__.py index c88fca2..5271cba 100644 --- a/mvt/android/parsers/__init__.py +++ b/mvt/android/parsers/__init__.py @@ -3,7 +3,4 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from .dumpsys import ( - parse_dumpsys_battery_history, - parse_dumpsys_receiver_resolver_table, -) +from .dumpsys import parse_dumpsys_receiver_resolver_table diff --git a/mvt/android/parsers/dumpsys.py b/mvt/android/parsers/dumpsys.py index 8491ccf..422b3c8 100644 --- a/mvt/android/parsers/dumpsys.py +++ b/mvt/android/parsers/dumpsys.py @@ -7,66 +7,6 @@ import re from typing import Any, Dict, List -def parse_dumpsys_battery_history(output: str) -> List[Dict[str, Any]]: - results = [] - - for line in output.splitlines(): - if line.startswith("Battery History "): - continue - - if line.strip() == "": - break - - time_elapsed = line.strip().split(" ", 1)[0] - - event = "" - if line.find("+job") > 0: - event = "start_job" - uid = line[line.find("+job") + 5 : line.find(":")] - service = line[line.find(":") + 1 :].strip('"') - package_name = service.split("/")[0] - elif line.find("-job") > 0: - event = "end_job" - uid = line[line.find("-job") + 5 : line.find(":")] - service = line[line.find(":") + 1 :].strip('"') - package_name = service.split("/")[0] - elif line.find("+running +wake_lock=") > 0: - uid = line[line.find("+running +wake_lock=") + 21 : line.find(":")] - event = "wake" - service = ( - line[line.find("*walarm*:") + 9 :].split(" ")[0].strip('"').strip() - ) - if service == "" or "/" not in service: - continue - - package_name = service.split("/")[0] - elif (line.find("+top=") > 0) or (line.find("-top") > 0): - if line.find("+top=") > 0: - event = "start_top" - top_pos = line.find("+top=") - else: - event = "end_top" - top_pos = line.find("-top=") - colon_pos = top_pos + line[top_pos:].find(":") - uid = line[top_pos + 5 : colon_pos] - service = "" - package_name = line[colon_pos + 1 :].strip('"') - else: - continue - - results.append( - { - "time_elapsed": time_elapsed, - "event": event, - "uid": uid, - "package_name": package_name, - "service": service, - } - ) - - return results - - def parse_dumpsys_receiver_resolver_table(output: str) -> Dict[str, Any]: results = {} diff --git a/tests/android/test_artifact_dumpsys_battery_history.py b/tests/android/test_artifact_dumpsys_battery_history.py new file mode 100644 index 0000000..acd8c36 --- /dev/null +++ b/tests/android/test_artifact_dumpsys_battery_history.py @@ -0,0 +1,44 @@ +# 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_battery_history import DumpsysBatteryHistoryArtifact +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysBatteryHistoryArtifact: + def test_parsing(self): + dba = DumpsysBatteryHistoryArtifact() + file = get_artifact("android_data/dumpsys_battery.txt") + with open(file) as f: + data = f.read() + + assert len(dba.results) == 0 + dba.parse(data) + assert len(dba.results) == 5 + assert dba.results[0]["package_name"] == "com.samsung.android.app.reminder" + assert dba.results[1]["event"] == "end_job" + assert dba.results[2]["event"] == "start_top" + assert dba.results[2]["uid"] == "u0a280" + assert dba.results[2]["package_name"] == "com.whatsapp" + assert dba.results[3]["event"] == "end_top" + assert dba.results[4]["package_name"] == "com.sec.android.app.launcher" + + def test_ioc_check(self, indicator_file): + dba = DumpsysBatteryHistoryArtifact() + file = get_artifact("android_data/dumpsys_battery.txt") + with open(file) as f: + data = f.read() + dba.parse(data) + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["app_ids"].append("com.samsung.android.app.reminder") + dba.indicators = ind + assert len(dba.detected) == 0 + dba.check_indicators() + assert len(dba.detected) == 2 diff --git a/tests/android/test_dumpsys_parser.py b/tests/android/test_dumpsys_parser.py index ce3604e..42a5afe 100644 --- a/tests/android/test_dumpsys_parser.py +++ b/tests/android/test_dumpsys_parser.py @@ -3,31 +3,12 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from mvt.android.parsers.dumpsys import ( - parse_dumpsys_battery_history, - parse_dumpsys_packages, -) +from mvt.android.parsers.dumpsys import parse_dumpsys_packages from ..utils import get_artifact class TestDumpsysParsing: - def test_battery_history_parsing(self): - file = get_artifact("android_data/dumpsys_battery.txt") - with open(file) as f: - data = f.read() - - res = parse_dumpsys_battery_history(data) - - assert len(res) == 5 - assert res[0]["package_name"] == "com.samsung.android.app.reminder" - assert res[1]["event"] == "end_job" - assert res[2]["event"] == "start_top" - assert res[2]["uid"] == "u0a280" - assert res[2]["package_name"] == "com.whatsapp" - assert res[3]["event"] == "end_top" - assert res[4]["package_name"] == "com.sec.android.app.launcher" - def test_packages_parsing(self): file = get_artifact("android_data/dumpsys_packages.txt") with open(file) as f: diff --git a/tests/android_androidqf/test_dumpsys_battery_history.py b/tests/android_androidqf/test_dumpsys_battery_history.py new file mode 100644 index 0000000..6c2308f --- /dev/null +++ b/tests/android_androidqf/test_dumpsys_battery_history.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_battery_history import DumpsysBatteryHistory +from mvt.common.module import run_module + +from ..utils import get_android_androidqf, list_files + + +class TestDumpsysBatteryHistoryModule: + def test_parsing(self): + data_path = get_android_androidqf() + m = DumpsysBatteryHistory(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/androidqf/dumpsys.txt b/tests/artifacts/androidqf/dumpsys.txt index eaafabc..be5587d 100644 --- a/tests/artifacts/androidqf/dumpsys.txt +++ b/tests/artifacts/androidqf/dumpsys.txt @@ -304,6 +304,13 @@ Battery History (0% used, 11KB used of 4096KB, 79 strings using 9632): +2s042ms (2) 100 c0000020 +wake_lock=u0a12:"Wakeful StateMachine: GeofencerStateMachine" +2s044ms (1) 100 80000020 -wake_lock +2s050ms (2) 100 c0000020 +wake_lock=u0a12:"NlpWakeLock" + +23m32s163ms (2) 100 c0000020 +job=u0a134:"com.google.android.gm/com.google.android.libraries.internal.growth.growthkit.internal.jobs.impl.GrowthKitJobService" + +23m33s713ms (2) 100 c0000020 +job=u0a134:"com.google.android.gm/.job.ProviderCreatedJob$ProviderCreatedJobService" + +23m33s752ms (2) 100 c0000020 +job=u0a134:"com.google.android.gm/com.android.mail.widget.NotifyDatasetChangedJob$NotifyDatasetChangedJobService" + +23m33s786ms (2) 100 c0000020 -job=u0a134:"com.google.android.gm/.job.ProviderCreatedJob$ProviderCreatedJobService" + +23m33s867ms (2) 100 c0000020 -job=u0a134:"com.google.android.gm/com.google.android.libraries.internal.growth.growthkit.internal.jobs.impl.GrowthKitJobService" + +23m33s910ms (2) 100 c0000020 -job=u0a134:"com.google.android.gm/com.android.mail.widget.NotifyDatasetChangedJob$NotifyDatasetChangedJobService" + Daily stats: Current start time: 2023-07-27-02-02-56 diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index 76fc730..1624777 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"] - == "009f9eaa04658acdd179b463e05e1ea1fffea132e6e7ee556f0c385ee69a0811" + == "cfae0e04ef139b5a2ae1e2b3d400ce67eb98e67ff66f56ba2a580fe41bc120d0" )