diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index ff31ac1091..65494dff9e 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -1471,3 +1471,33 @@ def test_windows_specific_virtmap(self, volatility, python): ) for expected_row in expected_rows: assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsShutdown: + def test_windows_lastshutdown(self, volatility, python, image): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.shutdown", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + + + assert rc == 0 + json_out = json.loads(out) + assert isinstance(json_out, list) + assert len(json_out) > 0 + expected_keys = {"Registry Key", "Last Shutdown Time"} + + #Here I controll for every row in the output if there are + #the registry key and the value other than controlling + #if the output actually exists and is the right output to shutdown plugin + for row in json_out: + assert isinstance(row, dict) + assert expected_keys.issubset(row.keys()) + assert row["Registry Key"] + assert row["Last Shutdown Time"] + assert "ControlSet" in row["Registry Key"] diff --git a/volatility3/framework/plugins/windows/registry/shutdown.py b/volatility3/framework/plugins/windows/registry/shutdown.py new file mode 100644 index 0000000000..e5346574e2 --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/shutdown.py @@ -0,0 +1,101 @@ +import logging +import struct +import datetime +from typing import Iterable, Optional + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry as registry_layer +from volatility3.framework.symbols.windows.extensions import registry +from volatility3.plugins.windows.registry import hivelist +from volatility3.framework.renderers import conversion + +vollog = logging.getLogger(__name__) + + +class LastShutdown(interfaces.plugins.PluginInterface): + """Extract last Windows shutdown time from registry""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hivelist", + component=hivelist.HiveList, + version=(2, 0, 0), + ), + ] + + @classmethod + def get_key( + cls, hive: registry_layer.RegistryHive, key_path: str + ) -> Optional["registry.CM_KEY_NODE"]: + try: + return hive.get_key(key_path) + except Exception: + return None + + def _generator(self, syshive: registry_layer.RegistryHive) -> Iterable: + + if not syshive: + return + + # Windows systems typically maintain a small number of ControlSets + # (commonly 1–3, but occasionally more in recovery scenarios). + # We iterate over a bounded range to ensure we can recover shutdown + # time even if the active ControlSet cannot be determined via + # Select\\Current or if registry data is partially corrupted. + for i in range(1, 5): + + key_path = f"ControlSet{i:03}\\Control\\Windows" + + key = self.get_key(syshive, key_path) + + if not key: + continue + + for v in key.get_values(): + + if v.get_name() == "ShutdownTime": + + try: + data = syshive.read(v.Data + 4, v.DataLength) + except exceptions.InvalidAddressException: + continue + + if not data or len(data) < 8: + continue + + filetime = struct.unpack("