From 7e0e071c5def32ce4f736997f9c869aa26a9c200 Mon Sep 17 00:00:00 2001 From: tek Date: Fri, 4 Aug 2023 16:17:52 +0200 Subject: [PATCH] Refactor DumpsysBatteryDaily module and add related artifact --- .../artifacts/dumpsys_battery_daily.py | 78 +++++++++++++++++ .../modules/adb/dumpsys_battery_daily.py | 28 +------ .../androidqf/dumpsys_battery_daily.py | 46 ++++++++++ .../modules/bugreport/battery_daily.py | 55 ++---------- mvt/android/parsers/__init__.py | 1 - mvt/android/parsers/dumpsys.py | 48 ----------- .../test_artifact_dumpsys_battery_daily.py | 37 ++++++++ .../test_dumpsys_battery_daily.py | 24 ++++++ .../android_data/dumpsys_battery.txt | 49 +++++++++++ tests/artifacts/androidqf/dumpsys.txt | 84 +++++++++++++++++++ tests/common/test_utils.py | 2 +- 11 files changed, 330 insertions(+), 122 deletions(-) create mode 100644 mvt/android/artifacts/dumpsys_battery_daily.py create mode 100644 mvt/android/modules/androidqf/dumpsys_battery_daily.py create mode 100644 tests/android/test_artifact_dumpsys_battery_daily.py create mode 100644 tests/android_androidqf/test_dumpsys_battery_daily.py diff --git a/mvt/android/artifacts/dumpsys_battery_daily.py b/mvt/android/artifacts/dumpsys_battery_daily.py new file mode 100644 index 0000000..d55e88a --- /dev/null +++ b/mvt/android/artifacts/dumpsys_battery_daily.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 typing import Union + +from .artifact import AndroidArtifact + + +class DumpsysBatteryDailyArtifact(AndroidArtifact): + """ + Parser for dumpsys dattery daily updates. + """ + + def serialize(self, record: dict) -> Union[dict, list]: + return { + "timestamp": record["from"], + "module": self.__class__.__name__, + "event": "battery_daily", + "data": f"Recorded update of package {record['package_name']} " + f"with vers {record['vers']}", + } + + 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, output: str) -> None: + daily = None + daily_updates = [] + for line in output.splitlines(): + if line.startswith(" Daily from "): + if len(daily_updates) > 0: + self.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, + } + ) + + if len(daily_updates) > 0: + self.results.extend(daily_updates) diff --git a/mvt/android/modules/adb/dumpsys_battery_daily.py b/mvt/android/modules/adb/dumpsys_battery_daily.py index 5798dba..b90af3c 100644 --- a/mvt/android/modules/adb/dumpsys_battery_daily.py +++ b/mvt/android/modules/adb/dumpsys_battery_daily.py @@ -4,14 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional -from mvt.android.parsers import parse_dumpsys_battery_daily +from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact from .base import AndroidExtraction -class DumpsysBatteryDaily(AndroidExtraction): +class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction): """This module extracts records from battery daily updates.""" def __init__( @@ -32,32 +32,12 @@ class DumpsysBatteryDaily(AndroidExtraction): results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: - return { - "timestamp": record["from"], - "module": self.__class__.__name__, - "event": "battery_daily", - "data": f"Recorded update of package {record['package_name']} " - f"with vers {record['vers']}", - } - - 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 --daily") self._adb_disconnect() - self.results = parse_dumpsys_battery_daily(output) + self.parse(output) self.log.info( "Extracted %d records from battery daily stats", len(self.results) diff --git a/mvt/android/modules/androidqf/dumpsys_battery_daily.py b/mvt/android/modules/androidqf/dumpsys_battery_daily.py new file mode 100644 index 0000000..8a8386c --- /dev/null +++ b/mvt/android/modules/androidqf/dumpsys_battery_daily.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_daily import DumpsysBatteryDailyArtifact + +from .base import AndroidQFModule + + +class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, 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_daily.py b/mvt/android/modules/bugreport/battery_daily.py index 93e5ba3..5d45640 100644 --- a/mvt/android/modules/bugreport/battery_daily.py +++ b/mvt/android/modules/bugreport/battery_daily.py @@ -4,14 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional -from mvt.android.parsers import parse_dumpsys_battery_daily +from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact from .base import BugReportModule -class BatteryDaily(BugReportModule): +class BatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule): """This module extracts records from battery daily updates.""" def __init__( @@ -32,26 +32,6 @@ class BatteryDaily(BugReportModule): results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: - return { - "timestamp": record["from"], - "module": self.__class__.__name__, - "event": "battery_daily", - "data": f"Recorded update of package {record['package_name']} " - f"with vers {record['vers']}", - } - - 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: @@ -61,30 +41,9 @@ class BatteryDaily(BugReportModule): ) return - lines = [] - in_batterystats = False - in_daily = False - for line in content.decode(errors="ignore").splitlines(): - if line.strip() == "DUMP OF SERVICE batterystats:": - in_batterystats = True - continue - - if not in_batterystats: - continue - - if line.strip() == "Daily stats:": - lines.append(line) - in_daily = True - continue - - if not in_daily: - continue - - if line.strip() == "": - break - - lines.append(line) - - self.results = parse_dumpsys_battery_daily("\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 daily stats", len(self.results)) diff --git a/mvt/android/parsers/__init__.py b/mvt/android/parsers/__init__.py index dd1afcb..c88fca2 100644 --- a/mvt/android/parsers/__init__.py +++ b/mvt/android/parsers/__init__.py @@ -4,7 +4,6 @@ # https://license.mvt.re/1.1/ from .dumpsys import ( - parse_dumpsys_battery_daily, parse_dumpsys_battery_history, parse_dumpsys_receiver_resolver_table, ) diff --git a/mvt/android/parsers/dumpsys.py b/mvt/android/parsers/dumpsys.py index 0b4b156..8491ccf 100644 --- a/mvt/android/parsers/dumpsys.py +++ b/mvt/android/parsers/dumpsys.py @@ -7,54 +7,6 @@ import re from typing import Any, Dict, List -def parse_dumpsys_battery_daily(output: str) -> list: - results = [] - daily = None - daily_updates = [] - for line in output.splitlines(): - 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, - } - ) - - if len(daily_updates) > 0: - results.extend(daily_updates) - - return results - - def parse_dumpsys_battery_history(output: str) -> List[Dict[str, Any]]: results = [] diff --git a/tests/android/test_artifact_dumpsys_battery_daily.py b/tests/android/test_artifact_dumpsys_battery_daily.py new file mode 100644 index 0000000..891b4c8 --- /dev/null +++ b/tests/android/test_artifact_dumpsys_battery_daily.py @@ -0,0 +1,37 @@ +# 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_daily import DumpsysBatteryDailyArtifact +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysBatteryDailyArtifact: + def test_parsing(self): + dba = DumpsysBatteryDailyArtifact() + 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) == 3 + + def test_ioc_check(self, indicator_file): + dba = DumpsysBatteryDailyArtifact() + 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.facebook.system") + dba.indicators = ind + assert len(dba.detected) == 0 + dba.check_indicators() + assert len(dba.detected) == 1 diff --git a/tests/android_androidqf/test_dumpsys_battery_daily.py b/tests/android_androidqf/test_dumpsys_battery_daily.py new file mode 100644 index 0000000..7d4ed01 --- /dev/null +++ b/tests/android_androidqf/test_dumpsys_battery_daily.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_daily import DumpsysBatteryDaily +from mvt.common.module import run_module + +from ..utils import get_android_androidqf, list_files + + +class TestDumpsysBatteryDailyModule: + def test_parsing(self): + data_path = get_android_androidqf() + m = DumpsysBatteryDaily(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) == 3 + assert len(m.timeline) == 3 + assert len(m.detected) == 0 diff --git a/tests/artifacts/android_data/dumpsys_battery.txt b/tests/artifacts/android_data/dumpsys_battery.txt index 29568ed..5518f2d 100644 --- a/tests/artifacts/android_data/dumpsys_battery.txt +++ b/tests/artifacts/android_data/dumpsys_battery.txt @@ -9,3 +9,52 @@ Battery History (0% used, 2720 used of 4096KB, 31 strings using 2694): +2d23h22m24s588ms (2) 079 +usb_data +top=u0a44:"com.sec.android.app.launcher" +Daily stats: + Current start time: 2022-08-17-01-15-45 + Next min deadline: 2022-08-18-01-00-00 + Next max deadline: 2022-08-18-03-00-00 + Current daily discharge step durations: + #0: +3h32m16s12ms to 96 (power-save-off) + #1: +2h44m44s14ms to 97 (screen-off, power-save-off, device-idle-on) + #2: +2h0m41s988ms to 98 (screen-off, power-save-off, device-idle-on) + Discharge total time: 11d 12h 30m 0s 400ms (from 3 steps) + Discharge screen off time: 9d 21h 51m 40s 100ms (from 2 steps) + Discharge screen off device idle time: 9d 21h 51m 40s 100ms (from 2 steps) + Current daily charge step durations: + #0: +5m4s541ms to 99 (screen-off, power-save-off, device-idle-off) + #1: +3m33s300ms to 98 (screen-off, power-save-off, device-idle-off) + Charge total time: 7h 11m 32s 0ms (from 2 steps) + Charge screen off time: 7h 11m 32s 0ms (from 2 steps) + Daily from 2022-08-16-15-56-39 to 2022-08-17-01-15-45: + Charge step durations: + #0: +5m15s53ms to 100 (screen-off, power-save-off, device-idle-off) + #1: +5m35s358ms to 99 (screen-off, power-save-off, device-idle-off) + #2: +3m43s598ms to 98 (screen-off, power-save-off, device-idle-off) + #3: +3m33s400ms to 97 (screen-off, power-save-off, device-idle-off) + #4: +2m32s364ms to 96 (screen-off, power-save-off, device-idle-off) + #5: +3m53s485ms to 95 (screen-off, power-save-off, device-idle-off) + #6: +3m43s317ms to 94 (screen-off, power-save-off, device-idle-off) + #7: +3m13s27ms to 93 (screen-off, power-save-off, device-idle-off) + #8: +1h9m49s978ms to 92 (power-save-off, device-idle-off) + #9: +3m43s682ms to 92 (screen-off, power-save-off, device-idle-off) + #10: +6m15s588ms to 91 (screen-off, power-save-off, device-idle-off) + Package changes: + Update com.facebook.services vers=385230290 + Update com.facebook.services vers=385230290 + Update com.facebook.services vers=385230290 + Update com.facebook.services vers=385230290 + Update com.facebook.services vers=385230290 + Update com.facebook.services vers=385230290 + Update com.facebook.katana vers=315814651 + Update com.facebook.katana vers=315814651 + Update com.facebook.katana vers=315814651 + Update com.facebook.katana vers=315814651 + Update com.facebook.katana vers=315814651 + Update com.facebook.katana vers=315814651 + Update com.facebook.system vers=385230279 + Update com.facebook.system vers=385230279 + Update com.facebook.system vers=385230279 + Update com.facebook.system vers=385230279 + Update com.facebook.system vers=385230279 + Update com.facebook.system vers=385230279 + diff --git a/tests/artifacts/androidqf/dumpsys.txt b/tests/artifacts/androidqf/dumpsys.txt index 0b409fc..eaafabc 100644 --- a/tests/artifacts/androidqf/dumpsys.txt +++ b/tests/artifacts/androidqf/dumpsys.txt @@ -277,3 +277,87 @@ Connection pool for /data/user/0/com.sec.android.inputmethod/databases/StickerRe 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 +------------------------------------------------------------------------------- +DUMP OF SERVICE batterystats: +Battery History (0% used, 11KB used of 4096KB, 79 strings using 9632): + 0 (19) RESET:TIME: 2023-07-27-12-34-18 + 0 (2) 100 c0100024 status=discharging health=good plug=none temp=260 volt=4345 current=226 ap_temp=27 -nr_connected -wifi_ap -otg misc_event=0x0 online=1 current_event=0x0 txshare_event=0x0 charge=3000 modemRailChargemAh=0 wifiRailChargemAh=0 +running +wake_lock +screen phone_signal_strength=great brightness=bright +wifi_running +wifi +usb_data wifi_signal_strength=3 wifi_suppl=disconn +ble_scan top=1000:"com.wssyncmldm" + 0 (2) 100 c0100024 user=0:"0" + 0 (2) 100 c0100024 userfg=0:"0" + +343ms (3) 100 80000024 -wake_lock -screen -usb_data stats=0:"get-stats" + +1s235ms (4) 100 c0000020 +wake_lock=1000:"ActivityManager-Sleep" brightness=dark stats=0:"screen-state" + +1s314ms (1) 100 80000020 -wake_lock + +1s320ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s321ms (1) 100 80000020 -wake_lock + +1s321ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s332ms (1) 100 80000020 -wake_lock + +1s332ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s334ms (1) 100 80000020 -wake_lock + +1s441ms (2) 100 c0000020 +wake_lock=1000:"startDream" + +1s751ms (1) 100 80000020 -wake_lock + +1s809ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s811ms (1) 100 80000020 -wake_lock + +1s811ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s821ms (1) 100 80000020 -wake_lock + +1s821ms (2) 100 c0000020 +wake_lock=1001:"*telephony-radio*" + +1s823ms (1) 100 80000020 -wake_lock -ble_scan + +2s042ms (2) 100 c0000020 +wake_lock=u0a12:"Wakeful StateMachine: GeofencerStateMachine" + +2s044ms (1) 100 80000020 -wake_lock + +2s050ms (2) 100 c0000020 +wake_lock=u0a12:"NlpWakeLock" + +Daily stats: + Current start time: 2023-07-27-02-02-56 + Next min deadline: 2023-07-28-01-00-00 + Next max deadline: 2023-07-28-03-00-00 + Current daily discharge step durations: + #0: +2h44m59s971ms to 98 (screen-off, power-save-off, device-idle-on) + Discharge total time: 11d 10h 59m 57s 100ms (from 1 steps) + Discharge screen off time: 11d 10h 59m 57s 100ms (from 1 steps) + Discharge screen off device idle time: 11d 10h 59m 57s 100ms (from 1 steps) + Current daily charge step durations: + #0: +2m32s269ms to 100 (power-save-off, device-idle-off) + Charge total time: 4h 13m 46s 900ms (from 1 steps) + Daily from 2023-07-26-03-02-02 to 2023-07-27-02-02-56: + Discharge step durations: + #0: +2h21m35s4ms to 75 (screen-off, power-save-off) + #1: +2h19m0s999ms to 76 (screen-off, power-save-off) + #2: +1h46m26s999ms to 77 (screen-off, power-save-off) + #3: +2h24m32s6ms to 78 (screen-off, power-save-off, device-idle-on) + #4: +2h44m58s966ms to 79 (screen-off, power-save-off, device-idle-on) + Discharge total time: 9d 16h 11m 19s 400ms (from 5 steps) + Discharge screen off time: 9d 16h 11m 19s 400ms (from 5 steps) + Discharge screen off device idle time: 10d 17h 55m 48s 600ms (from 2 steps) + Charge step durations: + #0: +5m45s118ms to 100 (screen-off, power-save-off, device-idle-off) + #1: +1m0s998ms to 99 (screen-off, power-save-off, device-idle-off) + #2: +2m1s894ms to 98 (screen-off, power-save-off, device-idle-off) + #3: +1m0s973ms to 97 (screen-off, power-save-off, device-idle-off) + #4: +3m33s239ms to 96 (screen-off, power-save-off, device-idle-off) + Charge step durations: + #0: +30s531ms to 100 (screen-off, power-save-off, device-idle-off) + #1: +30s527ms to 99 (screen-off, power-save-off, device-idle-off) + #2: +30s571ms to 98 (screen-off, power-save-off, device-idle-off) + #3: +1m1s53ms to 97 (screen-off, power-save-off, device-idle-off) + #4: +30s580ms to 96 (screen-off, power-save-off, device-idle-off) + #5: +30s568ms to 95 (screen-off, power-save-off, device-idle-off) + #6: +20s407ms to 94 (screen-off, power-save-off, device-idle-off) + #7: +7m16s300ms to 93 (screen-off, power-save-off, device-idle-off) + #8: +5m55s313ms to 92 (screen-off, power-save-off, device-idle-off) + #9: +6m35s856ms to 91 (screen-off, power-save-off, device-idle-off) + #10: +4m17s981ms to 90 (screen-off, power-save-off, device-idle-off) + #11: +3m43s342ms to 89 (screen-off, power-save-off, device-idle-off) + Charge total time: 4h 24m 18s 500ms (from 12 steps) + Charge screen off time: 4h 24m 18s 500ms (from 12 steps) + Package changes: + Update com.google.android.gm vers=63983425 + Update com.google.android.gm vers=63983425 + Update com.google.android.gm vers=63983425 + Update com.google.android.gm vers=63983425 + Update org.mozilla.firefox vers=2015962857 + Update org.mozilla.firefox vers=2015962857 + Update org.mozilla.firefox vers=2015962857 + Update org.mozilla.firefox vers=2015962857 + Update com.google.android.projection.gearhead vers=99632623 + Update com.google.android.projection.gearhead vers=99632623 + Update com.google.android.projection.gearhead vers=99632623 + diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index 9e323cc..76fc730 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"] - == "c6be3ada77674f5bb9750d24e84b9b7ccf8db0cd4a896d9c17f9456eeab4bd0b" + == "009f9eaa04658acdd179b463e05e1ea1fffea132e6e7ee556f0c385ee69a0811" )