diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index 7d9d86fb0..a667eed03 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -28,7 +28,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pyinstaller - pip install -e .[full,cloud] + pip install -e .[full,cloud,arrow] - name: Pyinstall executable run: | diff --git a/README.md b/README.md index cf735fc8d..8c5332a90 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,17 @@ pip install -e ".[dev]" Symbol table packs for the various operating systems are available for download at: + + The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: + + Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file). @@ -92,7 +96,7 @@ The latest generated copy of the documentation can be found at: Optional[str]: logger.info("Download PDB file...") file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) diff --git a/development/stock-linux-json.py b/development/stock-linux-json.py index 967cc7e18..713283e66 100644 --- a/development/stock-linux-json.py +++ b/development/stock-linux-json.py @@ -13,7 +13,6 @@ DWARF2JSON = "./dwarf2json" class Downloader: - def __init__(self, url_lists: List[List[str]]) -> None: self.url_lists = url_lists diff --git a/doc/source/conf.py b/doc/source/conf.py index ca868aff1..395863dc4 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,4 @@ -# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2026 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # # @@ -16,11 +16,10 @@ import os import sys +from importlib.util import find_spec import sphinx.ext.apidoc -from importlib.util import find_spec - def setup(app): volatility_directory = os.path.abspath( @@ -167,7 +166,7 @@ master_doc = "index" # General information about the project. project = "Volatility 3" -copyright = "2012-2025, Volatility Foundation" +copyright = "2012-2026, Volatility Foundation" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/pyproject.toml b/pyproject.toml index fca8ea436..290a31abd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ dev = [ "types-jsonschema>=4.23.0,<5", ] +arrow = ["pyarrow>=17.0.0"] + test = [ "volatility3[dev]", "pytest>=8.3.3,<9", @@ -52,7 +54,7 @@ test = [ docs = [ "volatility3[dev]", "sphinx>=4.0.0,<9", - "sphinx-autodoc-typehints>=3.0.0,<4", + "sphinx-autodoc-typehints>=3.0.0,<4; python_version >= '3.11'", "sphinx-rtd-theme>=3.0.1,<4", ] diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py index 15d7f79a6..6f8180000 100644 --- a/test/plugins/windows/test_scheduled_tasks.py +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -4,7 +4,7 @@ import traceback import unittest sys.path.insert(0, "../../volatility3") -from volatility3.plugins.windows import scheduled_tasks +from volatility3.plugins.windows.registry import scheduled_tasks class TestActionsDecoding(unittest.TestCase): @@ -101,233 +101,1815 @@ class TestTriggersDecoding(unittest.TestCase): "1808B", # fmt: off *[ - 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, - 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, - 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, - 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, - 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, - 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, - 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, - 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, - 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, - 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, - 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, - 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, - 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, - 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, - 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, - 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, - 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, - 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, - 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, - 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, - 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, - 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, - 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ] + 0x17, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x38, + 0x21, + 0x41, + 0x42, + 0x48, + 0x48, + 0x48, + 0x48, + 0xA0, + 0x12, + 0xA0, + 0xA4, + 0x48, + 0x48, + 0x48, + 0x48, + 0x0E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x41, + 0x00, + 0x75, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x80, + 0xF4, + 0x03, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xDD, + 0xDD, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x07, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x09, + 0x00, + 0x80, + 0x48, + 0x11, + 0xF8, + 0x36, + 0x1A, + 0xDB, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x2E, + 0xE2, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xC2, + 0x31, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xAA, + 0xAA, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xEE, + 0xEE, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xCC, + 0xCC, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x65, + 0x00, + 0x78, + 0x00, + 0x65, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x53, + 0x00, + 0x74, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x20, + 0x00, + 0x49, + 0x00, + 0x64, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x30, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x2A, + 0x00, + 0x5B, + 0x00, + 0x53, + 0x00, + 0x79, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x6D, + 0x00, + 0x5B, + 0x00, + 0x45, + 0x00, + 0x76, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x49, + 0x00, + 0x44, + 0x00, + 0x3D, + 0x00, + 0x32, + 0x00, + 0x5D, + 0x00, + 0x5D, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x88, + 0x88, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x03, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x04, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ], # fmt: on ) triggers = scheduled_tasks.TriggerSet.decode(buf) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 431461b6d..ff31ac109 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -4,6 +4,7 @@ import json import os import shutil import tempfile + from test import WindowsSamples, test_volatility @@ -437,7 +438,7 @@ class TestWindowsVadyarascan: class TestWindowsAmcache: def test_windows_generic_amcache(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( - "windows.amcache.Amcache", + "windows.registry.amcache.Amcache", image, volatility, python, @@ -492,7 +493,7 @@ class TestWindowsBigPools: # class TestWindowsCachedump: # def test_windows_generic_cachedump(self, volatility, python, image): # rc, out, _err = test_volatility.runvol_plugin( -# "windows.cachedump.Cachedump", +# "windows.registry.cachedump.Cachedump", # image, # volatility, # python, @@ -820,7 +821,7 @@ class TestWindowsLsadump: def test_windows_specific_lsadump(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path rc, out, _err = test_volatility.runvol_plugin( - "windows.lsadump.Lsadump", + "windows.registry.lsadump.Lsadump", image, volatility, python, diff --git a/test/renderers/__init__.py b/test/renderers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/renderers/test_parquet_renderers.py b/test/renderers/test_parquet_renderers.py new file mode 100644 index 000000000..c9c07b2db --- /dev/null +++ b/test/renderers/test_parquet_renderers.py @@ -0,0 +1,151 @@ +import io +import pytest +from abc import ABC, abstractmethod +from test import test_volatility + +HAS_PYARROW = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + import pyarrow.compute as pc + + HAS_PYARROW = True +except ImportError: + # The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue + pass + + +@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed") +class TestArrowRendererBase(ABC): + """Base class for testing Arrow-based renderers. + + Re-implements Windows and Linux plugin tests using PyArrow operations + instead of text-based assertions. + """ + + renderer_format = None # Override in subclasses + + @abstractmethod + def _get_table_from_output(self, output_bytes) -> "pa.Table": + """Parse output bytes into Arrow table. Override in subclasses.""" + + def test_windows_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "system" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "csrss.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "svchost.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) + + def test_linux_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + init_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "init") + ) + systemd_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "systemd") + ) + assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0) + + assert ( + table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "watchdog") + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) + + def test_windows_generic_handles(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.handles.Handles", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + pluginargs=("--pid", "4"), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 500 + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("Name")), "machine\\system" + ) + ).num_rows + > 0 + ) + + def test_linux_generic_lsof(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.lsof.Lsof", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 35 + + +class TestParquetRenderer(TestArrowRendererBase): + renderer_format = "parquet" + + def _get_table_from_output(self, output_bytes): + return pq.read_table(io.BytesIO(output_bytes)) + + +class TestArrowRenderer(TestArrowRendererBase): + renderer_format = "arrow" + + def _get_table_from_output(self, output_bytes): + return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all() diff --git a/test/volatility3_code_analysis.py b/test/volatility3_code_analysis.py index 100ad3074..43fed459b 100644 --- a/test/volatility3_code_analysis.py +++ b/test/volatility3_code_analysis.py @@ -82,7 +82,6 @@ class CodeViolation(metaclass=abc.ABCMeta): class UnrequiredVersionableUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -107,7 +106,6 @@ class UnrequiredVersionableUsage(CodeViolation): class DirectVolatilityImportUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -174,8 +172,11 @@ class ModuleVisitor(NodeVisitor): """ if ( node.module - and node.module.startswith("volatility3.") # Give a pass to volatility3 module - and node.module != "volatility3.framework.constants._version" # make an exception for this + and node.module.startswith( + "volatility3." + ) # Give a pass to volatility3 module + and node.module + != "volatility3.framework.constants._version" # make an exception for this ): for name in node.names: try: @@ -204,7 +205,6 @@ class ModuleVisitor(NodeVisitor): def enter_ImportFrom(self, node: ast.ImportFrom): self._check_vol3_import_from(node) - def enter_ClassDef(self, node: ast.ClassDef) -> Any: logger.debug("Entering class %s", node.name) clazz = None diff --git a/volatility3/__init__.py b/volatility3/__init__.py index 94a6721e1..9867f5d94 100644 --- a/volatility3/__init__.py +++ b/volatility3/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 - An open-source memory forensics framework""" + import inspect import sys from importlib import abc diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index cc97c4fcb..15e6cc7b4 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -10,6 +10,7 @@ User interfaces make use of the framework to: * run the plugin * display the results """ + import argparse import inspect import io @@ -106,13 +107,6 @@ class CommandLine: volatility3.framework.require_interface_version(2, 0, 0) - renderers = dict( - [ - (x.name.lower(), x) - for x in framework.class_subclasses(text_renderer.CLIRenderer) - ] - ) - # Load up system defaults delayed_logs, default_config = self.load_system_defaults("vol.json") @@ -193,14 +187,6 @@ class CommandLine: default=False, action="store_true", ) - parser.add_argument( - "-r", - "--renderer", - metavar="RENDERER", - help=f"Determines how to render the output ({', '.join(list(renderers))})", - default="quick", - choices=list(renderers), - ) parser.add_argument( "-f", "--file", @@ -270,11 +256,6 @@ class CommandLine: known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"] partial_args, _ = parser.parse_known_args(known_args) - banner_output = sys.stdout - if renderers[partial_args.renderer].structured_output: - banner_output = sys.stderr - banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") - ### Start up logging if partial_args.log: file_logger = logging.FileHandler(partial_args.log) @@ -346,6 +327,24 @@ class CommandLine: plugin_list = framework.list_plugins() + # Discover renderers after plugin directories are loaded + # This allows custom renderers to be found in plugin directories + renderers = dict( + [ + (x.name.lower(), x) + for x in framework.class_subclasses(text_renderer.CLIRenderer) + ] + ) + + parser.add_argument( + "-r", + "--renderer", + metavar="RENDERER", + help=f"Determines how to render the output ({', '.join(list(renderers))})", + default="quick", + choices=list(renderers), + ) + seen_automagics = set() chosen_configurables_list = {} for amagic in automagics: @@ -392,6 +391,13 @@ class CommandLine: # before all the plugins have been added argcomplete.autocomplete(parser) args = parser.parse_args() + + # Display banner - redirect to stderr if using structured output + banner_output = sys.stdout + if renderers[args.renderer].structured_output: + banner_output = sys.stderr + banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") + if args.plugin is None: parser.error( f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options" @@ -453,8 +459,9 @@ class CommandLine: raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value @@ -569,7 +576,7 @@ class CommandLine: delayed_logs.append( ( logging.DEBUG, - f"Loaded configuration: {json.dumps(result, indent = 2, sort_keys = True)}", + f"Loaded configuration: {json.dumps(result, indent=2, sort_keys=True)}", ) ) return delayed_logs, result @@ -758,7 +765,7 @@ class CommandLine: constants.LOGLEVEL_VVVV, ] ): - logging.addLevelName(level_value, f"DETAIL {level+1}") + logging.addLevelName(level_value, f"DETAIL {level + 1}") def file_handler_class_factory(self, direct=True): output_dir = self.output_dir diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1e437a0be..d55201371 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -278,7 +278,6 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - name = "quick" def get_render_options(self): @@ -348,7 +347,6 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - name = "csv" structured_output = True diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 0affe5d59..5bc29f220 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -344,8 +344,9 @@ class VolShell(cli.CommandLine): raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index ace4b2119..2a8039ff2 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -469,7 +469,7 @@ class Volshell(interfaces.plugins.PluginInterface): and dereference_count < MAX_DEREFERENCE_COUNT ): # before defreerencing the pointer, show it's information - print(f'{" " * dereference_count}{self._display_simple_type(volobject)}') + print(f"{' ' * dereference_count}{self._display_simple_type(volobject)}") # check that we can follow the pointer before dereferencing and do not # attempt to follow null pointers. @@ -486,7 +486,7 @@ class Volshell(interfaces.plugins.PluginInterface): if hasattr(volobject.vol, "members"): # display the header for this object, if the original object was just a type string, display the type information - struct_header = f'{" " * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)' + struct_header = f"{' ' * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)" if isinstance(object, str) and offset is None: suffix = ":" else: @@ -523,7 +523,7 @@ class Volshell(interfaces.plugins.PluginInterface): len_typename = len(member_type_name) if len(member_type_name) > MAX_TYPENAME_DISPLAY_LENGTH: len_typename = MAX_TYPENAME_DISPLAY_LENGTH - member_type_name = f"{member_type_name[:len_typename - 3]}..." + member_type_name = f"{member_type_name[: len_typename - 3]}..." if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 1c899f434..6943598cf 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 framework.""" + # Check the python version to ensure it's suitable import glob import sys diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 55b9b81e1..c03db7582 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -7,6 +7,7 @@ from loaded PE files. This module contains a standalone scanner, and also a :class:`~volatility3.framework.interfaces.layers.ScannerInterface` based scanner for use within the framework by calling :func:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan`. """ + import contextlib import logging import math @@ -449,7 +450,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): while (kernel_base + 0x2000000) > kernel_hint: for i in range(0, 0x200000, 0x1000): valid_kernel = self.check_kernel_offset( - context, vlayer, kernel_base, progress_callback + context, vlayer, kernel_base + i, progress_callback ) if valid_kernel: return valid_kernel diff --git a/volatility3/framework/automagic/stacker.py b/volatility3/framework/automagic/stacker.py index 596864264..2e9875086 100644 --- a/volatility3/framework/automagic/stacker.py +++ b/volatility3/framework/automagic/stacker.py @@ -153,8 +153,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface): constructor(context, config_path, requirement) # Stash the changed config items - self._cached = context.config.get(path, None), context.config.branch( - path + self._cached = ( + context.config.get(path, None), + context.config.branch(path), ) vollog.debug( f"physical_layer maximum_address: {physical_layer.maximum_address}" diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 52296f5ad..7d56b01b3 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -26,6 +26,7 @@ The self-referential indices for older versions of windows are listed below: | x64 | 0x1ED | +--------------+-------+ """ + import logging import struct from typing import Generator, Iterable, List, Optional, Tuple, Type diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 6d978e2a9..b1cc716e5 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -8,6 +8,7 @@ These requirement types allow plugins to request simple information types (such as strings, integers, etc) as well as indicating what they expect to be in the context (such as particular layers or symboltables). """ + import abc import logging import os diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 07b9e45ec..7f71c277e 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 26 # Number of changes that only add to the interface -VERSION_PATCH = 2 # Number of changes that do not change the interface +VERSION_MINOR = 27 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index f45bed926..3f8c52b43 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,6 +5,7 @@ Linux-specific values that aren't found in debug symbols """ + import enum from dataclasses import dataclass diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 32d33f657..125f41f8a 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -8,6 +8,7 @@ This has been made an object to allow quick swapping and changing of contexts, to allow a plugin to act on multiple different contexts without them interfering with each other. """ + import functools import hashlib import logging diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 90fa9bce0..581647922 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -79,7 +79,7 @@ def deprecated_method( "This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.", ) - deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated and will be removed in the first release after {removal_date}, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}" + deprecation_msg = f'Method "{deprecated_func.__module__ + "." + deprecated_func.__qualname__}" is deprecated and will be removed in the first release after {removal_date}, use "{replacement.__module__ + "." + replacement.__qualname__}" instead. {additional_information}' warnings.warn(deprecation_msg, FutureWarning) # Return the wrapped function with its original arguments return deprecated_func(*args, **kwargs) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 34b41727a..3b70c5c29 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -8,6 +8,7 @@ space or symbol tables, and by layers when an address is invalid. The :class:`PagedInvalidAddressException` contains information about the size of the invalid page. """ + from typing import Callable, Dict, Optional, Tuple from volatility3.framework import interfaces @@ -161,4 +162,4 @@ class VersionMismatchException(VolatilityException): self.failure_reason = failure_reason def __str__(self): - return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet." + return f"{self.source_component.__module__ + '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__ + '.' + self.target_component.__name__} {self.target_component.version} unmet." diff --git a/volatility3/framework/interfaces/automagic.py b/volatility3/framework/interfaces/automagic.py index 4ac386fc0..744a33bab 100644 --- a/volatility3/framework/interfaces/automagic.py +++ b/volatility3/framework/interfaces/automagic.py @@ -7,6 +7,7 @@ runs. Automagic objects attempt to automatically fill configuration values that a user has not filled. """ + import logging from abc import ABCMeta from typing import Any, List, Optional, Tuple, Type, Union diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 4f863898e..0b71e4cb2 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -11,6 +11,7 @@ convenience functions, most notably the object constructor function, `object`, which will construct a symbol on a layer at a particular offset. """ + import collections import copy from abc import ABCMeta, abstractmethod diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index 6c4e4b419..2e328124d 100644 --- a/volatility3/framework/interfaces/layers.py +++ b/volatility3/framework/interfaces/layers.py @@ -6,6 +6,7 @@ One layer may combine other layers, map data based on the data itself, or map a procedure (such as decryption) across another layer of data. """ + import collections.abc import functools import logging diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 2d8024465..1228c4d0a 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -3,12 +3,14 @@ # """Objects are the core of volatility, and provide pythonic access to interpreted values of data from a layer.""" + import abc import collections import collections.abc import contextlib +import dataclasses import logging -from typing import Any, Dict, List, Mapping, NamedTuple, Optional +from typing import Any, Dict, List, Mapping, Optional from volatility3.framework import constants, interfaces @@ -52,7 +54,8 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(NamedTuple): +@dataclasses.dataclass +class ObjectInformation: """Contains common information useful/pertinent only to an individual object (like an instance) @@ -71,12 +74,12 @@ class ObjectInformation(NamedTuple): size: Optional[int] = None def __getitem__(self, key): - if key in self._fields: + if key in self: return getattr(self, key) - raise KeyError(f"NamedTuple does not have a key {key}") + raise KeyError(f"No {key} present in ObjectInformation") def __contains__(self, key): - return key in self._fields + return key in [field.name for field in dataclasses.fields(self)] class ObjectInterface(metaclass=abc.ABCMeta): diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 925be72c9..1159fd290 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Symbols provide structural information about a set of bytes.""" + import bisect import collections.abc from abc import ABC, abstractmethod diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index 2e5572192..7c052f70a 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -6,6 +6,7 @@ The user of the file doesn't have to worry about the compression, but random access is not allowed.""" + import ctypes import logging import struct diff --git a/volatility3/framework/layers/codecs/__init__.py b/volatility3/framework/layers/codecs/__init__.py index e019bcbcd..5b9f2602b 100644 --- a/volatility3/framework/layers/codecs/__init__.py +++ b/volatility3/framework/layers/codecs/__init__.py @@ -2,7 +2,4 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""Codecs used for encoding or decoding data should live here - - -""" +"""Codecs used for encoding or decoding data should live here""" diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 696c33353..848580004 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -315,7 +315,13 @@ class Intel(linear.LinearlyMappedLayer): ): # The block isn't contiguous if stashed_offset is not None: - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) # Update all the stashed values after output stashed_offset = offset stashed_mapped_offset = mapped_offset @@ -334,7 +340,13 @@ class Intel(linear.LinearlyMappedLayer): and stashed_mapped_size is not None and stashed_map_layer is not None ): - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) def _mapping( self, offset: int, length: int, ignore_errors: bool = False diff --git a/volatility3/framework/layers/msf.py b/volatility3/framework/layers/msf.py index 2b4fae963..a7bf6466e 100644 --- a/volatility3/framework/layers/msf.py +++ b/volatility3/framework/layers/msf.py @@ -234,9 +234,13 @@ class PdbMSFStream(linear.LinearlyMappedLayer): layer_name=self.name, invalid_address=offset + returned ) else: - yield offset + returned, chunk_size, ( - self._pages[page] * page_size - ) + page_position, chunk_size, self._base_layer + yield ( + offset + returned, + chunk_size, + (self._pages[page] * page_size) + page_position, + chunk_size, + self._base_layer, + ) returned += chunk_size length -= chunk_size diff --git a/volatility3/framework/layers/resources.py b/volatility3/framework/layers/resources.py index 6121c2cff..66fb617af 100644 --- a/volatility3/framework/layers/resources.py +++ b/volatility3/framework/layers/resources.py @@ -305,8 +305,9 @@ class JarHandler(VolatilityHandler): def default_open(req: urllib.request.Request) -> Optional[Any]: """Handles the request if it's the jar scheme.""" if req.type == "jar": - subscheme, remainder = req.full_url.split(":")[1], ":".join( - req.full_url.split(":")[2:] + subscheme, remainder = ( + req.full_url.split(":")[1], + ":".join(req.full_url.split(":")[2:]), ) if subscheme != "file": vollog.log( diff --git a/volatility3/framework/layers/segmented.py b/volatility3/framework/layers/segmented.py index 9825ae15c..96b9618dc 100644 --- a/volatility3/framework/layers/segmented.py +++ b/volatility3/framework/layers/segmented.py @@ -129,7 +129,13 @@ class NonLinearlySegmentedLayer( return None # Crop it to the amount we need left chunk_size = min(size, length + offset - logical_offset) - yield logical_offset, chunk_size, mapped_offset, mapped_size, self._base_layer + yield ( + logical_offset, + chunk_size, + mapped_offset, + mapped_size, + self._base_layer, + ) current_offset += chunk_size # Terminate if we've gone (or reached) our required limit if current_offset >= offset + length: diff --git a/volatility3/framework/layers/vmware.py b/volatility3/framework/layers/vmware.py index 39fb21b63..5dc00f344 100644 --- a/volatility3/framework/layers/vmware.py +++ b/volatility3/framework/layers/vmware.py @@ -65,10 +65,10 @@ class VmwareLayer(segmented.SegmentedLayer): data = meta_layer.read(0, header_size) magic, unknown, groupCount = struct.unpack(self.header_structure, data) if magic not in [ - b"\xD0\xBE\xD2\xBE", - b"\xD1\xBA\xD1\xBA", - b"\xD2\xBE\xD2\xBE", - b"\xD3\xBE\xD3\xBE", + b"\xd0\xbe\xd2\xbe", + b"\xd1\xba\xd1\xba", + b"\xd2\xbe\xd2\xbe", + b"\xd3\xbe\xd3\xbe", ]: raise VmwareFormatException( self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}" diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index d4e6e2aa8..eea39206d 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -60,8 +60,9 @@ class Banners(interfaces.plugins.PluginInterface): not in b" #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~" ] if not failed: - yield format_hints.Hex(offset), str( - data, encoding="latin-1", errors="?" + yield ( + format_hints.Hex(offset), + str(data, encoding="latin-1", errors="?"), ) def run(self): diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 1c2ac52e9..10f391321 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -72,9 +72,12 @@ class IsfInfo(plugins.PluginInterface): for extension in constants.ISF_EXTENSIONS: # By ending with an extension (and therefore, not /), we should not return any directories if name.endswith(extension): - yield "jar:file:" + str( - pathlib.Path(base_name) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(base_name)) + + "!" + + name + ) else: for extension in constants.ISF_EXTENSIONS: diff --git a/volatility3/framework/plugins/linux/graphics/fbdev.py b/volatility3/framework/plugins/linux/graphics/fbdev.py index 1f9fd74b5..ebdbd1706 100644 --- a/volatility3/framework/plugins/linux/graphics/fbdev.py +++ b/volatility3/framework/plugins/linux/graphics/fbdev.py @@ -245,7 +245,6 @@ You can try using ffmpeg to decode the raw buffer. Example usage: return fb def _generator(self): - if not has_pil: vollog.error( "PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml." diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py index 348f3c13f..164fe62dd 100644 --- a/volatility3/framework/plugins/linux/ip.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -47,7 +47,17 @@ class Addr(plugins.PluginInterface): prefix_len = in_ifaddr.get_prefix_len() scope_type = in_ifaddr.get_scope_type() ip_addr = in_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip_addr, + prefix_len, + scope_type, + operational_state, + ) # Interface IPv6 Addresses inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev") @@ -55,7 +65,17 @@ class Addr(plugins.PluginInterface): prefix_len = inet6_ifaddr.get_prefix_len() scope_type = inet6_ifaddr.get_scope_type() ip6_addr = inet6_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ) def _enumerate_net_namespace_list(self): vmlinux = self.context.modules[self.config["kernel"]] @@ -82,16 +102,19 @@ class Addr(plugins.PluginInterface): scope_type, operational_state, ) in self._gather_net_dev_info(net_dev): - yield 0, ( - net_ns_id or renderers.NotAvailableValue(), - iface_ifindex, - iface_name, - mac_addr, - promisc, - ip6_addr, - prefix_len, - scope_type, - operational_state, + yield ( + 0, + ( + net_ns_id or renderers.NotAvailableValue(), + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ), ) def run(self): @@ -150,7 +173,16 @@ class Link(plugins.PluginInterface): ] flags_str = ",".join(flags_list) - yield net_ns_id or renderers.NotAvailableValue(), iface_name, mac_addr, operational_state, mtu, qdisc_name or renderers.NotAvailableValue(), qlen, flags_str + yield ( + net_ns_id or renderers.NotAvailableValue(), + iface_name, + mac_addr, + operational_state, + mtu, + qdisc_name or renderers.NotAvailableValue(), + qlen, + flags_str, + ) def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/linux/kmsg.py b/volatility3/framework/plugins/linux/kmsg.py index 1069a312f..ba02763d0 100644 --- a/volatility3/framework/plugins/linux/kmsg.py +++ b/volatility3/framework/plugins/linux/kmsg.py @@ -551,12 +551,15 @@ class Kmsg(interfaces.plugins.PluginInterface): for facility, level, timestamp, caller, line in ABCKmsg.run_all( context=self.context, config=self.config ): - yield 0, ( - facility, - level, - timestamp, - caller or renderers.NotAvailableValue(), - line, + yield ( + 0, + ( + facility, + level, + timestamp, + caller or renderers.NotAvailableValue(), + line, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index 8ed52e3b7..e0878647f 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -4,10 +4,10 @@ """A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import List, Iterable +from typing import Iterable, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins @@ -18,27 +18,41 @@ class Lsmod(plugins.PluginInterface): """Lists loaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 1) + _version = (3, 0, 3) - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = linux_utilities_modules.Modules.list_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str @@ -46,3 +60,18 @@ class Lsmod(plugins.PluginInterface): return linux_utilities_modules.Modules.list_modules( context, vmlinux_module_name ) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 283eabca0..75c2a1c2b 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 2) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -137,6 +137,12 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): element_type=int, optional=True, ), + requirements.BooleanRequirement( + name="files_only", + description="Include only file descriptors of type file", + optional=True, + default=False, + ), ] @classmethod @@ -145,6 +151,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): context: interfaces.context.ContextInterface, vmlinux_module_name: str, filter_func: Callable[[int], bool] = lambda _: False, + include_files_only: bool = False, ) -> Iterable[FDInternal]: """Enumerates open file descriptors in tasks @@ -167,16 +174,20 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0] fd_generator = linux.LinuxUtilities.files_descriptors_for_process( - context, linuxutils_symbol_table, task + context, linuxutils_symbol_table, task, files_only=include_files_only ) for fd_fields in fd_generator: yield FDInternal(task=task, fd_fields=fd_fields) - def _generator(self, pids, vmlinux_module_name): + def _generator(self, pids, vmlinux_module_name, include_files_only): filter_func = pslist.PsList.create_pid_filter(pids) + for fd_internal in self.list_fds( - self.context, vmlinux_module_name, filter_func=filter_func + self.context, + vmlinux_module_name, + filter_func=filter_func, + include_files_only=include_files_only, ): fd_user = fd_internal.to_user() yield (0, dataclasses.astuple(fd_user)) @@ -184,6 +195,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): def run(self): pids = self.config.get("pid", None) vmlinux_module_name = self.config["kernel"] + include_files_only = self.config.get("files_only") tree_grid_args = [ ("PID", int), @@ -201,7 +213,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", int), ] return renderers.TreeGrid( - tree_grid_args, self._generator(pids, vmlinux_module_name) + tree_grid_args, + self._generator( + pids, vmlinux_module_name, include_files_only=include_files_only + ), ) def generate_timeline(self): @@ -220,5 +235,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ) yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time - yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + fd_user.modification_time, + ) yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time diff --git a/volatility3/framework/plugins/linux/malware/check_afinfo.py b/volatility3/framework/plugins/linux/malware/check_afinfo.py index 47da21615..4ecf5f788 100644 --- a/volatility3/framework/plugins/linux/malware/check_afinfo.py +++ b/volatility3/framework/plugins/linux/malware/check_afinfo.py @@ -3,6 +3,7 @@ # """A module containing a plugin that verifies the operation function pointers of network protocols.""" + import logging from typing import List, Tuple, Generator diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py index 7805bbd8a..1834a616e 100644 --- a/volatility3/framework/plugins/linux/malware/check_modules.py +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -3,19 +3,18 @@ # import logging -from typing import List, Dict, Generator +from typing import Dict, Generator, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins vollog = logging.getLogger(__name__) -class Check_modules(plugins.PluginInterface): +class Check_modules(interfaces.plugins.PluginInterface): """Compares module list to sysfs info, if available""" _version = (3, 0, 1) @@ -23,7 +22,7 @@ class Check_modules(plugins.PluginInterface): @classmethod def compare_kset_and_lsmod( - cls, context: str, vmlinux_name: str + cls, context: interfaces.context.ContextInterface, vmlinux_name: str ) -> Generator[extensions.module, None, None]: kset_modules = linux_utilities_modules.Modules.get_kset_modules( context=context, vmlinux_name=vmlinux_name @@ -39,13 +38,16 @@ class Check_modules(plugins.PluginInterface): for mod_name in set(kset_modules.keys()).difference(lsmod_modules): yield kset_modules[mod_name] - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = compare_kset_and_lsmod @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), requirements.VersionRequirement( name="modules", component=linux_utilities_modules.Modules, @@ -54,17 +56,38 @@ class Check_modules(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_kset_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_kset_modules( cls, context: interfaces.context.ContextInterface, vmlinux_name: str ) -> Dict[str, extensions.module]: return linux_utilities_modules.Modules.get_kset_modules(context, vmlinux_name) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/check_syscall.py b/volatility3/framework/plugins/linux/malware/check_syscall.py index 1188bf250..6476d6621 100644 --- a/volatility3/framework/plugins/linux/malware/check_syscall.py +++ b/volatility3/framework/plugins/linux/malware/check_syscall.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """A module containing a plugin that checks the system call table for hooks.""" + import contextlib import logging from typing import List diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index dcd602c5d..a9f0b5b51 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -2,14 +2,21 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Set, Tuple, Iterable +from typing import Generator, Iterable, List, Set, Tuple + +from volatility3.framework import ( + constants, + deprecation, + exceptions, + interfaces, + renderers, +) +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import ( modules as linux_utilities_modules, ) -from volatility3.framework import interfaces, exceptions, deprecation -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins vollog = logging.getLogger(__name__) @@ -18,12 +25,12 @@ class Hidden_modules(plugins.PluginInterface): """Carves memory to find hidden kernel modules""" _required_framework_version = (2, 25, 0) - _version = (3, 0, 2) + _version = (3, 0, 3) @classmethod def find_hidden_modules( cls, context, vmlinux_module_name: str - ) -> extensions.module: + ) -> Generator[extensions.module, None, None]: if context.symbol_space.verify_table_versions( "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) ): @@ -81,29 +88,38 @@ class Hidden_modules(plugins.PluginInterface): vmlinux_module_name, known_module_addresses, modules_memory_boundaries ) - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = find_hidden_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, version=(3, 0, 1), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_modules_memory_boundaries( @@ -116,7 +132,7 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_module_address_alignment, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @classmethod @@ -144,13 +160,13 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_hidden_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.validate_alignment_patterns, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def _validate_alignment_patterns( @@ -195,3 +211,18 @@ class Hidden_modules(plugins.PluginInterface): ) } return known_module_addresses + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index c7e141c02..cbd9f87c1 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 3) + _version = (1, 0, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -37,6 +37,18 @@ class Malfind(interfaces.plugins.PluginInterface): element_type=int, optional=True, ), + requirements.IntRequirement( + name="dump-size", + description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", + optional=True, + default=64, + ), + requirements.BooleanRequirement( + name="dump-page", + description="Dump each dirty page and content - Default off", + optional=True, + default=False, + ), ] def _list_injections( @@ -51,14 +63,36 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] + dump_size = self.config["dump-size"] + + # Dumping page defaults to off, as in case a whole r-xp region is dirty + # this would likely dump 1000's of pages which might not always be wise nor necessary + + dump_page = self.config["dump-page"] + for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) vollog.debug( f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" ) + + # If is_suspicious returns true, this means at least one page + # in the region is dirty. If dump_page is true, then we dump + # all dirty pages + if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": - data = proc_layer.read(vma.vm_start, 64, pad=True) - yield vma, vma_name, data + malicious_pages = vma.get_malicious_pages(proc_layer) + offset = 0 + if dump_page: + # Dumping each dirty page + for page_addr in malicious_pages: + offset = page_addr - vma.vm_start + data = proc_layer.read(page_addr, dump_size, pad=True) + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset + else: + # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) + data = proc_layer.read(vma.vm_start, dump_size, pad=True) + yield vma, vma_name, data, offset def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel @@ -70,13 +104,15 @@ class Malfind(interfaces.plugins.PluginInterface): for task in tasks: process_name = utility.array_to_string(task.comm) - for vma, vma_name, data in self._list_injections(task): + for vma, vma_name, data, offset in self._list_injections(task): if is_32bit_arch: architecture = "intel" else: architecture = "intel64" - disasm = renderers.Disassembly(data, vma.vm_start, architecture) + disasm = renderers.Disassembly( + data, vma.vm_start + offset, architecture + ) yield ( 0, diff --git a/volatility3/framework/plugins/linux/malware/modxview.py b/volatility3/framework/plugins/linux/malware/modxview.py index c1707d26f..63b265202 100644 --- a/volatility3/framework/plugins/linux/malware/modxview.py +++ b/volatility3/framework/plugins/linux/malware/modxview.py @@ -2,15 +2,14 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Dict, Iterator +from typing import Dict, Iterator, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules - -from volatility3.framework import interfaces, deprecation, renderers +from volatility3.framework import deprecation, interfaces, renderers from volatility3.framework.configuration import requirements +from volatility3.framework.constants import architectures from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions -from volatility3.framework.constants import architectures from volatility3.framework.symbols.linux.utilities import tainting vollog = logging.getLogger(__name__) @@ -66,7 +65,7 @@ spot modules presence and taints.""" @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.flatten_run_modules_results, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def flatten_run_modules_results( cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True @@ -89,7 +88,7 @@ spot modules presence and taints.""" @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.run_modules_scanners, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def run_modules_scanners( cls, diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py index d724d4296..bd7f2b7cc 100644 --- a/volatility3/framework/plugins/linux/malware/netfilter.py +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -1,22 +1,22 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from dataclasses import dataclass, field -from abc import ABC, abstractmethod import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Iterator, List, Optional, Tuple import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from typing import Iterator, List, Tuple, Optional from volatility3 import framework from volatility3.framework import ( constants, + deprecation, + exceptions, interfaces, renderers, - exceptions, - deprecation, ) -from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import network vollog = logging.getLogger(__name__) @@ -223,7 +223,16 @@ class AbstractNetfilter(ABC): ) hooked = module_info is None - yield netns, proto_name, hook_name, priority, hook_ops_hook, module_info, symbol_name, hooked + yield ( + netns, + proto_name, + hook_name, + priority, + hook_ops_hook, + module_info, + symbol_name, + hooked, + ) @classmethod @abstractmethod @@ -304,7 +313,7 @@ class AbstractNetfilter(ABC): return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") @deprecation.method_being_removed( - removal_date="2025-09-25", + removal_date="2026-03-25", message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", ) def get_module_name_for_address(self, addr) -> str: diff --git a/volatility3/framework/plugins/linux/malware/tty_check.py b/volatility3/framework/plugins/linux/malware/tty_check.py index 1547c5cc6..00e85f251 100644 --- a/volatility3/framework/plugins/linux/malware/tty_check.py +++ b/volatility3/framework/plugins/linux/malware/tty_check.py @@ -100,11 +100,14 @@ class Tty_Check(plugins.PluginInterface): else: module_name = renderers.NotAvailableValue() - yield 0, ( - name, - format_hints.Hex(recv_buf), - module_name, - symbol_name or renderers.NotAvailableValue(), + yield ( + 0, + ( + name, + format_hints.Hex(recv_buf), + module_name, + symbol_name or renderers.NotAvailableValue(), + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index 97824aca0..a8864281a 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -75,10 +75,13 @@ class ModuleExtract(interfaces.plugins.PluginInterface): with self.open(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(base_address), - len(elf_data), - file_handle.preferred_filename, + yield ( + 0, + ( + format_hints.Hex(base_address), + len(elf_data), + file_handle.preferred_filename, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 1fd96d5d2..2d20a2fb1 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -386,7 +386,11 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): inode_out = inode_in.to_user(vmlinux_layer) description = f"Cached Inode for {inode_out.path}" yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time - yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + inode_out.modification_time, + ) yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time @classmethod @@ -813,7 +817,6 @@ class RecoverFs(plugins.PluginInterface): visited_paths = seen_prefixes = set() for inode_in in inodes_iter: - # Code is slightly duplicated here with the if-block below. # However this prevents unneeded tar manipulation if fifo # or sock inodes come through for example. diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 9b31976ec..71caa0853 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -225,18 +225,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): task_euid = self._format_cred(task_fields.euid) task_egid = self._format_cred(task_fields.egid) - yield 0, ( - format_hints.Hex(task_fields.offset), - task_fields.user_pid, - task_fields.user_tid, - task_fields.user_ppid, - task_fields.name, - task_uid, - task_gid, - task_euid, - task_egid, - task_fields.creation_time or renderers.NotAvailableValue(), - file_output, + yield ( + 0, + ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, + task_uid, + task_gid, + task_euid, + task_egid, + task_fields.creation_time or renderers.NotAvailableValue(), + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index 23b2f2c72..ff922784d 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -70,7 +70,6 @@ class PerfEvents(plugins.PluginInterface): for task in pslist.PsList.list_tasks( context, vmlinux_module_name, include_threads=True ): - # walk the list of perf_event entries for this process for event in task.perf_event_list.to_list( vmlinux.symbol_table_name + constants.BANG + "perf_event", "owner_entry" diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py index fb757beb5..2fde3aadc 100644 --- a/volatility3/framework/plugins/linux/vmaregexscan.py +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -64,7 +64,6 @@ class VmaRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for task in tasks: - if not task.mm: continue name = utility.array_to_string(task.comm) @@ -106,12 +105,15 @@ class VmaRegExScan(plugins.PluginInterface): bytes_result = result_data user_pid = task.tgid - yield 0, ( - user_pid, - name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + user_pid, + name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 64f5827c1..d9466f512 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -103,12 +103,15 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): layer_name=proc_layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.tgid, - rule_name, - name, - layer_data, + yield ( + 0, + ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + layer_data, + ), ) @classmethod diff --git a/volatility3/framework/plugins/mac/lsmod.py b/volatility3/framework/plugins/mac/lsmod.py index c6f57f889..05a0ee72f 100644 --- a/volatility3/framework/plugins/mac/lsmod.py +++ b/volatility3/framework/plugins/mac/lsmod.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's lsmod command.""" + from typing import Set from volatility3.framework import renderers, interfaces, exceptions diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index 0f3aa745c..3d9dcd916 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's mount command.""" + from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins diff --git a/volatility3/framework/plugins/mac/psaux.py b/volatility3/framework/plugins/mac/psaux.py index ba9b7b5f6..bdcfb1466 100644 --- a/volatility3/framework/plugins/mac/psaux.py +++ b/volatility3/framework/plugins/mac/psaux.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """In-memory artifacts from OSX systems.""" + from typing import Iterator, Tuple, Any, Generator, List from volatility3.framework import exceptions, renderers, interfaces diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py new file mode 100644 index 000000000..f4eaf9c9b --- /dev/null +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -0,0 +1,218 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import datetime +import logging +import sys +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + TextIO, +) +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.cli import text_renderer + +vollog = logging.getLogger(__name__) + +ARROW_PRESENT = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + + ARROW_PRESENT = True +except ImportError: + vollog.debug("Arrow/Parquet libraries not found") + + +class ArrowRenderer(text_renderer.CLIRenderer): + """Renderer that outputs Arrow IPC format data.""" + + name = "arrow" + structured_output = True + _version = (1, 0, 0) + + def __init__( + self, options: Optional[List[interfaces.renderers.RenderOption]] = None + ) -> None: + super().__init__(options) + + if not ARROW_PRESENT: + raise RuntimeError("Arrow output format requires the pyarrow package") + + self._to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.uint64, + format_hints.Hex: pa.uint64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + renderers.LayerData: pa.binary, + bytes: pa.binary, + } + + # indicates if the output from the plugin is nested, e.g., pstree + # which would then need to be flattened + self._is_tree_result = False + self._node_id_counter = 0 + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": + fields = [] + for column in grid.columns: + arrow_type = self._to_arrow_type[column.type] + fields.append(pa.field(column.name, arrow_type())) + + # if the output is nested, e.g., windows.pstree + if self._is_tree_result: + fields.append(pa.field("_vol_id", pa.uint64())) + fields.append(pa.field("_vol_parent_id", pa.uint64())) + + return pa.schema(fields) + + def _flatten_tree_structure(self, nested: List[Dict]) -> List[Dict]: + """ + Flattens a list of nested dicts using the `__children` key. + + Each node gets a `_vol_id` and a `_vol_parent_id` to preserve + the original tree structure in a flat format suitable for tabular output. + + Args: + nested: A list of dicts with optional `__children` lists (tree nodes). + + Returns: + A flat list of dicts with `_vol_id` and `_vol_parent_id`. + """ + rows = [] + self._node_id_counter = 0 + + def _process_node(node: Dict, parent_id: Optional[int]): + current_id = self._node_id_counter + self._node_id_counter += 1 + + entry = {k: v for k, v in node.items() if k != "__children"} + entry["_vol_id"] = current_id + entry["_vol_parent_id"] = parent_id + rows.append(entry) + + for child in node.get("__children", []): + _process_node(child, current_id) + + for root in nested: + _process_node(root, None) + + return rows + + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): + """Outputs the JSON data to a file in a particular format""" + + if self._is_tree_result: + result = self._flatten_tree_structure(result) + + t = pa.Table.from_pylist(result, schema=schema) + self.write_table(t, outfd) + + def write_table(self, t: "pa.Table", outfd: TextIO) -> None: + buf = pa.BufferOutputStream() + + writer = pa.ipc.new_stream(buf, t.schema) + writer.write_table(t) + writer.close() + + # Get the buffer bytes and write to output + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) + + def render(self, grid: interfaces.renderers.TreeGrid): + outfd = sys.stdout + final_output: Tuple[ + Dict[str, List[interfaces.renderers.TreeNode]], + List[interfaces.renderers.TreeNode], + ] = ({}, []) + + ignore_columns = self.ignored_columns(grid) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], + ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + acc_map, final_tree = accumulator + node_dict: Dict[str, Any] = {"__children": []} + line = [] + for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue + + data = list(node.values)[column_index] + + if isinstance(data, interfaces.renderers.BaseAbsentValue): + data = None + + if isinstance(data, renderers.Disassembly): + data = text_renderer.display_disassembly(data) + + if isinstance(data, renderers.LayerData): + data = text_renderer.LayerDataRenderer().render_bytes(data)[0] + + node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + + if node.parent: + acc_map[node.parent.path]["__children"].append(node_dict) + self._is_tree_result = True + else: + final_tree.append(node_dict) + acc_map[node.path] = node_dict + + return (acc_map, final_tree) + + if not grid.populated: + grid.populate(visitor, final_output) + else: + grid.visit(node=None, function=visitor, initial_accumulator=final_output) + + schema = self.to_arrow_schema(grid) + self.output_result(schema, outfd, final_output[1]) + + +class ParquetRenderer(ArrowRenderer): + """Renderer that outputs Parquet format data.""" + + name = "parquet" + structured_output = True + _version = (1, 0, 0) + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def write_table(self, table: "pa.Table", outfd: TextIO) -> None: + """ + Writes a table to stdout using the Parquet format. + + Args: + t: The Arrow table to write + outfd: The output file descriptor + + Returns: + Nothing + """ + # Write DataFrame to a temporary file-like object + buf = pa.BufferOutputStream() + pq.write_table(table, buf, compression="snappy") + + # Get the buffer as a bytes object + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index be144be91..0d1450127 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import amcache vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Amcache( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=amcache.Amcache, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Extract information on executed applications from the AmCache (deprecated).""" diff --git a/volatility3/framework/plugins/windows/cachedump.py b/volatility3/framework/plugins/windows/cachedump.py index 35127c6f3..3c474bc2c 100644 --- a/volatility3/framework/plugins/windows/cachedump.py +++ b/volatility3/framework/plugins/windows/cachedump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import cachedump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Cachedump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=cachedump.Cachedump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index b8c9fe751..2a65f76bb 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -78,7 +78,6 @@ class Callbacks(interfaces.plugins.PluginInterface): def _create_default_scan_constraints( context: interfaces.context.ContextInterface, symbol_table: str ) -> List[poolscanner.PoolConstraint]: - shutdown_packet_size = context.symbol_space.get_type( symbol_table + constants.BANG + "_SHUTDOWN_PACKET" ).size @@ -590,7 +589,11 @@ class Callbacks(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: component = renderers.UnreadableValue() - yield "KeBugCheckReasonCallbackListHead", callback.CallbackRoutine, component + yield ( + "KeBugCheckReasonCallbackListHead", + callback.CallbackRoutine, + component, + ) @classmethod def list_bugcheck_callbacks( diff --git a/volatility3/framework/plugins/windows/deskscan.py b/volatility3/framework/plugins/windows/deskscan.py index 35430be5d..202eaf330 100644 --- a/volatility3/framework/plugins/windows/deskscan.py +++ b/volatility3/framework/plugins/windows/deskscan.py @@ -77,6 +77,11 @@ class DeskScan(desktops.Desktops): continue for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), winsta_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + winsta_name, + session_id, + desktop_name, + process_name, + process_pid, + ) diff --git a/volatility3/framework/plugins/windows/desktops.py b/volatility3/framework/plugins/windows/desktops.py index c6557085e..f2a985c6b 100644 --- a/volatility3/framework/plugins/windows/desktops.py +++ b/volatility3/framework/plugins/windows/desktops.py @@ -63,9 +63,14 @@ class Desktops(interfaces.plugins.PluginInterface): for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name): # for each desktop, walk its threads for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), station_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + station_name, + session_id, + desktop_name, + process_name, + process_pid, + ) def _generator(self): kernel_name = self.config["kernel"] diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index b851cf7fd..cb9d3b8bf 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -22,7 +22,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the loaded DLLs in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 0) + _version = (3, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -173,6 +173,10 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except exceptions.InvalidAddressException: size_of_image = renderers.NotAvailableValue() + LoadCount = entry.get_load_count() + if LoadCount is None: + LoadCount = renderers.NotAvailableValue() + yield ( 0, ( @@ -186,6 +190,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): size_of_image, BaseDllName, FullDllName, + LoadCount, DllLoadTime, file_output, ), @@ -232,6 +237,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", format_hints.Hex), ("Name", str), ("Path", str), + ("LoadCount", int), ("LoadTime", datetime.datetime), ("File output", str), ], diff --git a/volatility3/framework/plugins/windows/hashdump.py b/volatility3/framework/plugins/windows/hashdump.py index e496e77a9..c4b99d86f 100644 --- a/volatility3/framework/plugins/windows/hashdump.py +++ b/volatility3/framework/plugins/windows/hashdump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import hashdump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Hashdump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=hashdump.Hashdump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps user hashes from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/kpcrs.py b/volatility3/framework/plugins/windows/kpcrs.py index 213e3833c..d544fd498 100644 --- a/volatility3/framework/plugins/windows/kpcrs.py +++ b/volatility3/framework/plugins/windows/kpcrs.py @@ -96,7 +96,6 @@ class KPCRs(interfaces.plugins.PluginInterface): yield kpcr, kpcr.member(kpcr_member) def _generator(self) -> Iterator[Tuple]: - for kpcr, current_prcb in self.list_kpcrs(self.context, self.config["kernel"]): yield ( 0, diff --git a/volatility3/framework/plugins/windows/lsadump.py b/volatility3/framework/plugins/windows/lsadump.py index 0b36ddef0..ab81f18df 100644 --- a/volatility3/framework/plugins/windows/lsadump.py +++ b/volatility3/framework/plugins/windows/lsadump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import lsadump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Lsadump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=lsadump.Lsadump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/malware/direct_system_calls.py b/volatility3/framework/plugins/windows/malware/direct_system_calls.py index dce09605b..f6b9e53bb 100644 --- a/volatility3/framework/plugins/windows/malware/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/malware/direct_system_calls.py @@ -451,12 +451,15 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface): address, disasm_bytes = syscall_info - yield 0, ( - proc_name, - proc.UniqueProcessId, - vad_path, - format_hints.Hex(address), - disasm_bytes, + yield ( + 0, + ( + proc_name, + proc.UniqueProcessId, + vad_path, + format_hints.Hex(address), + disasm_bytes, + ), ) def run(self) -> renderers.TreeGrid: diff --git a/volatility3/framework/plugins/windows/malware/hollowprocesses.py b/volatility3/framework/plugins/windows/malware/hollowprocesses.py index f981ae340..1ea46b540 100644 --- a/volatility3/framework/plugins/windows/malware/hollowprocesses.py +++ b/volatility3/framework/plugins/windows/malware/hollowprocesses.py @@ -198,10 +198,13 @@ class HollowProcesses(interfaces.plugins.PluginInterface): for check in checks: for note in check(proc, vads, dlls): - yield 0, ( - pid, - proc_name, - note, + yield ( + 0, + ( + pid, + proc_name, + note, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/malware/malfind.py b/volatility3/framework/plugins/windows/malware/malfind.py index 01da93e1f..2f6d9eb56 100644 --- a/volatility3/framework/plugins/windows/malware/malfind.py +++ b/volatility3/framework/plugins/windows/malware/malfind.py @@ -92,8 +92,11 @@ class Malfind(interfaces.plugins.PluginInterface): for vad, data_object in cls.list_injection_sites( context, kernel_layer_name, symbol_table, proc ): - yield vad, data_object.context.layers[data_object.layer_name].read( - data_object.offset, data_object.length + yield ( + vad, + data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length + ), ) @classmethod diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py new file mode 100644 index 000000000..cd898239f --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -0,0 +1,233 @@ +import logging +from typing import List, Union, Tuple + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + + +# https://www.ired.team/offensive-security/defense-evasion/masquerading-processes-in-userland-through-_peb +# https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Masquerade-PEB.ps1 +class PebMasquerade(interfaces.plugins.PluginInterface): + """Detects potential process name spoofing by comparing EPROCESS and PEB data.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 27, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + ] + + @classmethod + def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[ + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + ]: + """Extract process names and related information from various sources (EPROCESS and PEB). + + Args: + proc: The process object + + Returns: + tuple: (eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline) + """ + eprocess_imagefilename = renderers.NotAvailableValue() + eprocess_seaudit_imagefilename = renderers.NotAvailableValue() + peb_imagefilepath = renderers.NotAvailableValue() + peb_cmdline = renderers.NotAvailableValue() + + try: + eprocess_imagefilename = utility.array_to_string(proc.ImageFileName) + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read EPROCESS.ImageFileName for PID %d", proc.UniqueProcessId + ) + except Exception as e: + vollog.warning( + "Error reading EPROCESS.ImageFileName for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + audit = proc.SeAuditProcessCreationInfo.ImageFileName.Name + audit_string = audit.get_string() + if audit_string: + eprocess_seaudit_imagefilename = audit_string + except exceptions.InvalidAddressException: + vollog.debug( + "Unable to read SeAuditProcessCreationInfo.ImageFileName for PID %d", + proc.UniqueProcessId, + ) + except AttributeError: + vollog.debug( + "SeAuditProcessCreationInfo structure not available for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading SeAuditProcessCreationInfo for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + peb = proc.get_peb() + if peb and peb.ProcessParameters: + # Get ImagePathName + try: + image_path_str = peb.ProcessParameters.ImagePathName.get_string() + if image_path_str: + peb_imagefilepath = image_path_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ImagePathName for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ImagePathName for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + cmdline_str = peb.ProcessParameters.CommandLine.get_string() + if cmdline_str: + peb_cmdline = cmdline_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ProcessParameters.CommandLine for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ProcessParameters.CommandLine for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + except (AttributeError, exceptions.InvalidAddressException): + # Important for cases where PEB does not exist or is inaccessible (e.g SYSTEM process) + vollog.debug("Unable to access PEB for PID %d", proc.UniqueProcessId) + except Exception as e: + vollog.warning( + "Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e) + ) + + return ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) + + def _generator(self, pids, context, kernel_module_name): + pid_filter = pslist.PsList.create_pid_filter(pids) + + for proc in pslist.PsList.list_processes( + context=context, + kernel_module_name=kernel_module_name, + filter_func=pid_filter, + ): + proc_id = proc.UniqueProcessId + try: + peb = proc.get_peb() + except (exceptions.InvalidAddressException, AttributeError): + vollog.debug( + "Unable to access PEB for PID %d, skipping process", proc_id + ) + peb_imagefilepath_length_check = False + peb_cmdline_length_check = False + ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) = PebMasquerade.get_process_names(proc) + + if isinstance(peb_imagefilepath, str) and peb: + try: + # Length values are of type USHORT + peb_imagefilepath_length = ( + peb.ProcessParameters.ImagePathName.Length // 2 + ) + peb_imagefilepath_maxlength = ( + peb.ProcessParameters.ImagePathName.MaximumLength // 2 - 1 + ) + + if (peb_imagefilepath_length != len(peb_imagefilepath)) or ( + peb_imagefilepath_maxlength != len(peb_imagefilepath) + ): + peb_imagefilepath_length_check = True + except Exception as e: + vollog.warning( + "PEB.ImagePathName Length comparison error for PID %d: %s", + proc_id, + str(e), + ) + + if isinstance(peb_cmdline, str) and peb: + try: + # Length values are of type USHORT + peb_cmdline_length = peb.ProcessParameters.CommandLine.Length // 2 + peb_cmdline_maxlength = ( + peb.ProcessParameters.CommandLine.MaximumLength // 2 - 1 + ) + + if (peb_cmdline_length != len(peb_cmdline)) or ( + peb_cmdline_maxlength != len(peb_cmdline) + ): + peb_cmdline_length_check = True + except Exception as e: + vollog.warning( + "PEB.CommandLine Length comparison error for PID %d: %s", + proc_id, + str(e), + ) + yield ( + 0, + ( + proc_id, + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline_length_check, + peb_imagefilepath_length_check, + ), + ) + + def run(self): + pids = self.config.get("pid", None) + context = self.context + kernel_module_name = self.config["kernel"] + return renderers.TreeGrid( + [ + ("PID", int), + ("EPROCESS_ImageFileName", str), + ("EPROCESS_SeAudit_ImageFileName", str), + ("PEB_ImageFilePath", str), + ("PEB_ImageFilePath_Spoofed", bool), + ("PEB_CommandLine_Spoofed", bool), + ], + self._generator(pids, context, kernel_module_name), + ) diff --git a/volatility3/framework/plugins/windows/malware/processghosting.py b/volatility3/framework/plugins/windows/malware/processghosting.py index f234bc2e7..fb8ec527d 100644 --- a/volatility3/framework/plugins/windows/malware/processghosting.py +++ b/volatility3/framework/plugins/windows/malware/processghosting.py @@ -149,9 +149,12 @@ class ProcessGhosting(interfaces.plugins.PluginInterface): for file_object_address, delete_pending, delete_on_close in cls._vad_checks( control_area, path ): - yield format_hints.Hex( - file_object_address - ), delete_pending, delete_on_close, vad_base + yield ( + format_hints.Hex(file_object_address), + delete_pending, + delete_on_close, + vad_base, + ) def _generator(self, procs): kernel = self.context.modules[self.config["kernel"]] @@ -187,14 +190,17 @@ class ProcessGhosting(interfaces.plugins.PluginInterface): else: path = renderers.NotAvailableValue() - yield 0, ( - pid, - process_name, - format_hints.Hex(base_address), - format_hints.Hex(file_object_address), - delete_pending or renderers.NotApplicableValue(), - delete_on_close or renderers.NotApplicableValue(), - path, + yield ( + 0, + ( + pid, + process_name, + format_hints.Hex(base_address), + format_hints.Hex(file_object_address), + delete_pending or renderers.NotApplicableValue(), + delete_on_close or renderers.NotApplicableValue(), + path, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py index d9cba0704..10c6222bc 100644 --- a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py @@ -648,12 +648,15 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): csystem, cryptdll_base, cryptdll_size ) - yield 0, ( - lsass_proc.UniqueProcessId, - "lsass.exe", - skeleton_key_present, - format_hints.Hex(csystem.Initialize), - format_hints.Hex(csystem.Decrypt), + yield ( + 0, + ( + lsass_proc.UniqueProcessId, + "lsass.exe", + skeleton_key_present, + format_hints.Hex(csystem.Initialize), + format_hints.Hex(csystem.Decrypt), + ), ) def _lsass_proc_filter(self, proc): diff --git a/volatility3/framework/plugins/windows/malware/suspicious_threads.py b/volatility3/framework/plugins/windows/malware/suspicious_threads.py index 3da8cb21a..803a0b04b 100644 --- a/volatility3/framework/plugins/windows/malware/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/malware/suspicious_threads.py @@ -196,14 +196,17 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): for vad_path, note in self._check_thread_address( exe_path, ranges, address ): - yield 0, ( - proc_name, - pid, - tid, - context, - format_hints.Hex(address), - vad_path, - note, + yield ( + 0, + ( + proc_name, + pid, + tid, + context, + format_hints.Hex(address), + vad_path, + note, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index f0be3cf7f..2dfecb93b 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -132,19 +132,22 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # There should only be one STANDARD_INFORMATION attribute, but we # do this just in case. for std_information in mft_record.standard_information_entries(): - yield 0, cls.MFTScanResult( - format_hints.Hex(std_information.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - renderers.NotApplicableValue(), - "STANDARD_INFORMATION", - conversion.wintime_to_datetime(std_information.CreationTime), - conversion.wintime_to_datetime(std_information.ModifiedTime), - conversion.wintime_to_datetime(std_information.UpdatedTime), - conversion.wintime_to_datetime(std_information.AccessedTime), - renderers.NotApplicableValue(), + yield ( + 0, + cls.MFTScanResult( + format_hints.Hex(std_information.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + renderers.NotApplicableValue(), + "STANDARD_INFORMATION", + conversion.wintime_to_datetime(std_information.CreationTime), + conversion.wintime_to_datetime(std_information.ModifiedTime), + conversion.wintime_to_datetime(std_information.UpdatedTime), + conversion.wintime_to_datetime(std_information.AccessedTime), + renderers.NotApplicableValue(), + ), ) except exceptions.InvalidAddressException: pass @@ -163,26 +166,28 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # File Name Attribute try: for filename_info in mft_record.filename_entries(): - # If we don't have a valid enum, coerce to hex so we can keep the record try: permissions = filename_info.Flags.lookup() except ValueError: permissions = hex(filename_info.Flags) - yield 1, cls.MFTScanResult( - format_hints.Hex(filename_info.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - permissions, - "FILE_NAME", - conversion.wintime_to_datetime(filename_info.CreationTime), - conversion.wintime_to_datetime(filename_info.ModifiedTime), - conversion.wintime_to_datetime(filename_info.UpdatedTime), - conversion.wintime_to_datetime(filename_info.AccessedTime), - filename_info.get_full_name(), + yield ( + 1, + cls.MFTScanResult( + format_hints.Hex(filename_info.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + permissions, + "FILE_NAME", + conversion.wintime_to_datetime(filename_info.CreationTime), + conversion.wintime_to_datetime(filename_info.ModifiedTime), + conversion.wintime_to_datetime(filename_info.UpdatedTime), + conversion.wintime_to_datetime(filename_info.AccessedTime), + filename_info.get_full_name(), + ), ) except exceptions.InvalidAddressException: return @@ -214,22 +219,25 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield level, ( - record.offset, - record.record_type, - int(record.record_number), - int(record.link_count), - record.mft_type, - record.permissions, - record.attribute_type, - record.created, - record.modified, - record.updated, - record.accessed, + yield ( + level, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + record.record_type, + int(record.record_number), + int(record.link_count), + record.mft_type, + record.permissions, + record.attribute_type, + record.created, + record.modified, + record.updated, + record.accessed, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), ), ) @@ -344,22 +352,25 @@ class ADS(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - record.offset, - str(record.signature), - int(record.record_number), - record.attribute_type, + yield ( + 0, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + str(record.signature), + int(record.record_number), + record.attribute_type, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), + ( + str(record.stream_name) + if isinstance(record.stream_name, objects.String) + else record.stream_name + ), + record.content, ), - ( - str(record.stream_name) - if isinstance(record.stream_name, objects.String) - else record.stream_name - ), - record.content, ) def run(self): @@ -454,13 +465,16 @@ class ResidentData(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - resident_data_entry.offset, - str(resident_data_entry.signature), - int(resident_data_entry.record_number), - resident_data_entry.attribute_type, - str(resident_data_entry.filename), - resident_data_entry.content, + yield ( + 0, + ( + resident_data_entry.offset, + str(resident_data_entry.signature), + int(resident_data_entry.record_number), + resident_data_entry.attribute_type, + str(resident_data_entry.filename), + resident_data_entry.content, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index 1ec965737..18a7b5919 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -119,13 +119,16 @@ class Modules(interfaces.plugins.PluginInterface): if self.config["dump"]: file_output = self.dump_module(session_layers, pe_table_name, mod) - yield 0, ( - format_hints.Hex(mod.vol.offset), - format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), - BaseDllName, - FullDllName, - file_output, + yield ( + 0, + ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 157282ce1..43dcd0482 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -436,7 +436,6 @@ class PoolScanner(plugins.PluginInterface): constraints, alignment=alignment, ): - mem_objects = header.get_object( constraint=constraint, use_top_down=is_windows_8_or_later, diff --git a/volatility3/framework/plugins/windows/registry/amcache.py b/volatility3/framework/plugins/windows/registry/amcache.py index a6ad1848e..ed078993c 100644 --- a/volatility3/framework/plugins/windows/registry/amcache.py +++ b/volatility3/framework/plugins/windows/registry/amcache.py @@ -246,13 +246,29 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, entry in self._generator(): if isinstance(entry.last_modify_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} registry key modified", + timeliner.TimeLinerType.MODIFIED, + entry.last_modify_time, + ) if isinstance(entry.last_modify_time_2, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2 + yield ( + f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", + timeliner.TimeLinerType.CREATED, + entry.last_modify_time_2, + ) if isinstance(entry.install_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} installed", + timeliner.TimeLinerType.CREATED, + entry.install_time, + ) if isinstance(entry.compile_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", + timeliner.TimeLinerType.MODIFIED, + entry.compile_time, + ) @classmethod def get_amcache_hive( @@ -319,20 +335,23 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): vollog.debug(f"Found sha1hash {sha1_hash}") product_name = _get_string_value(values, val_enum.Product.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=company, - last_modify_time=last_mod_time, - last_modify_time_2=last_mod_time_2, - install_time=install_time, - compile_time=compile_time, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=company, + last_modify_time=last_mod_time, + last_modify_time_2=last_mod_time_2, + install_time=install_time, + compile_time=compile_time, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=product_name, ), - product_name=product_name, ) @classmethod @@ -365,15 +384,18 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) version = _get_string_value(values, val_enum.Version.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.Program.name, - company=company, - last_modify_time=conversion.wintime_to_datetime( - program_key.LastWriteTime.QuadPart + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.Program.name, + company=company, + last_modify_time=conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ), + install_time=install_time, + product_name=product, + product_version=version, ), - install_time=install_time, - product_name=product, - product_version=version, ) @classmethod @@ -411,14 +433,17 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore - yield program_id.strip().strip("\u0000"), _AmcacheEntry( - AmcacheEntryType.Program.name, - path=path, - last_modify_time=last_mod, - install_time=install_date, - product_name=product, - company=publisher, - product_version=version, + yield ( + program_id.strip().strip("\u0000"), + _AmcacheEntry( + AmcacheEntryType.Program.name, + path=path, + last_modify_time=last_mod, + install_time=install_date, + product_name=product, + company=publisher, + product_version=version, + ), ) @classmethod @@ -456,19 +481,22 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): prod_ver = _get_string_value(values, val_enum.ProductVersion.value) program_id = _get_string_value(values, val_enum.ProgramID.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=publisher, - last_modify_time=last_mod, - compile_time=linkdate, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=publisher, + last_modify_time=last_mod, + compile_time=linkdate, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=prod_name, + product_version=prod_ver, ), - product_name=prod_name, - product_version=prod_ver, ) @classmethod @@ -485,7 +513,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): wanted_values = [key.value for key in val_enum] for binary_key in driver_binary_key.get_subkeys(): - values = { str(value.get_name()): value for value in binary_key.get_values() @@ -636,7 +663,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield 0, empty_program def run(self): - return renderers.TreeGrid( [ ("EntryType", str), diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py index 2c660229b..a3e5fabe2 100644 --- a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -185,7 +185,6 @@ NULL = "\u0000" class _ScheduledTasksReader(io.BytesIO): - def read_task_scheduler_time(self) -> Optional[datetime.datetime]: _ = bool(self.read_aligned_u1()) # is_localized filetime = self.decode_filetime() @@ -393,7 +392,6 @@ class TaskAction: num_attachment_filenames = reader.read_u4() if num_attachment_filenames is not None: - attachment_filenames = [ reader.read_bstring() for _ in range(num_attachment_filenames) ] @@ -1138,11 +1136,23 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, task in self._generator(): if isinstance(task.last_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", timeliner.TimeLinerType.ACCESSED, task.last_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", + timeliner.TimeLinerType.ACCESSED, + task.last_run_time, + ) if isinstance(task.last_successful_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", timeliner.TimeLinerType.ACCESSED, task.last_successful_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", + timeliner.TimeLinerType.ACCESSED, + task.last_successful_run_time, + ) if isinstance(task.creation_time, datetime.datetime): - yield f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", timeliner.TimeLinerType.CREATED, task.creation_time + yield ( + f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", + timeliner.TimeLinerType.CREATED, + task.creation_time, + ) @classmethod def get_software_hive( @@ -1203,7 +1213,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte def parse_dynamic_info_value( cls, dyn_info_value: reg_extensions.CM_KEY_VALUE ) -> Optional[DynamicInfo]: - try: data = dyn_info_value.decode_data() except exceptions.InvalidAddressException: @@ -1318,7 +1327,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte all_actions = action_set.actions or [None] if action_set is not None else [None] for action, trigger in itertools.product(all_actions, all_triggers): - if action is not None: if action.action_type in ( ActionType.Exe, diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 62d8e3b88..104d062b2 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import scheduled_tasks vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class ScheduledTasks( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=scheduled_tasks.ScheduledTasks, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Decodes scheduled task information from the Windows registry, including information about triggers, actions, run times, and creation times (deprecated).""" diff --git a/volatility3/framework/plugins/windows/sessions.py b/volatility3/framework/plugins/windows/sessions.py index 29d0b2104..158820529 100644 --- a/volatility3/framework/plugins/windows/sessions.py +++ b/volatility3/framework/plugins/windows/sessions.py @@ -95,13 +95,16 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # Group and yield each row for rows in sessions.values(): for row in rows: - yield 0, ( - row.get("session_id"), - row.get("session_type"), - row.get("process_id"), - row.get("process_name"), - row.get("user_name"), - row.get("process_start"), + yield ( + 0, + ( + row.get("session_id"), + row.get("session_type"), + row.get("process_id"), + row.get("process_name"), + row.get("user_name"), + row.get("process_start"), + ), ) def generate_timeline(self): diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index 7a03ebbb6..8935757d4 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -51,9 +51,17 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime]]: for _, (_, last_modified, last_update, _, _, file_path) in self._generator(): if isinstance(last_update, datetime): - yield f"Shimcache: File {file_path} executed", timeliner.TimeLinerType.ACCESSED, last_update + yield ( + f"Shimcache: File {file_path} executed", + timeliner.TimeLinerType.ACCESSED, + last_update, + ) if isinstance(last_modified, datetime): - yield f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified + yield ( + f"Shimcache: File {file_path} modified", + timeliner.TimeLinerType.MODIFIED, + last_modified, + ) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -161,7 +169,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf continue try: - if proc_layer.read(vad.get_start(), 4) != b"\xEF\xBE\xAD\xDE": + if proc_layer.read(vad.get_start(), 4) != b"\xef\xbe\xad\xde": if pid == 624: vollog.debug("VAD magic bytes don't match DEADBEEF") continue diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 5f0e4761e..1bfb98fda 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -150,7 +150,6 @@ class SvcScan(interfaces.plugins.PluginInterface): def _get_service_key( context, config_path: str, kernel_module_name: str ) -> Optional[objects.StructType]: - for hive in hivelist.HiveList.list_hives( context=context, base_config_path=interfaces.configuration.path_join( diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 082b82284..1588f292e 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -167,16 +167,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) info = self.gather_thread_info(ethread, vads_cache) if info: - yield 0, ( - format_hints.Hex(info.offset), - info.pid, - info.tid, - format_hints.Hex(info.start_addr), - info.start_path or renderers.NotAvailableValue(), - format_hints.Hex(info.win32_start_addr), - info.win32_start_path or renderers.NotAvailableValue(), - info.create_time, - info.exit_time, + yield ( + 0, + ( + format_hints.Hex(info.offset), + info.pid, + info.tid, + format_hints.Hex(info.start_addr), + info.start_path or renderers.NotAvailableValue(), + format_hints.Hex(info.win32_start_addr), + info.win32_start_path or renderers.NotAvailableValue(), + info.create_time, + info.exit_time, + ), ) def generate_timeline(self): diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py index 068838e35..6a3cc394f 100644 --- a/volatility3/framework/plugins/windows/vadregexscan.py +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -62,7 +62,6 @@ class VadRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for proc in procs: - # attempt to create a process layer for each proc proc_layer_name = proc.add_process_layer() if not proc_layer_name: @@ -106,12 +105,15 @@ class VadRegExScan(plugins.PluginInterface): max_length=proc.ImageFileName.vol.count, errors="replace", ) - yield 0, ( - proc_id, - process_name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + proc_id, + process_name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 04b9aadd9..9a38213fa 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -4,6 +4,7 @@ import logging from typing import Iterable, List, Tuple +import datetime from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements @@ -18,7 +19,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" _required_framework_version = (2, 22, 0) - _version = (1, 1, 3) + _version = (1, 1, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -99,12 +100,24 @@ class VadYaraScan(interfaces.plugins.PluginInterface): layer_name=layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.UniqueProcessId, - rule_name, - name, - layer_data, + yield ( + 0, + ( + format_hints.Hex(offset), + task.UniqueProcessId, + task.get_create_time(), + task.InheritedFromUniqueProcessId, + task.ImageFileName.cast( + "string", + max_length=task.ImageFileName.vol.count, + errors="replace", + ), + task.get_session_id(), + task.ActiveThreads, + rule_name, + name, + layer_data, + ), ) @classmethod @@ -130,6 +143,11 @@ class VadYaraScan(interfaces.plugins.PluginInterface): [ ("Offset", format_hints.Hex), ("PID", int), + ("CreateTime", datetime.datetime), + ("PPID", int), + ("ImageFileName", str), + ("SessionId", int), + ("Threads", int), ("Rule", str), ("Component", str), ("Value", renderers.LayerData), diff --git a/volatility3/framework/plugins/windows/windows.py b/volatility3/framework/plugins/windows/windows.py index 9d4317df9..c98869665 100644 --- a/volatility3/framework/plugins/windows/windows.py +++ b/volatility3/framework/plugins/windows/windows.py @@ -112,15 +112,18 @@ class Windows(interfaces.plugins.PluginInterface): ) continue - yield 0, ( - format_hints.Hex(window.vol.offset), - station_name, - sess_id, - desktop_name, - window_name or renderers.NotAvailableValue(), - window_proc, - process_name, - process_pid, + yield ( + 0, + ( + format_hints.Hex(window.vol.offset), + station_name, + sess_id, + desktop_name, + window_name or renderers.NotAvailableValue(), + window_proc, + process_name, + process_pid, + ), ) def run(self): diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 8732e6e88..899c19196 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -6,6 +6,7 @@ Renderers display the unified output format in some manner (be it text or file or graphical output """ + import collections import collections.abc import dataclasses diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index d57c7e9f1..83f36a1a0 100644 --- a/volatility3/framework/renderers/format_hints.py +++ b/volatility3/framework/renderers/format_hints.py @@ -8,6 +8,7 @@ These hints allow a plugin to indicate how they would like data from a particula Text renderers should attempt to honour all hints provided in this module where possible """ + from typing import Type, Union from volatility3.framework import interfaces diff --git a/volatility3/framework/symbols/__init__.py b/volatility3/framework/symbols/__init__.py index 87f2288d7..a050298ba 100644 --- a/volatility3/framework/symbols/__init__.py +++ b/volatility3/framework/symbols/__init__.py @@ -210,9 +210,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): replacements = set() # Whole Symbols that still need traversing while traverse_list: - template_traverse_list, traverse_list = [ - self._resolved[traverse_list[0]] - ], traverse_list[1:] + template_traverse_list, traverse_list = ( + [self._resolved[traverse_list[0]]], + traverse_list[1:], + ) # Traverse a single symbol looking for any ReferenceTemplate objects while template_traverse_list: traverser, template_traverse_list = ( diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 33035bc70..001f817bd 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -246,9 +246,12 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): if name.endswith(zip_match + extension) or ( zip_match == "*" and name.endswith(extension) ): - yield "jar:file:" + str( - pathlib.Path(zip_path) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(zip_path)) + + "!" + + name + ) @classmethod def create( diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index ac27b7e42..19fb8f1d4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -99,8 +99,10 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 3, 1) + _version = (2, 4, 0) _required_framework_version = (2, 0, 0) + deleted = "(deleted)" + smear = "" framework.require_interface_version(*_required_framework_version) @@ -168,6 +170,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # vfsmnt can be the vfsmount object itself (>=3.3) or a vfsmount * (<3.3) return "" + inode = dentry.d_inode path_reversed = [] smeared = False while ( @@ -191,6 +194,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): parent = dentry.d_parent dname = dentry.d_name.name_as_str() + # empty dentry names are most likely # the result of smearing if not dname: @@ -203,7 +207,10 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # if there is smear the missing dname will be empty. e.g. if the normal # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. - return f" {path}" + return f"{LinuxUtilities.smear} {path}" + + if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: + path = f" {path} {LinuxUtilities.deleted}" return path @classmethod @@ -301,7 +308,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f"{pre_name}:[{inode.i_ino:d}]" @classmethod - def path_for_file(cls, context, task, filp) -> str: + def path_for_file(cls, context, task, filp, files_only=False) -> str: """Returns a file (or sock pipe) pathname relative to the task's root directory. A 'file' structure doesn't have enough information to properly restore its @@ -340,7 +347,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): except exceptions.InvalidAddressException: dname_is_valid = False - if dname_is_valid: + if dname_is_valid and not files_only: ret = LinuxUtilities._get_new_sock_pipe_path(context, task, filp) else: ret = LinuxUtilities._get_path_file(task, filp) @@ -353,6 +360,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): context: interfaces.context.ContextInterface, symbol_table: str, task: interfaces.objects.ObjectInterface, + files_only: bool = False, ): try: files = task.files @@ -376,7 +384,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): for fd_num, filp in enumerate(fds): if filp and filp.is_readable(): - full_path = LinuxUtilities.path_for_file(context, task, filp) + full_path = LinuxUtilities.path_for_file( + context, task, filp, files_only + ) yield fd_num, filp, full_path diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..aa052cf6d 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -36,7 +36,6 @@ vollog = logging.getLogger(__name__) class module(generic.GenericIntelProcess): - def is_valid(self): """Determine whether it is a valid module object by verifying the self-referential in module_kobject. This also confirms that the module is actively allocated and @@ -991,7 +990,6 @@ class maple_tree(objects.StructType): class mm_struct(objects.StructType): - # TODO: As of version 3.0.0 this method should be removed def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """ @@ -1273,6 +1271,42 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None + def get_malicious_pages(self, proclayer) -> List[int]: + """Identifies and returns a list of potentially malicious memory pages. + + A page is considered malicious if it is: + - Executable (protection flags match 'r-x') + - Dirty (modified since process start, according to proclayer.is_dirty()) + + Args: + proclayer: The process's memory layer + + Returns: + List[int]: A list of virtual addresses for pages flagged as potentially malicious. + """ + + malicious_pages = [] + flags_str = self.get_protection() + + if ( + proclayer + and "r-x" in flags_str + and self.vm_file.dereference().vol.offset != 0 + ): + for i in range(self.vm_start, self.vm_end, proclayer.page_size): + try: + if proclayer.is_dirty(i): + vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !") + malicious_pages.append(i) + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug(f"Unable to translate address {hex(i)} : {excp}") + # Abort as it is likely that other addresses in the same range will also fail + break + return malicious_pages + # used by malfind def is_suspicious(self, proclayer=None): ret = False @@ -1288,7 +1322,7 @@ class vm_area_struct(objects.StructType): try: if proclayer.is_dirty(i): vollog.warning( - f"Found malicious (dirty+exec) page at {hex(i)} !" + f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !" ) # We do not attempt to find other dirty+exec pages once we have found one ret = True @@ -1971,8 +2005,9 @@ class mnt_namespace(objects.StructType): self._context, self ) for node in self.mounts.get_nodes(): + # See kernel's node_to_mount() mnt = linux.LinuxUtilities.container_of( - node, "mount", "mnt_list", vmlinux + node, "mount", "mnt_node", vmlinux ) yield mnt else: @@ -3048,7 +3083,6 @@ class latch_tree_root(objects.StructType): class kernel_symbol(objects.StructType): - def _offset_to_ptr(self, off) -> int: layer = self._context.layers[self.vol.layer_name] long_mask = (1 << layer.bits_per_register) - 1 diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 0ae748814..2b16ec6e0 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,37 +1,36 @@ import logging import warnings +from abc import ABCMeta, abstractmethod from typing import ( + Callable, + Dict, + Generator, Iterable, Iterator, List, - Optional, - Tuple, NamedTuple, - Dict, + Optional, Set, - Generator, + Tuple, Union, ) -from abc import ABCMeta, abstractmethod +import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3 import framework from volatility3.framework import ( constants, - interfaces, deprecation, exceptions, + interfaces, objects, renderers, ) -from volatility3.framework.constants import architectures -from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import tainting -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract - vollog = logging.getLogger(__name__) @@ -297,7 +296,6 @@ class Modules(interfaces.configuration.VersionableInterface): # process each module coming from back the current source for module in gatherer.gather_modules(context, kernel_module_name): - # the kernel sends back a ModuleInfo directly if isinstance(module, ModuleInfo): modinfo = module @@ -383,7 +381,7 @@ class Modules(interfaces.configuration.VersionableInterface): vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterable[extensions.module]: """Enumerate hidden modules by taking advantage of memory address alignment patterns This technique is much faster and uses less memory than the traditional scan method @@ -920,19 +918,11 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): The constructor of the plugin must call super() with the `implementation` set """ - _version = (1, 0, 1) - _required_framework_version = (2, 0, 0) - - framework.require_interface_version(*_required_framework_version) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=architectures.LINUX_ARCHS, - ), requirements.VersionRequirement( name="linux_utilities_modules", component=Modules, @@ -941,25 +931,29 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): requirements.VersionRequirement( name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) ), - requirements.BooleanRequirement( - name="dump", - description="Extract listed modules", - default=False, - optional=True, - ), ] - def generator(self): + @classmethod + def generate_results( + cls, + context: interfaces.context.ContextInterface, + implementation: Callable[ + [interfaces.context.ContextInterface, str], Iterable[extensions.module] + ], + kernel_module_name: str, + dump: bool, + open_implementation: Optional[interfaces.plugins.FileHandlerInterface], + ): """ Uses the implementation set in the constructor call to produce consistent output fields across module gathering plugins """ - for module in self.implementation(self.context, self.config["kernel"]): + for module in implementation(context, kernel_module_name): try: name = utility.array_to_string(module.name) except exceptions.InvalidAddressException: vollog.debug( - f"Unable to recover name for module {module.vol.offset:#x} from implementation {self.implementation}" + f"Unable to recover name for module {module.vol.offset:#x} from implementation {implementation}" ) continue @@ -969,21 +963,21 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): taints = ",".join( tainting.Tainting.get_taints_parsed( - self.context, self.config["kernel"], module.taints, True + context, kernel_module_name, module.taints, True ) ) parameters_iter = Modules.get_load_parameters( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) parameters = ", ".join([f"{key}={value}" for key, value in parameters_iter]) file_name = renderers.NotApplicableValue() - if self.config["dump"]: + if dump and open_implementation: elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) if not elf_data: vollog.warning( @@ -991,31 +985,30 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): ) file_name = renderers.NotAvailableValue() else: - file_name = self.open.sanitize_filename( + file_name = open_implementation.sanitize_filename( f"kernel_module.{name}.{module.vol.offset:#x}.elf" ) - with self.open(file_name) as file_handle: + with open_implementation(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(module.vol.offset), - name, - format_hints.Hex(code_size), - taints, - parameters, - file_name, + yield ( + 0, + ( + format_hints.Hex(module.vol.offset), + name, + format_hints.Hex(code_size), + taints, + parameters, + file_name, + ), ) - def run(self): - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Module Name", str), - ("Code Size", format_hints.Hex), - ("Taints", str), - ("Load Arguments", str), - ("File Output", str), - ], - self._generator(), - ) + columns_results = [ + ("Offset", format_hints.Hex), + ("Module Name", str), + ("Code Size", format_hints.Hex), + ("Taints", str), + ("Load Arguments", str), + ("File Output", str), + ] diff --git a/volatility3/framework/symbols/windows/__init__.py b/volatility3/framework/symbols/windows/__init__.py index f9541579e..3296d7d2c 100755 --- a/volatility3/framework/symbols/windows/__init__.py +++ b/volatility3/framework/symbols/windows/__init__.py @@ -41,6 +41,7 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("_POOL_TRACKER_BIG_PAGES", pool.POOL_TRACKER_BIG_PAGES) self.set_type_class("_IMAGE_DOS_HEADER", pe.IMAGE_DOS_HEADER) self.set_type_class("_KTIMER", extensions.KTIMER) + self.set_type_class("_LDR_DATA_TABLE_ENTRY", extensions.LDR_DATA_TABLE_ENTRY) # Might not necessarily defined in every version of windows self.optional_set_type_class("_IMAGE_NT_HEADERS", pe.IMAGE_NT_HEADERS) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 4fc65564e..f32415124 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1710,3 +1710,16 @@ class SHARED_CACHE_MAP(objects.StructType): ) return vacb_list + + +class LDR_DATA_TABLE_ENTRY(objects.StructType): + def get_load_count(self) -> Optional[int]: + try: + LoadCount = self.LoadCount.cast("short") + except Exception: + try: + LoadCount = self.ObsoleteLoadCount.cast("short") + except Exception: + LoadCount = None + + return LoadCount diff --git a/volatility3/framework/symbols/windows/extensions/callbacks.py b/volatility3/framework/symbols/windows/extensions/callbacks.py index f54db39f2..855933f59 100644 --- a/volatility3/framework/symbols/windows/extensions/callbacks.py +++ b/volatility3/framework/symbols/windows/extensions/callbacks.py @@ -48,7 +48,6 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject): return False try: - device = self.DeviceObject if not device or not (device.DriverObject.DriverStart % 0x1000 == 0): vollog.debug( diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 0d39f8173..000c50319 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -220,7 +220,6 @@ class GUIExtensions(interfaces.configuration.VersionableInterface): break class tagWND(objects.StructType, pool.ExecutiveObject): - def is_valid(self) -> bool: """ Enforce a valid sid diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index bf20c1ffc..ddc21798f 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -54,7 +54,6 @@ class MFTEntry(objects.StructType): return max(names, key=lambda x: len(str(x))) def _attributes(self) -> Iterator["MFTAttribute"]: - # We will update this on each pass in the next loop and use it as the new offset. attr_base_offset = self.FirstAttrOffset attribute_object_type_name = ( diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 3b32d30c7..e7d92a48d 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -183,7 +183,6 @@ class SHIM_CACHE_ENTRY(objects.StructType): == self.ListEntry.Flink.Blink.dereference().vol.offset ) ): - return True else: return False diff --git a/volatility3/framework/symbols/windows/pdbconv.py b/volatility3/framework/symbols/windows/pdbconv.py index c23ffb350..5baf23fb2 100644 --- a/volatility3/framework/symbols/windows/pdbconv.py +++ b/volatility3/framework/symbols/windows/pdbconv.py @@ -489,7 +489,7 @@ class PdbReader: """Strips unnecessary components from the start of a symbol name.""" new_name = name - if new_name[:1] in ["_", "@", "\u007F"]: + if new_name[:1] in ["_", "@", "\u007f"]: new_name = new_name[1:] name_array = new_name.split("@") diff --git a/volatility3/plugins/__init__.py b/volatility3/plugins/__init__.py index 6afa8baf4..27fc0938c 100644 --- a/volatility3/plugins/__init__.py +++ b/volatility3/plugins/__init__.py @@ -12,6 +12,7 @@ are dependent upon, please DO NOT alter or remove this file unless you know the The framework is configured this way to allow plugin developers/users to override any plugin functionality whether existing or new. """ + from volatility3.framework import constants __path__ = constants.PLUGINS_PATH diff --git a/volatility3/plugins/linux/__init__.py b/volatility3/plugins/linux/__init__.py index 2d3e2386e..2ea8fb250 100644 --- a/volatility3/plugins/linux/__init__.py +++ b/volatility3/plugins/linux/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/mac/__init__.py b/volatility3/plugins/mac/__init__.py index 3ac3f1553..3f8e81ce1 100644 --- a/volatility3/plugins/mac/__init__.py +++ b/volatility3/plugins/mac/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/__init__.py b/volatility3/plugins/windows/__init__.py index d74f4fcd5..468493508 100644 --- a/volatility3/plugins/windows/__init__.py +++ b/volatility3/plugins/windows/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/registry/__init__.py b/volatility3/plugins/windows/registry/__init__.py index aeeaa87f2..f012a52a0 100644 --- a/volatility3/plugins/windows/registry/__init__.py +++ b/volatility3/plugins/windows/registry/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/symbols/__init__.py b/volatility3/symbols/__init__.py index c35f07cbe..162ea013e 100644 --- a/volatility3/symbols/__init__.py +++ b/volatility3/symbols/__init__.py @@ -6,6 +6,7 @@ This is the namespace for all volatility symbols, and determines the path for loading symbol ISF files """ + from volatility3.framework import constants __path__ = constants.SYMBOL_BASEPATHS