diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index e7c40607f..3df690543 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,4 +1,4 @@ -name: Black python linter +name: Black python formatter on: [push, pull_request] diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml new file mode 100644 index 000000000..7d9d86fb0 --- /dev/null +++ b/.github/workflows/build-pyinstaller.yml @@ -0,0 +1,51 @@ +name: build-pyinstaller +on: + push: + branches: + - stable + - develop + - "release/**" + pull_request: + branches: + - stable + - "release/**" + workflow_dispatch: + +jobs: + exe: + runs-on: windows-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -e .[full,cloud] + + - name: Pyinstall executable + run: | + pyinstaller --clean -y vol.spec + pyinstaller --clean -y volshell.spec + + - name: Move files + run: | + mv dist/vol.exe vol.exe + mv dist/volshell.exe volshell.exe + + - name: Archive + uses: actions/upload-artifact@v4 + with: + name: volatility3-pyinstaller + path: | + vol.exe + volshell.exe + README.md + LICENSE.txt diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e2e741a9f..c79f1c086 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [ "develop" ] + branches: ["develop"] pull_request: # The branches below must be a subset of the branches above - branches: [ "develop" ] + branches: ["develop"] # schedule: # - cron: '16 8 * * 0' @@ -32,43 +32,42 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - queries: security-and-quality # ,security-extended + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: security-and-quality # ,security-extended + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 398ff8ae3..9b9cbed4d 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -20,12 +20,8 @@ jobs: - name: Setup python-pip run: python -m pip install --upgrade pip - - name: Install dependencies - run: | - pip install -r requirements.txt - - name: Install volatility3 run: pip install . - name: Run volatility3 - run: vol --help \ No newline at end of file + run: vol --help diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml new file mode 100644 index 000000000..98a05a616 --- /dev/null +++ b/.github/workflows/ruff.yaml @@ -0,0 +1,15 @@ +--- +name: Ruff + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/ruff-action@v3.2.1 + with: + args: check + src: "." diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index acbb2af74..266e8bcc3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,54 +1,70 @@ name: Test Volatility3 on: [push, pull_request] jobs: - build: runs-on: ubuntu-22.04 strategy: matrix: python-version: ["3.8"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install Cmake - pip install build - pip install -r ./test/requirements-testing.txt + - name: Install dependencies + run: | + python -m pip install --upgrade pip Cmake build + pip install .[test] - - name: Build PyPi packages - run: | - python -m build + - name: Build PyPi packages + run: | + python -m build - - name: Download images - run: | - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz" - gunzip linux-sample-1.bin.gz - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz" - gunzip win-xp-laptop-2005-06-25.img.gz + - name: Download images + run: | + mkdir test_images + cd test_images + curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz" + gunzip linux-sample-1.bin.gz + curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz" + gunzip win-xp-laptop-2005-06-25.img.gz + curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-10_19041-2025_03.dmp.gz" + gunzip win-10_19041-2025_03.dmp.gz + cd - - - name: Download and Extract symbols - run: | - cd ./volatility3/symbols - curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip - unzip linux.zip - cd - + - name: Download and Extract symbols + run: | + cd ./volatility3/symbols + curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip + curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/symbols_win-10_19041-2025_03.zip + unzip linux.zip + unzip symbols_win-10_19041-2025_03.zip + cd - - - name: Testing... - run: | - py.test ./test/test_volatility.py --volatility=vol.py --image win-xp-laptop-2005-06-25.img -k test_windows -v - py.test ./test/test_volatility.py --volatility=vol.py --image linux-sample-1.bin -k test_linux -v + - name: Testing... + run: | + # VolShell + pytest --cov-append --cov-report=html --cov= ./test/plugins/windows/windows.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v + pytest --cov-append --cov-report=html --cov= ./test/plugins/linux/linux.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v - - name: Clean up post-test - run: | - rm -rf *.bin - rm -rf *.img - cd volatility3/symbols - rm -rf linux - rm -rf linux.zip - cd - + # Volatility + pytest --cov-append --cov-report=html --cov= ./test/plugins/windows/windows.py --volatility=vol.py --image=./test_images/win-10_19041-2025_03.dmp -k "test_windows and not test_windows_volshell" -v --durations=0 + pytest --cov-append --cov-report=html --cov= ./test/plugins/linux/linux.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v --durations=0 + + - name: Create coverage artifacts + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: htmlcov + overwrite: true + retention-days: 7 + + - name: Clean up post-test + run: | + rm -rf test_images + cd volatility3/symbols + rm -rf linux + rm -rf linux.zip + cd - diff --git a/.github/workflows/vol3-code-analysis.yml b/.github/workflows/vol3-code-analysis.yml new file mode 100644 index 000000000..fc2b297fd --- /dev/null +++ b/.github/workflows/vol3-code-analysis.yml @@ -0,0 +1,24 @@ +name: Volatility3 Code Analysis +on: [push, pull_request] +jobs: + + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.8"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + + - name: Testing... + run: | + python ./test/volatility3_code_analysis.py diff --git a/.gitignore b/.gitignore index c132736a9..c550db705 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ ENV/ # PyTest cache files .pytest_cache/ + +# Coverage cache +.coverage diff --git a/.readthedocs.yml b/.readthedocs.yml index e7c2b25d5..628e79ebf 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -20,4 +20,7 @@ build: # Optionally set the version of Python and requirements required to build your docs python: install: - - requirements: doc/requirements.txt + - method: pip + path: . + extra_requirements: + - docs diff --git a/API_CHANGES.md b/API_CHANGES.md index 61d8781fb..a4d8d9b13 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -4,6 +4,100 @@ API Changes When an addition to the existing API is made, the minor version is bumped. When an API feature or function is removed or changed, the major version is bumped. +2.25.0 +====== +Pointer class now supports `get_raw_value()`. +`KTIMER` no longer supports `get_raw_dpc()`. + +2.24.0 +====== +Support `encoding` parameter for `objects.utility.array_to_string` + +2.23.0 +====== +Add support for windows GUI classes and OS distinguishers. +Add a symbol_table_name for `ExecutiveObject.get_object_header()`/ + +2.22.0 +====== +Linux net constants added. +Network objects moved to separate versionable module. + +2.21.0 +====== +`uuid` method added to `linux.extensions`. + +2.20.0 +====== +NM_TYPES_DESC constants added to linux. +`latch_tree_root` and `kernel_symbol` added to linux extensions. +Linux `module` class additions: +* `get_module_address_boundaries` +* `section_typetab` +Linux `task_struct` class additions: +* `get_address_space_layer` +* `state` +Linux `bpf_prog` class additions: +* `bpf_jit_binary_hdr_address` + +2.19.0 +====== +Introduction of `Modules` versionable linux extension module. +Deprecation of some `LinuxUtilities` functions relating to modules. + +2.18.0 +====== +Addition of `scatterlist` linux extension. + +2.17.0 +====== +The addition of a `types` member to `SymbolInterface` + +2.16.0 +====== +Addition of TAINT_FLAG constants, `TaintFlag` dataclass +Addition of linux `tainting` versionable module + +2.15.0 +====== +Addition of `convert_fourcc_code` to `LinuxUtilities` class + +2.14.0 +====== +No significant changes (part of the 2.16.0 PR which took time in development) + +2.13.0 +====== +Linux `task` object extension addition of `getppid` + +2.12.0 +====== +Changes to the Intel layer to support `PROT_NONE` pages. + +2.11.0 +====== +Addition of `get_type` method to windows `CM_KEY_NODE` registry structure + +2.10.0 +====== +No significant API changes (CLI changes to the JSONL text renderer) + +2.9.0 +===== +No significant API changes (change to call `linux.LinuxUtilities.get_module_from_volobj_type` to get the kernel) + +2.8.0 +===== +Addition of the `BinOrAbsent`, `HexOrAbsent`, `HexBytesOrAbsent` and `MultiTypeDataOrAbsent` data type renderers + +2.7.0 +===== +Addition of `is_valid`, `get_create_time` and `get_exit_time` to ETHREAD structure + +2.6.0 +===== +No significant changes (again, the version got bump twice in the PR straight to 2.7.0) + 2.5.0 ===== Add in support for specifying a type override for object_from_symbol @@ -50,5 +144,3 @@ an absolute offset. This can be done with `Module.get_absolute_symbol_address` * Added context.modules * Added ModuleRequirement * Added get\_symbols\_by\_absolute\_location - - diff --git a/CODING_STYLE.md b/CODING_STYLE.md new file mode 100644 index 000000000..a4e248ffe --- /dev/null +++ b/CODING_STYLE.md @@ -0,0 +1,104 @@ +Coding Standards +================ + +The coding standards for volatility are mostly by our linter and our code formatter. +All code submissions will be vetted automatically through tests from both and the submission will not be accepted if either of these fail. + +Code Linter: Ruff +Code Formatter: Black + +In addition, there are some coding practices that we employ to prevent specific failure cases and ensure consistency across the codebase. These are documented below along with the rationale for the decision. + +This is heavily based upon https://google.github.io/styleguide/pyguide.html with minor modifications for volatility use. + +Imports +------- + +Use import statements for packages and modules only, not for individual types, classes, or functions and ideally not aliased unless the imported name would cause confusion. This is to prevent people from importing something that was itself imported from elsewhere (which can lead to confusion and add in an unnecessary dependency in the import chain). + +* Use `import x` for importing packages and modules. +* Use `from x import y` where x is the package prefix and y is the module name with no prefix. +* Use `from x import y as z` in any of the following circumstances: + * Two modules named `y` are to be imported. + * `y` conflicts with a top-level name defined in the current module. + * `y` conflicts with a common parameter name that is part of the public API (e.g., `features`). + * `y` is an inconveniently long name. + * `y` is too generic in the context of your code (e.g., `from storage.file_system import options as fs_options`). + +Exemptions from this rule: + + * Symbols from the following modules are used to support static analysis and type checking: + * `typing` module + * `collections.abc` module + * `typing_extensions` module + +Function calls +-------------- + +For longer function calls, where line length is no longer an issue, favour using keyword arguments for clarity over unnamed positional arguments. +This helps coders learning the code from examples to know what parameters to pass in and avoids ordering mistakes. + +Global Mutable State +-------------------- + +Avoid mutable global state. + +In those rare cases where using global state is warranted, mutable global entities should be declared at the module level or as a class attribute and made internal by prepending an _ to the name. If necessary, external access to mutable global state must be done through public functions or class methods. See Naming below. Please explain the design reasons why mutable global state is being used in a comment or a doc linked to from a comment. + +Module-level constants are permitted and encouraged. For example: _MAX_HOLY_HANDGRENADE_COUNT = 3 for an internal use constant or SIR_LANCELOTS_FAVORITE_COLOR = "blue" for a public API constant. Constants must be named using all caps with underscores. See Naming below. + +Exceptions +---------- + +Never use catch-all except: statements, or catch Exception or StandardError, unless you are + + * re-raising the exception, or + * creating an isolation point in the program where exceptions are not propagated but are recorded and suppressed instead, such as protecting a thread from crashing by guarding its outermost block. + +Python is very tolerant in this regard and except: will really catch everything including misspelled names, sys.exit() calls, Ctrl+C interrupts, unittest failures and all kinds of other exceptions that you simply don’t want to catch. + +Versioning +---------- + +Modules that inherit from `VersionableInterface` define a `_version` attribute which states their version. This is a tuple of `(MAJOR, MINOR, PATCH)` numbers, which can then be used for Semantic Versioning (where modifications that change the API in a non-backwards compatible way bump the `MAJOR` version (and set the `MINOR` and `PATCH` to 0) and additive changes increase the `MINOR` version (and set the `PATCH` to 0). Changes that have no effect on the external interface (either input or output form) should have their `PATCH` number incremented. This allows for callers of the interface to determine when changes have happened and whether their code will still work with it. Volatility carries out these checks through the requirements system, where a plugin can define what requirements it has. + +Shared functionality +-------------------- + +Within a plugin, there may be functions that are useful to other plugins. These are created as `classmethod`s so that the plugin can be depended upon by other plugins in their requirements section, without needing to instantiate a whole copy of the plugin. It is not a staticmethod, because the caller may wish to determine information about the class the method is defined in, and this is not easily accessible for staticmethods. +A classmethod usually takes a `context` for its first method (and if it requires one, a configuration string for it second). All other parameters should generally be basic types (such as strings, numbers, etc) so that future work requiring parallelization does not have complex types to have to keep in sync. In particular, the idea was to ensure only one context was used per method (and each object brings its own context with it, meaning the function signature should not include objects to avoid discrepancies). + +Comprehensions +-------------- + +Comprehensions are allowed, however multiple for clauses or filter expressions are not permitted. Optimize for readability, not conciseness. + +Lambda functions +---------------- + +Okay for one-liners. Prefer generator expressions over map() or filter() with a lambda. + +Default Arguments +----------------- + +Default arguments are fine, but not with mutable types (because they're constructed once at module load time and can lead to confusion/errors.) + +Format strings +-------------- +Generally f-strings are preferred, and where possible a format modifier should be used over a separate method call. As an example, hex output should be `f"0x{offset:x}"` rather than `f"{hex(offset)}"`. +F-strings should be used over other formatting methods *except* in cases of logging where the f-string gets calculated/executed whether the log message is displayed or not (where as parameters are not evaluated if not needed). +The ruff linter should alert about these situations and exceptions can be maded if needed. + +True/False Evaluations +---------------------- + +Use the “implicit” false if possible, e.g., if foo: rather than if foo != []:. There are a few caveats that you should keep in mind though: + + * Always use `if foo is None:` (or `is not None`) to check for a `None` value. E.g., when testing whether a variable or argument that defaults to `None` was set to some other value. The other value might be a value that’s false in a boolean context! + * Never compare a boolean variable to `False` using `==`. Use `if not x:` instead. If you need to distinguish `False` from `None` then chain the expressions, such as `if not x and x is not None:`. + * For sequences (strings, lists, tuples), use the fact that empty sequences are false, so `if seq:` and `if not seq:` are preferable to `if len(seq):` and `if not len(seq):` respectively. + +Logging +------- + +We do allow f-string usage in log messages, although technically it should be avoided since it will be evaluated even if the log message is never emitted. diff --git a/MANIFEST.in b/MANIFEST.in index 1cec729f6..863621381 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ prune development include * .* -include doc/make.bat doc/Makefile doc/requirements.txt +include pyproject.toml doc/make.bat doc/Makefile recursive-include doc/source * recursive-include volatility3 *.json recursive-exclude doc/source volatility3.*.rst diff --git a/README.md b/README.md index 1463c2bde..79401a18f 100644 --- a/README.md +++ b/README.md @@ -14,80 +14,70 @@ technical and performance challenges associated with the original code base that became apparent over the previous 10 years. Another benefit of the rewrite is that Volatility 3 could be released under a custom license that was more aligned with the goals of the Volatility community, -the Volatility Software License (VSL). See the -[LICENSE](https://www.volatilityfoundation.org/license/vsl-v1.0) file for +the Volatility Software License (VSL). See the +[LICENSE](https://www.volatilityfoundation.org/license/vsl-v1.0) file for more details. -## Requirements - -Volatility 3 requires Python 3.8.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as: - -```shell -pip3 install -r requirements-minimal.txt -``` - -Alternately, the minimal packages will be installed automatically when Volatility 3 is installed using pip. However, as noted in the Quick Start section below, Volatility 3 does not *need* to be installed prior to using it. - -```shell -pip3 install . -``` - -To enable the full range of Volatility 3 functionality, use a command like the one below. For partial functionality, comment out any unnecessary packages in [requirements.txt](requirements.txt) prior to running the command. - -```shell -pip3 install -r requirements.txt -``` - -## Downloading Volatility - -The latest stable version of Volatility will always be the stable branch of the GitHub repository. You can get the latest version of the code using the following command: - -```shell -git clone https://github.com/volatilityfoundation/volatility3.git -``` - ## Quick Start -1. Clone the latest version of Volatility from GitHub: +1. Install the required dependencies: ```shell - git clone https://github.com/volatilityfoundation/volatility3.git + pip install --user -e ".[full]" ``` 2. See available options: ```shell - python3 vol.py -h + vol -h ``` -3. To get more information on a Windows memory sample and to make sure -Volatility supports that sample type, run -`python3 vol.py -f windows.info` - - Example: +3. To get more information on a Windows memory sample and to make sure Volatility supports that sample type, run `vol -f windows.info`: ```shell - python3 vol.py -f /home/user/samples/stuxnet.vmem windows.info + vol -f /home/user/samples/stuxnet.vmem windows.info ``` -4. Run some other plugins. The `-f` or `--single-location` is not strictly -required, but most plugins expect a single sample. Some also -require/accept other options. Run `python3 vol.py -h` -for more information on a particular command. +4. Run some other plugins. The `-f` or `--single-location` is not strictly required, but most plugins expect a single sample. +Some also require/accept other options. Run `vol -h` for more information on a particular command. + +## Installing + +Volatility 3 requires Python 3.8.0 or later and is published on the [PyPi registry](https://pypi.org/project/volatility3). + +```shell +pip install volatility3 +``` + +If you want to use the latest development version of Volatility 3 we recommend you manually clone this repository and install an editable version of the project. +We recommend you use a virtual environment to keep installed dependencies separate from system packages. + +The latest stable version of Volatility will always be the `stable` branch of the GitHub repository. The default branch is `develop`. + +```shell +git clone https://github.com/volatilityfoundation/volatility3.git +cd volatility3/ +python3 -m venv venv && . venv/bin/activate +pip install -e ".[dev]" +``` ## Symbol Tables 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). @@ -106,7 +96,7 @@ The latest generated copy of the documentation can be found at: List[str]: return [self.long_name] - def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None: + def create_prerequisites( + self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str + ) -> None: pass - def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> List[float]: + def create_results( + self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str + ) -> List[float]: self.create_prerequisites(plugin, image, image_hash) # Volatility 2 Test - print(f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}") + print( + f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}" + ) os.chdir(self.path) cmd = self.plugin_cmd(plugin, image) start_time = time.perf_counter() try: - completed = subprocess.run(cmd, cwd = self.path, capture_output = True, timeout = 420) + completed = subprocess.run( + cmd, cwd=self.path, capture_output=True, timeout=420 + ) except subprocess.TimeoutExpired as excp: completed = excp end_time = time.perf_counter() total_time = end_time - start_time - print(f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}") + print( + f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}" + ) with open( - os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stdout'), - "wb") as f: + os.path.join( + self.output_directory, + f"{self.short_name}_{plugin.name}_{image_hash}_stdout", + ), + "wb", + ) as f: f.write(completed.stdout) if completed.stderr: with open( - os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stderr'), - "wb") as f: + os.path.join( + self.output_directory, + f"{self.short_name}_{plugin.name}_{image_hash}_stderr", + ), + "wb", + ) as f: f.write(completed.stderr) return [total_time] @@ -77,31 +95,57 @@ class Volatility2Test(VolatilityTest): long_name = "Volatility 2" def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage): - return ["python2", "-u", "vol.py", "-f", image.filepath, "--profile", image.vol2_profile - ] + plugin.vol2_plugin_parameters + image.vol2_plugin_parameters.get(plugin.name, []) + return ( + [ + "python2", + "-u", + "vol.py", + "-f", + image.filepath, + "--profile", + image.vol2_profile, + ] + + plugin.vol2_plugin_parameters + + image.vol2_plugin_parameters.get(plugin.name, []) + ) def result_titles(self): return [self.long_name, "Imageinfo", f"{self.long_name} + Imageinfo"] - def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash) -> List[float]: + def create_results( + self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash + ) -> List[float]: result = super().create_results(plugin, image, image_hash) result += [image.vol2_imageinfo_time, result[0] + image.vol2_imageinfo_time] return result - def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash): + def create_prerequisites( + self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash + ): # Volatility 2 image info if not image.vol2_profile: - print(f"[*] Testing {self.short_name} imageinfo with image {image.filepath}") + print( + f"[*] Testing {self.short_name} imageinfo with image {image.filepath}" + ) os.chdir(self.path) cmd = ["python2", "-u", "vol.py", "-f", image.filepath, "imageinfo"] start_time = time.perf_counter() - vol2_completed = subprocess.run(cmd, cwd = self.path, capture_output = True) + vol2_completed = subprocess.run(cmd, cwd=self.path, capture_output=True) end_time = time.perf_counter() image.vol2_imageinfo_time = end_time - start_time - print(f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}") - with open(os.path.join(self.output_directory, f'vol2_imageinfo_{image_hash}_stdout'), "wb") as f: + print( + f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}" + ) + with open( + os.path.join( + self.output_directory, f"vol2_imageinfo_{image_hash}_stdout" + ), + "wb", + ) as f: f.write(vol2_completed.stdout) - image.vol2_profile = re.search(b"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout)[1] + image.vol2_profile = re.search( + rb"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout + )[1] class RekallTest(VolatilityTest): @@ -113,11 +157,16 @@ class RekallTest(VolatilityTest): plugin.rekall_plugin_parameters = plugin.vol2_plugin_parameters if not image.rekall_plugin_parameters: image.rekall_plugin_parameters = image.vol2_plugin_parameters - return ["rekall", "-f", image.filepath] + plugin.rekall_plugin_parameters + image.rekall_plugin_parameters.get( - plugin.name, []) + return ( + ["rekall", "-f", image.filepath] + + plugin.rekall_plugin_parameters + + image.rekall_plugin_parameters.get(plugin.name, []) + ) - def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None: - shutil.rmtree('/home/mike/.rekall_cache/sessions') + def create_prerequisites( + self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str + ) -> None: + shutil.rmtree("/home/mike/.rekall_cache/sessions") class Volatility3Test(VolatilityTest): @@ -125,14 +174,18 @@ class Volatility3Test(VolatilityTest): long_name = "Volatility 3" def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]: - return [ - "python", - "-u", - "vol.py", - "-q", - "-f", - image.filepath, - ] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, []) + return ( + [ + "python", + "-u", + "vol.py", + "-q", + "-f", + image.filepath, + ] + + plugin.vol3_plugin_parameters + + image.vol3_plugin_parameters.get(plugin.name, []) + ) class Volatility3PyPyTest(VolatilityTest): @@ -140,26 +193,31 @@ class Volatility3PyPyTest(VolatilityTest): long_name = "Volatility 3 (PyPy)" def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]: - return [ - "pypy3", - "-u", - "vol.py", - "-q", - "-f", - image.filepath, - ] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, []) + return ( + [ + "pypy3", + "-u", + "vol.py", + "-q", + "-f", + image.filepath, + ] + + plugin.vol3_plugin_parameters + + image.vol3_plugin_parameters.get(plugin.name, []) + ) class VolatilityTester: - - def __init__(self, - images: List[VolatilityImage], - plugins: List[VolatilityPlugin], - frameworks: List[str], - output_dir: str, - vol2_path: str = None, - vol3_path: str = None, - rekall_path = None): + def __init__( + self, + images: List[VolatilityImage], + plugins: List[VolatilityPlugin], + frameworks: List[str], + output_dir: str, + vol2_path: str = None, + vol3_path: str = None, + rekall_path=None, + ): self.images = images self.plugins = plugins if not vol2_path: @@ -172,7 +230,7 @@ class VolatilityTester: Volatility3Test(vol3_path, output_dir), Volatility3PyPyTest(vol3_path, output_dir), Volatility2Test(vol2_path, output_dir), - RekallTest(rekall_path, output_dir) + RekallTest(rekall_path, output_dir), ] self.tests = [x for x in available_tests if x.short_name.lower() in frameworks] self.csv_writer = None @@ -183,7 +241,7 @@ class VolatilityTester: print(f"[?] Frameworks: {[x.long_name for x in self.tests]}") def run_tests(self): - with open("volatility-timings.csv", 'w') as csvfile: + with open("volatility-timings.csv", "w") as csvfile: self.csv_writer = csv.writer(csvfile) titles = ["Image Hash", "Image Path", "Plugin Name"] for test in self.tests: @@ -203,72 +261,121 @@ class VolatilityTester: self.csv_writer.writerow([image_hash, image.filepath, plugin.name] + results) -if __name__ == '__main__': +if __name__ == "__main__": plugins = [ - VolatilityPlugin(name = "pslist", - vol2_plugin_parameters = ["pslist"], - vol3_plugin_parameters = ["windows.pslist"]), - VolatilityPlugin(name = "psscan", - vol2_plugin_parameters = ["psscan"], - vol3_plugin_parameters = ["windows.psscan"], - rekall_plugin_parameters = ["psscan", "--scan_kernel"]), - VolatilityPlugin(name = "driverscan", - vol2_plugin_parameters = ["driverscan"], - vol3_plugin_parameters = ["windows.driverscan"], - rekall_plugin_parameters = ["driverscan", "--scan_kernel"]), - VolatilityPlugin(name = "handles", - vol2_plugin_parameters = ["handles"], - vol3_plugin_parameters = ["windows.handles"]), - VolatilityPlugin(name = "modules", - vol2_plugin_parameters = ["modules"], - vol3_plugin_parameters = ["windows.modules"]), - VolatilityPlugin(name = "hivelist", - vol2_plugin_parameters = ["hivelist"], - vol3_plugin_parameters = ["registry.hivelist"], - rekall_plugin_parameters = ["hives"]), - VolatilityPlugin(name = "vadinfo", - vol2_plugin_parameters = ["vadinfo"], - vol3_plugin_parameters = ["windows.vadinfo"], - rekall_plugin_parameters = ["vad"]), - VolatilityPlugin(name = "modscan", - vol2_plugin_parameters = ["modscan"], - vol3_plugin_parameters = ["windows.modscan"], - rekall_plugin_parameters = ["modscan", "--scan_kernel"]), - VolatilityPlugin(name = "svcscan", - vol2_plugin_parameters = ["svcscan"], - vol3_plugin_parameters = ["windows.svcscan"], - rekall_plugin_parameters = ["svcscan"]), - VolatilityPlugin(name = "ssdt", vol2_plugin_parameters = ["ssdt"], vol3_plugin_parameters = ["windows.ssdt"]), - VolatilityPlugin(name = "printkey", - vol2_plugin_parameters = ["printkey", "-K", "Classes"], - vol3_plugin_parameters = ["registry.printkey", "--key", "Classes"], - rekall_plugin_parameters = ["printkey", "--key", "Classes"]) + VolatilityPlugin( + name="pslist", + vol2_plugin_parameters=["pslist"], + vol3_plugin_parameters=["windows.pslist"], + ), + VolatilityPlugin( + name="psscan", + vol2_plugin_parameters=["psscan"], + vol3_plugin_parameters=["windows.psscan"], + rekall_plugin_parameters=["psscan", "--scan_kernel"], + ), + VolatilityPlugin( + name="driverscan", + vol2_plugin_parameters=["driverscan"], + vol3_plugin_parameters=["windows.driverscan"], + rekall_plugin_parameters=["driverscan", "--scan_kernel"], + ), + VolatilityPlugin( + name="handles", + vol2_plugin_parameters=["handles"], + vol3_plugin_parameters=["windows.handles"], + ), + VolatilityPlugin( + name="modules", + vol2_plugin_parameters=["modules"], + vol3_plugin_parameters=["windows.modules"], + ), + VolatilityPlugin( + name="hivelist", + vol2_plugin_parameters=["hivelist"], + vol3_plugin_parameters=["registry.hivelist"], + rekall_plugin_parameters=["hives"], + ), + VolatilityPlugin( + name="vadinfo", + vol2_plugin_parameters=["vadinfo"], + vol3_plugin_parameters=["windows.vadinfo"], + rekall_plugin_parameters=["vad"], + ), + VolatilityPlugin( + name="modscan", + vol2_plugin_parameters=["modscan"], + vol3_plugin_parameters=["windows.modscan"], + rekall_plugin_parameters=["modscan", "--scan_kernel"], + ), + VolatilityPlugin( + name="svcscan", + vol2_plugin_parameters=["svcscan"], + vol3_plugin_parameters=["windows.svcscan"], + rekall_plugin_parameters=["svcscan"], + ), + VolatilityPlugin( + name="ssdt", + vol2_plugin_parameters=["ssdt"], + vol3_plugin_parameters=["windows.ssdt"], + ), + VolatilityPlugin( + name="printkey", + vol2_plugin_parameters=["printkey", "-K", "Classes"], + vol3_plugin_parameters=["registry.printkey", "--key", "Classes"], + rekall_plugin_parameters=["printkey", "--key", "Classes"], + ), ] parser = argparse.ArgumentParser() - parser.add_argument("--output-dir", type = str, default = os.getcwd(), help = "Directory to store all results") - parser.add_argument("--vol3path", - type = str, - default = os.path.join(os.getcwd(), 'volatility3'), - help = "Path ot the volatility 3 directory") - parser.add_argument("--vol2path", - type = str, - default = os.path.join(os.getcwd(), 'volatility'), - help = "Path to the volatility 2 directory") - parser.add_argument("--rekallpath", - type = str, - default = os.path.join(os.getcwd(), 'rekall'), - help = "Path to the rekall directory") - parser.add_argument("--frameworks", - nargs = "+", - type = str, - choices = [x.short_name.lower() for x in VolatilityTest.__subclasses__()], - default = [x.short_name.lower() for x in VolatilityTest.__subclasses__()], - help = "A comma separated list of frameworks to test") - parser.add_argument('images', metavar = 'IMAGE', type = str, nargs = '+', help = 'The list of images to compare') + parser.add_argument( + "--output-dir", + type=str, + default=os.getcwd(), + help="Directory to store all results", + ) + parser.add_argument( + "--vol3path", + type=str, + default=os.path.join(os.getcwd(), "volatility3"), + help="Path to the volatility 3 directory", + ) + parser.add_argument( + "--vol2path", + type=str, + default=os.path.join(os.getcwd(), "volatility"), + help="Path to the volatility 2 directory", + ) + parser.add_argument( + "--rekallpath", + type=str, + default=os.path.join(os.getcwd(), "rekall"), + help="Path to the rekall directory", + ) + parser.add_argument( + "--frameworks", + nargs="+", + type=str, + choices=[x.short_name.lower() for x in VolatilityTest.__subclasses__()], + default=[x.short_name.lower() for x in VolatilityTest.__subclasses__()], + help="A comma separated list of frameworks to test", + ) + parser.add_argument( + "images", + metavar="IMAGE", + type=str, + nargs="+", + help="The list of images to compare", + ) args = parser.parse_args() - vt = VolatilityTester([VolatilityImage(filepath = x) for x in args.images], plugins, - [x.lower() for x in args.frameworks], args.output_dir, args.vol2path, args.vol3path, - args.rekallpath) + vt = VolatilityTester( + [VolatilityImage(filepath=x) for x in args.images], + plugins, + [x.lower() for x in args.frameworks], + args.output_dir, + args.vol2path, + args.vol3path, + args.rekallpath, + ) vt.run_tests() diff --git a/development/mac-kdk/parse_pbzx2.py b/development/mac-kdk/parse_pbzx2.py index 173a4d648..b175539b3 100644 --- a/development/mac-kdk/parse_pbzx2.py +++ b/development/mac-kdk/parse_pbzx2.py @@ -7,11 +7,12 @@ # Cleaned up C version (as the basis for my code) here, thanks to Pepijn Bruienne / @bruienne # https://gist.github.com/bruienne/029494bbcfb358098b41 +import os import struct import sys -def seekread(f, offset = None, length = 0, relative = True): +def seekread(f, offset=None, length=0, relative=True): if offset is not None: # offset provided, let's seek f.seek(offset, [0, 1, 2][relative]) @@ -22,55 +23,57 @@ def seekread(f, offset = None, length = 0, relative = True): def parse_pbzx(pbzx_path): section = 0 - xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section) - with open(pbzx_path, 'rb') as f: + xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz" + with open(pbzx_path, "rb") as f: # pbzx = f.read() # f.close() - magic = seekread(f, length = 4) - if magic != 'pbzx': + magic = seekread(f, length=4) + if magic != "pbzx": raise RuntimeError("Error: Not a pbzx file") # Read 8 bytes for initial flags - flags = seekread(f, length = 8) + flags = seekread(f, length=8) # Interpret the flags as a 64-bit big-endian unsigned int - flags = struct.unpack('>Q', flags)[0] + flags = struct.unpack(">Q", flags)[0] while flags & (1 << 24): - with open(xar_out_path, 'wb') as xar_f: + with open(xar_out_path, "wb") as xar_f: xar_f.seek(0, os.SEEK_END) # Read in more flags - flags = seekread(f, length = 8) - flags = struct.unpack('>Q', flags)[0] + flags = seekread(f, length=8) + flags = struct.unpack(">Q", flags)[0] # Read in length - f_length = seekread(f, length = 8) - f_length = struct.unpack('>Q', f_length)[0] - xzmagic = seekread(f, length = 6) - if xzmagic != '\xfd7zXZ\x00': + f_length = seekread(f, length=8) + f_length = struct.unpack(">Q", f_length)[0] + xzmagic = seekread(f, length=6) + if xzmagic != "\xfd7zXZ\x00": # This isn't xz content, this is actually _raw decompressed cpio_ chunk of 16MB in size... # Let's back up ... - seekread(f, offset = -6, length = 0) + seekread(f, offset=-6, length=0) # ... and split it out ... - f_content = seekread(f, length = f_length) + f_content = seekread(f, length=f_length) section += 1 - decomp_out = '%s.part%02d.cpio' % (pbzx_path, section) - with open(decomp_out, 'wb') as g: + decomp_out = f"{pbzx_path}.part{section:02d}.cpio" + with open(decomp_out, "wb") as g: g.write(f_content) # Now to start the next section, which should hopefully be .xz (we'll just assume it is ...) section += 1 - xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section) + xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz" else: f_length -= 6 # This part needs buffering - f_content = seekread(f, length = f_length) - tail = seekread(f, offset = -2, length = 2) + f_content = seekread(f, length=f_length) + tail = seekread(f, offset=-2, length=2) xar_f.write(xzmagic) xar_f.write(f_content) - if tail != 'YZ': + if tail != "YZ": raise RuntimeError("Error: Footer is not xar file footer") def main(): parse_pbzx(sys.argv[1]) - print("Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file") + print( + "Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file" + ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/development/pdbparse-to-json.py b/development/pdbparse-to-json.py index 819e44e15..fe4b8ab60 100644 --- a/development/pdbparse-to-json.py +++ b/development/pdbparse-to-json.py @@ -13,31 +13,30 @@ import pdbparse.undecorate logger = logging.getLogger(__name__) logger.setLevel(1) -if __name__ == '__main__': +if __name__ == "__main__": console = logging.StreamHandler() console.setLevel(1) - formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s') + formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s") console.setFormatter(formatter) logger.addHandler(console) class PDBRetreiver: - def retreive_pdb(self, guid: str, file_name: str) -> Optional[str]: logger.info("Download PDB file...") - file_name = ".".join(file_name.split(".")[:-1] + ['pdb']) - for sym_url in ['http://msdl.microsoft.com/download/symbols']: + file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) + for sym_url in ["http://msdl.microsoft.com/download/symbols"]: url = sym_url + f"/{file_name}/{guid}/" result = None - for suffix in [file_name[:-1] + '_', file_name]: + for suffix in [file_name[:-1] + "_", file_name]: try: - logger.debug(f"Attempting to retrieve {url + suffix}") + logger.debug("Attempting to retrieve %s", url + suffix) result, _ = request.urlretrieve(url + suffix) except request.HTTPError as excp: - logger.debug(f"Failed with {excp}") + logger.debug("Failed with %s", excp) if result: - logger.debug(f"Successfully written to {result}") + logger.debug("Successfully written to %s", result) break return result @@ -69,7 +68,7 @@ class PDBConvertor: "float": "float", "double": "float", "long double": "float", - "void": "void" + "void": "void", } base_type_size = { @@ -122,13 +121,18 @@ class PDBConvertor: self._seen_ctypes.add(ctype) return self.ctype[ctype] - def lookup_ctype_pointers(self, ctype_pointer: str) -> Dict[str, Union[str, Dict[str, str]]]: - base_type = ctype_pointer.replace('32P', '').replace('64P', '') + def lookup_ctype_pointers( + self, ctype_pointer: str + ) -> Dict[str, Union[str, Dict[str, str]]]: + base_type = ctype_pointer.replace("32P", "").replace("64P", "") if base_type == ctype_pointer: # We raise a KeyError, because we've been asked about a type that isn't a pointer raise KeyError self._seen_ctypes.add(base_type) - return {"kind": "pointer", "subtype": {"kind": "base", "name": self.ctype[base_type]}} + return { + "kind": "pointer", + "subtype": {"kind": "base", "name": self.ctype[base_type]}, + } def read_pdb(self) -> Dict: """Reads in the PDB file and forms essentially a python dictionary of necessary data""" @@ -137,32 +141,31 @@ class PDBConvertor: "enums": self.read_enums(), "metadata": self.generate_metadata(), "symbols": self.read_symbols(), - "base_types": self.read_basetypes() + "base_types": self.read_basetypes(), } return output def generate_metadata(self) -> Dict[str, Any]: """Generates the metadata necessary for this object""" dbg = self._pdb.STREAM_DBI - last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), 'ascii')[-16:] - guidstr = u'{:08x}{:04x}{:04x}{}'.format(self._pdb.STREAM_PDB.GUID.Data1, self._pdb.STREAM_PDB.GUID.Data2, - self._pdb.STREAM_PDB.GUID.Data3, last_bytes) + last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), "ascii")[ + -16: + ] + guidstr = f"{self._pdb.STREAM_PDB.GUID.Data1:08x}{self._pdb.STREAM_PDB.GUID.Data2:04x}{self._pdb.STREAM_PDB.GUID.Data3:04x}{last_bytes}" pdb_data = { "GUID": guidstr.upper(), "age": self._pdb.STREAM_PDB.Age, "database": "ntkrnlmp.pdb", - "machine_type": int(dbg.machine) + "machine_type": int(dbg.machine), } result = { "format": "6.0.0", "producer": { "datetime": datetime.datetime.now().isoformat(), "name": "pdbconv", - "version": "0.1.0" + "version": "0.1.0", }, - "windows": { - "pdb": pdb_data - } + "windows": {"pdb": pdb_data}, } return result @@ -173,16 +176,21 @@ class PDBConvertor: stream = self._pdb.STREAM_TPI for type_index in stream.types: user_type = stream.types[type_index] - if (user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref): + if user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref: output.update(self._format_enum(user_type)) return output def _format_enum(self, user_enum): output = { user_enum.name: { - 'base': self.lookup_ctype(user_enum.utype), - 'size': self._determine_size(user_enum.utype), - 'constants': dict([(enum.name, enum.enum_value) for enum in user_enum.fieldlist.substructs]) + "base": self.lookup_ctype(user_enum.utype), + "size": self._determine_size(user_enum.utype), + "constants": dict( + [ + (enum.name, enum.enum_value) + for enum in user_enum.fieldlist.substructs + ] + ), } } return output @@ -195,14 +203,14 @@ class PDBConvertor: try: sects = self._pdb.STREAM_SECT_HDR_ORIG.sections omap = self._pdb.STREAM_OMAP_FROM_SRC - except AttributeError as e: + except AttributeError: # In this case there is no OMAP, so we use the given section # headers and use the identity function for omap.remap sects = self._pdb.STREAM_SECT_HDR.sections omap = None for sym in self._pdb.STREAM_GSYM.globals: - if not hasattr(sym, 'offset'): + if not hasattr(sym, "offset"): continue try: virt_base = sects[sym.segment - 1].VirtualAddress @@ -223,9 +231,9 @@ class PDBConvertor: stream = self._pdb.STREAM_TPI for type_index in stream.types: user_type = stream.types[type_index] - if (user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref): + if user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref: output.update(self._format_usertype(user_type, "struct")) - elif (user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref): + elif user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref: output.update(self._format_usertype(user_type, "union")) return output @@ -233,16 +241,22 @@ class PDBConvertor: """Produces a single usertype""" fields: Dict[str, Dict[str, Any]] = {} [fields.update(self._format_field(s)) for s in usertype.fieldlist.substructs] - return {usertype.name: {'fields': fields, 'kind': kind, 'size': usertype.size}} + return {usertype.name: {"fields": fields, "kind": kind, "size": usertype.size}} def _format_field(self, field) -> Dict[str, Dict[str, Any]]: - return {field.name: {"offset": field.offset, "type": self._format_kind(field.index)}} + return { + field.name: {"offset": field.offset, "type": self._format_kind(field.index)} + } def _determine_size(self, field): output = None if isinstance(field, str): output = self.base_type_size[field] - elif (field.leaf_type == "LF_STRUCTURE" or field.leaf_type == "LF_ARRAY" or field.leaf_type == "LF_UNION"): + elif ( + field.leaf_type == "LF_STRUCTURE" + or field.leaf_type == "LF_ARRAY" + or field.leaf_type == "LF_UNION" + ): output = field.size elif field.leaf_type == "LF_POINTER": output = self.base_type_size[field.ptr_attr.type] @@ -256,6 +270,7 @@ class PDBConvertor: output = self._determine_size(field.index) if output is None: import pdb + pdb.set_trace() raise ValueError(f"Unknown size for field: {field.name}") return output @@ -267,36 +282,37 @@ class PDBConvertor: output = self.lookup_ctype_pointers(kind) except KeyError: try: - output = {'kind': 'base', 'name': self.lookup_ctype(kind)} + output = {"kind": "base", "name": self.lookup_ctype(kind)} except KeyError: - output = {'kind': 'base', 'name': kind} - elif kind.leaf_type == 'LF_MODIFIER': + output = {"kind": "base", "name": kind} + elif kind.leaf_type == "LF_MODIFIER": output = self._format_kind(kind.modified_type) - elif kind.leaf_type == 'LF_STRUCTURE': - output = {'kind': 'struct', 'name': kind.name} - elif kind.leaf_type == 'LF_UNION': - output = {'kind': 'union', 'name': kind.name} - elif kind.leaf_type == 'LF_BITFIELD': + elif kind.leaf_type == "LF_STRUCTURE": + output = {"kind": "struct", "name": kind.name} + elif kind.leaf_type == "LF_UNION": + output = {"kind": "union", "name": kind.name} + elif kind.leaf_type == "LF_BITFIELD": output = { - 'kind': 'bitfield', - 'type': self._format_kind(kind.base_type), - 'bit_length': kind.length, - 'bit_position': kind.position + "kind": "bitfield", + "type": self._format_kind(kind.base_type), + "bit_length": kind.length, + "bit_position": kind.position, } - elif kind.leaf_type == 'LF_POINTER': - output = {'kind': 'pointer', 'subtype': self._format_kind(kind.utype)} - elif kind.leaf_type == 'LF_ARRAY': + elif kind.leaf_type == "LF_POINTER": + output = {"kind": "pointer", "subtype": self._format_kind(kind.utype)} + elif kind.leaf_type == "LF_ARRAY": output = { - 'kind': 'array', - 'count': kind.size // self._determine_size(kind.element_type), - 'subtype': self._format_kind(kind.element_type) + "kind": "array", + "count": kind.size // self._determine_size(kind.element_type), + "subtype": self._format_kind(kind.element_type), } - elif kind.leaf_type == 'LF_ENUM': - output = {'kind': 'enum', 'name': kind.name} - elif kind.leaf_type == 'LF_PROCEDURE': - output = {'kind': "function"} + elif kind.leaf_type == "LF_ENUM": + output = {"kind": "enum", "name": kind.name} + elif kind.leaf_type == "LF_PROCEDURE": + output = {"kind": "function"} else: import pdb + pdb.set_trace() return output @@ -306,40 +322,70 @@ class PDBConvertor: if "64" in self._pdb.STREAM_DBI.machine: ptr_size = 8 - output = {"pointer": {"endian": "little", "kind": "int", "signed": False, "size": ptr_size}} + output = { + "pointer": { + "endian": "little", + "kind": "int", + "signed": False, + "size": ptr_size, + } + } for index in self._seen_ctypes: output[self.ctype[index]] = { "endian": "little", "kind": self.ctype_python_types.get(self.ctype[index], "int"), "signed": False if "_U" in index else True, - "size": self.base_type_size[index] + "size": self.base_type_size[index], } return output -if __name__ == '__main__': - parser = argparse.ArgumentParser(description = "Convertor for PDB files to Volatility 3 Intermediate Symbol Format") - parser.add_argument("-o", "--output", metavar = "OUTPUT", help = "Filename for data output", required = True) - file_group = parser.add_argument_group("file", description = "File-based conversion of PDB to ISF") - file_group.add_argument("-f", "--file", metavar = "FILE", help = "PDB file to translate to ISF") - data_group = parser.add_argument_group("data", description = "Convert based on a GUID and filename pattern") - data_group.add_argument("-p", "--pattern", metavar = "PATTERN", help = "Filename pattern to recover PDB file") - data_group.add_argument("-g", - "--guid", - metavar = "GUID", - help = "GUID + Age string for the required PDB file", - default = None) - data_group.add_argument("-k", - "--keep", - action = "store_true", - default = False, - help = "Keep the downloaded PDB file") +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Convertor for PDB files to Volatility 3 Intermediate Symbol Format" + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT", + help="Filename for data output", + required=True, + ) + file_group = parser.add_argument_group( + "file", description="File-based conversion of PDB to ISF" + ) + file_group.add_argument( + "-f", "--file", metavar="FILE", help="PDB file to translate to ISF" + ) + data_group = parser.add_argument_group( + "data", description="Convert based on a GUID and filename pattern" + ) + data_group.add_argument( + "-p", + "--pattern", + metavar="PATTERN", + help="Filename pattern to recover PDB file", + ) + data_group.add_argument( + "-g", + "--guid", + metavar="GUID", + help="GUID + Age string for the required PDB file", + default=None, + ) + data_group.add_argument( + "-k", + "--keep", + action="store_true", + default=False, + help="Keep the downloaded PDB file", + ) args = parser.parse_args() delfile = False filename = None if args.guid is not None and args.pattern is not None: - filename = PDBRetreiver().retreive_pdb(guid = args.guid, file_name = args.pattern) + filename = PDBRetreiver().retreive_pdb(guid=args.guid, file_name=args.pattern) delfile = True elif args.file: filename = args.file @@ -352,7 +398,7 @@ if __name__ == '__main__': convertor = PDBConvertor(filename) with open(args.output, "w") as f: - json.dump(convertor.read_pdb(), f, indent = 2, sort_keys = True) + json.dump(convertor.read_pdb(), f, indent=2, sort_keys=True) if args.keep: print(f"Temporary PDB file: {filename}") diff --git a/development/schema_validate.py b/development/schema_validate.py index 0908e934f..cf9565d68 100644 --- a/development/schema_validate.py +++ b/development/schema_validate.py @@ -1,34 +1,33 @@ import argparse import json +import logging import os import sys # TODO: Rather nasty hack, when volatility's actually installed this would be unnecessary sys.path += ".." -import logging - console = logging.StreamHandler() console.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s') +formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s") console.setFormatter(formatter) logger = logging.getLogger("") logger.addHandler(console) logger.setLevel(logging.DEBUG) -from volatility3 import schemas +from volatility3 import schemas # noqa: E402 -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser("Validates ") - parser.add_argument("-s", "--schema", dest = "schema", default = None) - parser.add_argument("filenames", metavar = "FILE", nargs = '+') + parser.add_argument("-s", "--schema", dest="schema", default=None) + parser.add_argument("filenames", metavar="FILE", nargs="+") args = parser.parse_args() schema = None if args.schema: - with open(os.path.abspath(args.schema), 'r') as s: + with open(os.path.abspath(args.schema)) as s: schema = json.load(s) failures = [] @@ -36,7 +35,7 @@ if __name__ == '__main__': try: if os.path.exists(filename): print(f"[?] Validating file: {filename}") - with open(filename, 'r') as t: + with open(filename) as t: test = json.load(t) if args.schema: diff --git a/development/stock-linux-json.py b/development/stock-linux-json.py index 877f78e1c..713283e66 100644 --- a/development/stock-linux-json.py +++ b/development/stock-linux-json.py @@ -9,15 +9,14 @@ import requests import rpmfile from debian import debfile -DWARF2JSON = './dwarf2json' +DWARF2JSON = "./dwarf2json" class Downloader: - def __init__(self, url_lists: List[List[str]]) -> None: self.url_lists = url_lists - def download_lists(self, keep = False): + def download_lists(self, keep=False): for url_list in self.url_lists: print("Downloading files...") files_for_processing = self.download_list(url_list) @@ -35,43 +34,45 @@ class Downloader: with tempfile.NamedTemporaryFile() as archivedata: archivedata.write(data.content) archivedata.seek(0) - if url.endswith('.rpm'): + if url.endswith(".rpm"): processed_files[url] = self.process_rpm(archivedata) - elif url.endswith('.deb'): + elif url.endswith(".deb"): processed_files[url] = self.process_deb(archivedata) return processed_files def process_rpm(self, archivedata) -> Optional[str]: - rpm = rpmfile.RPMFile(fileobj = archivedata) + rpm = rpmfile.RPMFile(fileobj=archivedata) member = None extracted = None for member in rpm.getmembers(): - if 'vmlinux' in member.name or 'System.map' in member.name: + if "vmlinux" in member.name or "System.map" in member.name: print(f" - Extracting {member.name}") extracted = rpm.extractfile(member) break if not member or not extracted: return None - with tempfile.NamedTemporaryFile(delete = False, - prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output: + with tempfile.NamedTemporaryFile( + delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map" + ) as output: print(f" - Writing to {output.name}") output.write(extracted.read()) return output.name def process_deb(self, archivedata) -> Optional[str]: - deb = debfile.DebFile(fileobj = archivedata) + deb = debfile.DebFile(fileobj=archivedata) member = None extracted = None for member in deb.data.tgz().getmembers(): - if member.name.endswith('vmlinux') or 'System.map' in member.name: + if member.name.endswith("vmlinux") or "System.map" in member.name: print(f" - Extracting {member.name}") extracted = deb.data.get_file(member.name) break if not member or not extracted: return None - with tempfile.NamedTemporaryFile(delete = False, - prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output: + with tempfile.NamedTemporaryFile( + delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map" + ) as output: print(f" - Writing to {output.name}") output.write(extracted.read()) return output.name @@ -83,43 +84,55 @@ class Downloader: if named_files[i] is None: print(f"FAILURE: None encountered for {i}") return - args = [DWARF2JSON, 'linux'] - output_filename = 'unknown-kernel.json' + args = [DWARF2JSON, "linux"] + output_filename = "unknown-kernel.json" for named_file in named_files: - prefix = '--system-map' - if 'System' not in named_files[named_file]: - prefix = '--elf' - output_filename = './' + '-'.join((named_file.split('/')[-1]).split('-')[2:])[:-4] + '.json.xz' + prefix = "--system-map" + if "System" not in named_files[named_file]: + prefix = "--elf" + output_filename = ( + "./" + + "-".join((named_file.split("/")[-1]).split("-")[2:])[:-4] + + ".json.xz" + ) args += [prefix, named_files[named_file]] print(f" - Running {args}") - proc = subprocess.run(args, capture_output = True) + proc = subprocess.run(args, capture_output=True) print(f" - Writing to {output_filename}") - with lzma.open(output_filename, 'w') as f: + with lzma.open(output_filename, "w") as f: f.write(proc.stdout) -if __name__ == '__main__': - parser = argparse.ArgumentParser(description = "Takes a list of URLs for Centos and downloads them") - parser.add_argument("-f", - "--file", - dest = 'filename', - metavar = "FILENAME", - help = "Filename to be read", - required = True) - parser.add_argument("-d", - "--dwarf2json", - dest = 'dwarfpath', - metavar = "PATH", - default = DWARF2JSON, - help = "Path to the dwarf2json binary", - required = True) - parser.add_argument("-k", - "--keep", - dest = 'keep', - action = 'store_true', - help = 'Keep extracted temporary files after completion', - default = False) +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Takes a list of URLs for Centos and downloads them" + ) + parser.add_argument( + "-f", + "--file", + dest="filename", + metavar="FILENAME", + help="Filename to be read", + required=True, + ) + parser.add_argument( + "-d", + "--dwarf2json", + dest="dwarfpath", + metavar="PATH", + default=DWARF2JSON, + help="Path to the dwarf2json binary", + required=True, + ) + parser.add_argument( + "-k", + "--keep", + dest="keep", + action="store_true", + help="Keep extracted temporary files after completion", + default=False, + ) args = parser.parse_args() DWARF2JSON = args.dwarfpath @@ -132,4 +145,4 @@ if __name__ == '__main__': urls += [[lines[2 * i].strip(), lines[(2 * i) + 1].strip()]] d = Downloader(urls) - d.download_lists(keep = args.keep) + d.download_lists(keep=args.keep) diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index d3ba51224..000000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# These packages are required for building the documentation. -sphinx>=4.0.0,<7 -sphinx_autodoc_typehints>=1.4.0 -sphinx-rtd-theme>=0.4.3 - -yara-python -yara-x -pycryptodome -pefile diff --git a/doc/source/basics.rst b/doc/source/basics.rst index 1b8e64780..278ef4d73 100644 --- a/doc/source/basics.rst +++ b/doc/source/basics.rst @@ -14,7 +14,7 @@ Memory layers ------------- A memory layer is a body of data that can be accessed by requesting data at a specific address. At its lowest level -this data is stored on a phyiscal medium (RAM) and very early computers addresses locations in memory directly. However, +this data is stored on a phyiscal medium (RAM) and very early computers addressed locations in memory directly. However, as the size of memory increased and it became more difficult to manage memory most architectures moved to a "paged" model of memory, where the available memory is cut into specific fixed-sized pages. To help further, programs can ask for any address and the processor will look up their (virtual) address in a map, to find out where the (physical) address that it lives at is, @@ -25,8 +25,8 @@ address `9`). The automagic that runs at the start of every volatility session a kernel virtual layer, which allows for kernel addresses to be looked up and the correct data returned. There can, however, be several maps, and in general there is a different map for each process (although a portion of the operating system's memory is usually mapped to the same location across all processes). The maps may take the same address but point to a different part of -physical memory. It also means that two processes could theoretically share memory, but having an virtual address mapped to the -same physical address as another process. See the worked example below for more information. +physical memory. It also means that two processes could theoretically share memory, both having a virtual address mapped to the +same physical address. See the worked example below for more information. To translate an address on a layer, call :py:meth:`layer.mapping(offset, length, ignore_errors) ` and it will return a list of chunks without overlap, in order, for the requested range. If a portion cannot be mapped, an exception will be thrown unless `ignore_errors` is true. Each @@ -61,7 +61,7 @@ mean they each see something different: 4 -> 2 16 - Free In this example, part of the operating system is visible across all processes (although not all processes can write to the memory, there -is a permissions model for intel addressing which is not discussed further here).) +is a permissions model for Intel addressing which is not discussed further here). In Volatility 3 mappings are represented by a directed graph of layers, whose end nodes are :py:class:`DataLayers ` and whose internal nodes are :py:class:`TranslationLayers `. @@ -69,13 +69,13 @@ In this way, a raw memory image in the LiME file format and a page file can be c memory layer. When requesting addresses from the Intel layer, it will use the Intel memory mapping algorithm, along with the address of the directory table base or page table map, to translate that address into a physical address, which will then either be directed towards the swap layer or the LiME layer. Should it -be directed towards the LiME layer, the LiME file format algorithm will be translate the new address to determine where +be directed towards the LiME layer, the LiME file format algorithm will translate the new address to determine where within the file the data is stored. When the :py:meth:`layer.read() ` method is called, the translation is done automatically and the correct data gathered and combined. .. note:: Volatility 2 had a similar concept, called address spaces, but these could only stack linearly one on top of another. -The list of layers supported by volatility can be determined by running the `frameworkinfo` plugin. +The list of layers supported by Volatility can be determined by running the `frameworkinfo` plugin. Templates and Objects --------------------- @@ -167,8 +167,7 @@ There are certain setup tasks that establish the context in a way favorable to a several tasks that are repetitive and also easy to get wrong. These are called :py:class:`Automagic `, since they do things like magically taking a raw memory image and automatically providing the plugin with an appropriate Intel translation layer and an -accurate symbol table without either the plugin or the calling program having to specify all the necessary details. +accurate symbol table without either the plugin or the calling program having to specify all the necessary details. Automagics are a core component which consumers of the library can call or not at their discretion. .. note:: Volatility 2 used to do this as well, but it wasn't a particularly modular mechanism, and was used only for stacking address spaces (rather than identifying profiles), and it couldn't really be disabled/configured easily. - Automagics in Volatility 3 are a core component which consumers of the library can call or not at their discretion. diff --git a/doc/source/complex-plugin.rst b/doc/source/complex-plugin.rst index 8ab8a5186..3db204081 100644 --- a/doc/source/complex-plugin.rst +++ b/doc/source/complex-plugin.rst @@ -13,7 +13,7 @@ There is scope for this, in order to run multiple plugins (see `Writing plugins is to provide a parameterized `classmethod` within the plugin, which will allow the method to yield whatever kind of output it will generate and take whatever parameters it might need. -This is how processes are listed, which is an often used function. The code lives within the +As an example, an often used function is listing processes. The code lives within the :py:class:`~volatility3.plugins.windows.pslist.PsList` plugin but can be used by other plugins by providing the appropriate parameters (see :py:meth:`~volatility3.plugins.windows.pslist.PsList.list_processes`). @@ -36,8 +36,8 @@ each plugin in order to populate the context's configuration correctly based on between plugins). Once the automagics have been constructed, the plugin can be instantiated using the helper function :py:func:`~volatility3.framework.plugins.construct_plugin` providing: - * the base context (containing the configuration and any already loaded layers or symbol tables), - * the plugin class to run, + * the base context (containing the configuration and any already loaded layers or symbol tables) + * the plugin class to run * the configuration path within the context for the plugin * any callback to determine progress in lengthy operations * an open method for the plugin to create files during the run @@ -58,7 +58,7 @@ ContextManager, so it can be used by the python `with` keyword). This is set on that can be set on the filename, and a :py:class:`~volatility3.framework.interfaces.plugins.FileHandlerInterface` is the result. This mimics an `IO[bytes]` object, which closely mimics a standard python file-like object. -As such code for outputting to a file would be expected to look something like: +As such, code for outputting to a file would be expected to look something like: .. code-block:: python @@ -77,7 +77,7 @@ Scanners are objects that adhere to the :py:class:`~volatility3.framework.interf passed to the :py:meth:`~volatility3.framework.interfaces.layers.TranslationLayerInterface.scan` method on layers which will divide the provided range of sections (or the entire layer if none are provided) and call the :py:meth:`~volatility3.framework.interfaces.layers.ScannerInterface`'s call method -method with each chunk as a parameter, ensuring a suitable amount of overlap (as defined by the scanner). +with each chunk as a parameter, ensuring a suitable amount of overlap (as defined by the scanner). The offset of the chunk, within the layer, is also provided as a parameter. Scanners can technically maintain state, but it is not recommended since the ordering that the chunks are scanned is @@ -92,18 +92,18 @@ Empirically it was found that scanners are typically not the most time intensive extensive scanning) and so parallelism does not offer significant gains. As such, parallelism is not enabled by default but interfaces can easily enable parallelism when desired. -Writing/Using Intermediate Symbol Format Files ----------------------------------------------- +Writing / Using Intermediate Symbol Format Files +------------------------------------------------ It can occasionally be useful to create a data file containing the static structures that can create a :py:class:`~volatility3.framework.interfaces.objects.Template` to be instantiated on a layer. Volatility has all the machinery necessary to construct these for you from properly formatted JSON data. -The JSON format is documented by the JSON schema files located in schemas. These are versioned using standard .so +The JSON format is documented by the JSON schema files located in the schemas directory. These are versioned using standard .so library versioning, so they may not increment as expected. Each schema lists an available version that can be used, which specifies five different sections: -* Base_types - These are the basic type names that will make up the native/primitive types +* Base_types - These are the basic type names that will make up the native / primitive types * User_types - These are the standard definitions of type structures, most will go here * Symbols - These list offsets that are associated with specific names (and can be associated with specific type names) * Enums - Enumerations that offer a number of choices @@ -180,7 +180,7 @@ of data. Each chunk contains the following information, in order: **layer_name** the layer that this data comes from -An example (and the most common layer encountered in memory forensics) would be an Intel layer, which models the intel +An example (and the most common layer encountered in memory forensics) would be an Intel layer, which models the Intel page mapping system. Based on a series of tables stored within the layer itself, an intel layer can convert a virtual address to a physical address. It should be noted that intel layers allow multiple virtual addresses to map to the same physical address (but a single virtual address cannot ever map to more than one physical address). @@ -195,7 +195,7 @@ like `abcdr`, requesting `mapping(5, 4)` would return: (7,2,0,2, 'physical_layer') ] -This mapping mechanism allows for great flexibility in that chunks making up a virtual layer can come from multiple +This mapping mechanism allows for great flexibility because chunks making up a virtual layer can come from multiple different range layers, allowing for swap space to be used to construct the virtual layer, for example. Also, by defining the mapping method, the read and write methods (which read and write into the domain layer) are defined for you to write to the lower layers (which in turn can write to layers even lower than that) until eventually they arrive at a @@ -264,7 +264,7 @@ so it therefore populates the `metadata` property. This is defined as a read-on includes data from every underlying layer. As such, CrashDumpLayer would actually specify this value by setting it in the protected dictionary by `self._direct_metadata['page_map_offset']`. -There is, unfortunately, no easy way to form consensus between a particular layer may want and what a particular layer +There is, unfortunately, no easy way to form consensus between what a particular layer may want and what a particular layer may be able to provide. At the moment, the main information that layers may populate are: * `os` with values of `Windows`, `Linux`, `Mac` or `unknown` diff --git a/doc/source/conf.py b/doc/source/conf.py index cabfdc327..ca868aff1 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,6 +19,8 @@ import sys import sphinx.ext.apidoc +from importlib.util import find_spec + def setup(app): volatility_directory = os.path.abspath( @@ -124,7 +126,7 @@ def setup(app): # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("../..")) -from volatility3.framework import constants +from volatility3.framework import constants # noqa: E402 # -- General configuration ------------------------------------------------ @@ -147,13 +149,9 @@ extensions = [ autosectionlabel_prefix_document = True -try: - import sphinx_autodoc_typehints - +if find_spec("sphinx_autodoc_typehints") is not None: extensions.append("sphinx_autodoc_typehints") -except ImportError: - # If the autodoc typehints extension isn't available, carry on regardless - pass +# If the autodoc typehints extension isn't available, carry on regardless # Add any paths that contain templates here, relative to this directory. # templates_path = ['tools/templates'] @@ -169,7 +167,7 @@ master_doc = "index" # General information about the project. project = "Volatility 3" -copyright = "2012-2024, Volatility Foundation" +copyright = "2012-2025, 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/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index d4b40d053..d0b097e0e 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -6,41 +6,47 @@ This guide will give you a brief overview of how volatility3 works as well as a Acquiring memory ---------------- -Volatility3 does not provide the ability to acquire memory. Below are some examples of tools that can be used to acquire memory, but more are available: +Volatility3 does not provide the ability to acquire memory. Below is an example of a tool that can be used to acquire memory on Linux systems: * `AVML - Acquire Volatile Memory for Linux `_ -* `LiME - Linux Memory Extract `_ -Be aware that LiME raw format is not supported by volatility3, the padded or lime option should be used instead. `This issue contains further information `_. +Other tools may exist, but please verify their maintenance status and compatibility with volatility3 before use. -Procedure to create symbol tables for linux --------------------------------------------- +Procedure to create symbol tables for Linux +------------------------------------------- -To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`. +It is recommended to first check the repository `volatility3-symbols `_ for pre-generated JSON.xz symbol table files. +This repository provides files organized by kernel version for popular Linux distributions such as Debian, Ubuntu, and AlmaLinux. + +If you cannot find a suitable symbol table for your kernel version there, please refer to :ref:`symbol-tables:Mac or Linux symbol tables` to create one manually. + +After creating the file, place it under the directory ``volatility3/symbols``. +Volatility3 will automatically detect and use symbol tables from this location. -.. tip:: It may be possible to locate pre-made ISF files from the `Linux ISF Server `_ , - which is built and maintained by `kevthehermit `_. - After creating the file or downloading it from the ISF server, place the file under the directory ``volatility3/symbols/linux``. - If necessary create a linux directory under the symbols directory (this will become unnecessary in future versions). Listing plugins --------------- -The following is a sample of the linux plugins available for volatility3, it is not complete and more more plugins may -be added. For a complete reference, please see the volatility 3 :doc:`list of plugins `. -For plugin requests, please create an issue with a description of the requested plugin. +Volatility3 currently supports over 40 Linux-specific plugins covering a wide range of forensic analysis needs, such as process enumeration, memory-mapped file inspection, loaded modules, and kernel tracing features. + +Some representative plugins include: + +- ``linux.pslist``: Lists running processes with their PIDs and PPIDs. +- ``linux.bash``: Recovers bash command history from memory. +- ``linux.lsmod``: Displays loaded kernel modules. +- ``linux.kmsg``: Reads messages from the kernel log buffer. +- ``linux.elfs``: Lists all memory-mapped ELF files. +- ``linux.check_creds``: Checks for suspicious credential structures. +- ``linux.vmayarascan``: Scans process memory using YARA signatures. + +For a full list of supported plugins, run the following command: .. code-block:: shell-session - $ python3 vol.py --help | grep -i linux. | head -n 5 - banners.Banners Attempts to identify potential linux banners in an - linux.bash.Bash Recovers bash command history from memory. - linux.check_afinfo.Check_afinfo - linux.check_creds.Check_creds - linux.check_idt.Check_idt + $ python3 vol.py --help | grep -i linux. -.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of linux plugins. +.. note:: You can also filter and inspect available plugins using more sophisticated patterns or tools like ``grep``, ``awk``, or simply explore the source under ``volatility3/framework/plugins/linux``. Using plugins @@ -60,14 +66,14 @@ banners ~~~~~~~ In this example we will be using a memory dump from the Insomni'hack teaser 2020 CTF Challenge called Getdents. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge. -Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. +Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. .. code-block:: shell-session $ python3 vol.py -f memory.vmem banners - Volatility 3 Framework 2.0.1 + Volatility 3 Framework 2.26.0 Progress: 100.00 PDB scanning finished Offset Banner @@ -79,85 +85,79 @@ Thanks go to `stuxnet `_ for providing this memo 0x7fde0010 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18) -The above command helps us to find the memory dump's kernel version and the distribution version. Now using the above banner we can search for the needed ISF file from the ISF server. -If ISF file cannot be found then, follow the instructions on :ref:`getting-started-linux-tutorial:Procedure to create symbol tables for linux`. After that, place the ISF file under the ``volatility3/symbols/linux`` directory. +The above command helps us identify the kernel version and distribution from the memory dump. +Using this information, follow the instructions in :ref:`getting-started-linux-tutorial:Procedure to create symbol tables for linux` to generate the required ISF file. +Once created, place the file under the ``volatility3/symbols`` directory so that Volatility3 can recognize it automatically. + +linux.boottime +~~~~~~~~~~~~~~ + +This plugin provides the system boot time extracted from memory. +It is useful for establishing a timeline, particularly when analyzing incident response scenarios or determining system uptime. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.boottime + + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + + TIME NS Boot Time + + - 2022-02-10 06:50:16.450008 UTC + +This timestamp can serve as a reference point for correlating system events, such as process start times, logs, or malicious activity. -.. tip:: Use the banner text which is most repeated to search from ISF Server. linux.pslist ~~~~~~~~~~~~ +This plugin lists active processes by walking the task list from memory. +It provides detailed metadata for each process, including identifiers and user/group information. + .. code-block:: shell-session $ python3 vol.py -f memory.vmem linux.pslist - Volatility 3 Framework 2.0.1 Stacking attempts finished + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + OFFSET (V) PID TID PPID COMM UID GID EUID EGID CREATION TIME File output - PID PPID COMM + 0x8ca6db1aac80 1 1 0 systemd 0 0 0 0 2022-02-10 06:50:16.364213 UTC Disabled + 0x8ca6db1a9640 2 2 0 kthreadd 0 0 0 0 2022-02-10 06:50:16.364213 UTC Disabled + 0x8ca6db1ac2c0 3 3 2 rcu_gp 0 0 0 0 2022-02-10 06:50:16.372213 UTC Disabled + ... - 1 0 systemd - 2 0 kthreadd - 3 2 kworker/0:0 - 4 2 kworker/0:0H - 5 2 kworker/u256:0 - 6 2 mm_percpu_wq - 7 2 ksoftirqd/0 - 8 2 rcu_sched - 9 2 rcu_bh - 10 2 migration/0 - 11 2 watchdog/0 - 12 2 cpuhp/0 - 13 2 kdevtmpfs - 14 2 netns - 15 2 rcu_tasks_kthre - 16 2 kauditd - ..... +This detailed view allows investigators to correlate user privileges, startup times, and relationships between processes more precisely than before. -``linux.pslist`` helps us to list the processes which are running, their PIDs and PPIDs. linux.pstree ~~~~~~~~~~~~ +This plugin presents the process hierarchy as a tree, clearly showing parent-child relationships between processes. .. code-block:: shell-session $ python3 vol.py -f memory.vmem linux.pstree - Volatility 3 Framework 2.0.1 + + Volatility 3 Framework 2.26.0 Progress: 100.00 Stacking attempts finished - PID PPID COMM + OFFSET (V) PID TID PPID COMM + + 0x8ca6db1aac80 1 1 0 systemd + * 0x8ca6db3342c0 278 278 1 systemd-journal + * 0x8ca6d005ac80 315 315 1 systemd-udevd + * 0x8ca6d0eac2c0 478 478 1 systemd-resolve + * ... + *** 0x8ca67108c2c0 1507 1507 1438 gdm-x-session + **** 0x8ca671215900 1527 1527 1507 Xorg + **** 0x8ca671210000 1608 1608 1507 gnome-session-b + ***** 0x8ca66fba42c0 1765 1765 1608 ssh-agent + + +It helps identify unusual or suspicious process structures such as orphaned child processes, injected children under legitimate parents, or long chains of shell execution. +The tree view is particularly useful for spotting anomalies in process launch sequences or privilege escalations by inspecting unexpected parent-child relationships. - 1 0 systemd - * 636 1 polkitd - * 514 1 acpid - * 1411 1 pulseaudio - * 517 1 rsyslogd - * 637 1 cups-browsed - * 903 1 whoopsie - * 522 1 ModemManager - * 525 1 cron - * 526 1 avahi-daemon - ** 542 526 avahi-daemon - * 657 1 unattended-upgr - * 914 1 kerneloops - * 532 1 dbus-daemon - * 1429 1 ibus-x11 - * 929 1 kerneloops - * 1572 1 gsd-printer - * 933 1 upowerd - * 1071 1 rtkit-daemon - * 692 1 gdm3 - ** 1234 692 gdm-session-wor - *** 1255 1234 gdm-x-session - **** 1257 1255 Xorg - **** 1266 1255 gnome-session-b - ***** 1537 1266 gsd-clipboard - ***** 1539 1266 gsd-color - ***** 1542 1266 gsd-datetime - ***** 2950 1266 deja-dup-monito - ***** 1546 1266 gsd-housekeepin - ***** 1548 1266 gsd-keyboard - ***** 1550 1266 gsd-media-keys -``linux.pstree`` helps us to display the parent child relationships between processes. linux.bash ~~~~~~~~~~ @@ -168,7 +168,7 @@ Now to find the commands that were run in the bash shell by using ``linux.bash`` $ python3 vol.py -f memory.vmem linux.bash - Volatility 3 Framework 2.0.1 + Volatility 3 Framework 2.26.0 Progress: 100.00 Stacking attempts finished PID Process CommandTime Command @@ -177,17 +177,85 @@ Now to find the commands that were run in the bash shell by using ``linux.bash`` 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 rub - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 uname -a - 1733 bash 2020-01-16 14:00:36.000000 uname -a - 1733 bash 2020-01-16 14:00:36.000000 sudo apt autoclean - 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:41.000000 chmod +x meterpreter 1733 bash 2020-01-16 14:00:42.000000 sudo ./meterpreter + + +linux.ip.Addr and linux.ip.Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Network configuration is an essential aspect of memory forensics. +Analyzing the network interfaces and their IP assignments can reveal active connections, misconfigured settings, or even artifacts of malicious activity. + +Volatility3 provides the following two plugins to examine this information: + +**linux.ip.Addr** displays IP-related metadata for each interface, including IPv4/IPv6 addresses, MAC, scope, and interface status. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.ip.Addr + + NetNS Index Interface MAC Promiscuous IP Prefix Scope Type State + 4026531992 2 enp0s3 08:00:27:8a:4d:eb False 10.0.2.15 24 global UP + ... + +**linux.ip.Link** shows lower-level link information such as MTU, Qdisc, and interface flags. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.ip.Link + + NS Interface MAC State MTU Qdisc Qlen Flags + 4026531992 enp0s3 08:00:27:8a:4d:eb UP 1500 fq_codel 1000 BROADCAST,LOWER_UP,MULTICAST,UP + +Together, these plugins help investigators assess the system’s network exposure and identify anomalies such as multiple network namespaces, unexpected IP addresses, or active interfaces in promiscuous mode. + +linux.malfind +~~~~~~~~~~~~~ + +This plugin scans process memory for suspicious executable regions that may indicate code injection or malicious payloads. +It is particularly useful for detecting fileless malware, injected shellcode, or unpacked runtime payloads that do not correspond to legitimate binary files on disk. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.malfind + + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + PID Process Start End Path Protection Hexdump Disasm + + 540 networkd-dispat 0x7f1506482000 0x7f1506483000 Anonymous Mapping rwx + 00 00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 ........C....... + 4c 8d 15 f9 ff ff ff ff 25 03 00 00 00 0f 1f 00 L.......%....... + ... + 0x7f1506482000: add byte ptr [rax], al + 0x7f1506482002: add byte ptr [rax], al + ... + 0x7f1506482013: stc + +In this output: + +- **PID / Process**: Identifies the target process (in this case, `networkd-dispat`, PID 540) +- **Start / End**: The memory address range of the suspicious region +- **Path**: Indicates that the region is an anonymous memory mapping (i.e., not backed by a file) +- **Protection**: The region is marked `rwx` (read-write-execute), which is uncommon for legitimate memory regions +- **Disasm**: Shows the disassembled machine code found in that memory region + +**Key indicators to focus on:** + +- **Anonymous Mapping + rwx**: Memory that is not backed by a file and has execute permissions is often used for injected code +- **Disassembly patterns**: Repetitive `add` instructions, `nop`, or unusual instruction sequences can be artifacts of shellcode, packer stubs, or JIT-compiled code +- **Process context**: The suspicious memory is found in `networkd-dispat`, a system service — if this service is not expected to have dynamic executable memory regions, it may be compromised + +Use this plugin early in an investigation to flag processes for deeper inspection. + +Further Exploration and Contribution +------------------------------------ + +This guide has introduced several key Linux plugins available in Volatility 3 for memory forensics. +However, many more plugins are available, covering topics such as kernel modules, page cache analysis, tracing frameworks, and malware detection. + +If you identify gaps in plugin functionality or wish to extend support for a specific analysis use case, you are encouraged to contribute new plugins or enhancements. +Your insights can help shape the future of Linux memory forensics. + diff --git a/doc/source/getting-started-mac-tutorial.rst b/doc/source/getting-started-mac-tutorial.rst index 42e58c0d5..f4889d689 100644 --- a/doc/source/getting-started-mac-tutorial.rst +++ b/doc/source/getting-started-mac-tutorial.rst @@ -37,7 +37,7 @@ For plugin requests, please create an issue with a description of the requested mac.check_sysctl.Check_sysctl mac.check_trap_table.Check_trap_table -.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of macOS plugins. +.. note:: Here the command is piped to grep and head to provide the start of the list of macOS plugins. Using plugins @@ -78,7 +78,7 @@ Thanks go to `stuxnet `_ for providing this memo The above command helps us to find the memory dump's Darwin kernel version. Now using the above banner we can search for the needed ISF file. -If ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory. +If an ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory. mac.pslist ~~~~~~~~~~ @@ -125,7 +125,7 @@ mac.pstree 337 1 system_installd * 455 337 update_dyld_shar -``mac.pstree`` helps us to display the parent child relationships between processes. +``mac.pstree`` helps us to display the parent-child relationships between processes. mac.ifconfig ~~~~~~~~~~~~ @@ -150,4 +150,4 @@ mac.ifconfig utun0 False utun0 fe80:5::2a95:bb15:87e3:977c False -we can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation. +We can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation. diff --git a/doc/source/getting-started-windows-tutorial.rst b/doc/source/getting-started-windows-tutorial.rst index c89b065f5..3896000a2 100644 --- a/doc/source/getting-started-windows-tutorial.rst +++ b/doc/source/getting-started-windows-tutorial.rst @@ -15,19 +15,19 @@ Memory can be acquired using a number of tools, below are some examples but othe Listing Plugins --------------- -The following is a sample of the windows plugins available for volatility3, it is not complete and more more plugins may +The following is a sample of the windows plugins available for volatility3, it is not complete and more plugins may be added. For a complete reference, please see the volatility 3 :doc:`list of plugins `. For plugin requests, please create an issue with a description of the requested plugin. .. code-block:: shell-session - $ python3 vol.py --help | grep windows | head -n 5 + $ python3 vol.py --help | grep windows | head -n 4 windows.bigpools.BigPools windows.cmdline.CmdLine windows.crashinfo.Crashinfo windows.dlllist.DllList -.. note:: Here the the command is piped to grep and head in-order to provide the start of a list of the available windows plugins. +.. note:: Here the command is piped to grep and head to provide the start of a list of the available windows plugins. Using plugins ------------- @@ -95,9 +95,9 @@ windows.pstree ** 616 504 svchost.exe 0xfa8002b86ab0 13 314 0 False 2022-02-07 16:32:16.000000 N/A ** 624 504 svchost.exe 0xfa8002410630 10 350 0 False 2022-02-07 16:30:14.000000 N/A -``windows.pstree`` helps to display the parent child relationships between processes. +``windows.pstree`` helps to display the parent-child relationships between processes. -.. note:: Here the the command is piped to head in-order to provide smaller output, here listing only the first 20. +.. note:: Here the command is piped to head to provide smaller output, here listing only the first 20. windows.hashdump ~~~~~~~~~~~~~~~~ @@ -116,9 +116,3 @@ windows.hashdump Dennis 1003 aad3b435b51404eeaad3b435b51404ee cf96684bbc7877920adaa9663698bf54 ``windows.hashdump`` helps to list the hashes of the users in the system. - - - - - - diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst index 66dabfafe..d3bc9613a 100644 --- a/doc/source/glossary.rst +++ b/doc/source/glossary.rst @@ -23,7 +23,7 @@ Alignment .. _Array: Array - This represents a list of items, which can be access by an index, which is zero-based (meaning the first + This represents a list of items, which can be accessed by an index, which is zero-based (meaning the first element has index 0). Items in arrays are almost always the same size (it is not a generic list, as in python) even if they are :ref:`pointers` to different sized objects. @@ -43,7 +43,14 @@ Dereference .. _Domain: Domain - This the grouping for input values for a mapping or mathematical function. + The set of input values for a mapping or mathematical function. + +I +- +.. _Intermediate Symbol File (ISF): + +Intermediate Symbol File (ISF) + They contain kernel structures and specific offsets formatted as JSON. For macOS and Linux analysis, the kernel needs to be added as an ISF file to the volatility 3 symbols directory. For Windows, the required ISF file can often be generated from PDB files automatically downloaded from Microsoft servers, and therefore does not require manual intervention. M - @@ -54,9 +61,7 @@ Map, mapping of the :ref:`Range`). Mappings can be seen as a mathematical function, and therefore volatility 3 attempts to use mathematical functional notation where possible. Within volatility a mapping is most often used to refer to the function for translating addresses from a higher layer (domain) to a lower layer (range). - For further information, please see - `Function (mathematics) in wikipedia https://en.wikipedia.org/wiki/Function_(mathematics)` - + For further information, please see `Function (mathematics) in Wikipedia_`. .. _Member: @@ -69,7 +74,7 @@ O .. _Object: Object - This has a specific meaning within computer programming (as in Object Oriented Programming), but within the world + This has a specific meaning within computer programming (as in object-oriented programming), but within the world of Volatility it is used to refer to a type that has been associated with a chunk of data, or a specific instance of a type. See also :ref:`Type`. @@ -116,6 +121,11 @@ Page Table possible to use them as a way to map a particular address within a (potentially larger, but sparsely populated) virtual space to a concrete (and usually contiguous) physical space, through the process of :ref:`mapping`. +.. _Plugin: + +Plugin + Plugins are the "functions" of the volatility framework. They carry out algorithms on data stored in layers using objects constructed from symbols. Broadly, plugins take in a number of TranslationLayers (the data, which is a representation of part of an image, in a specified type described by templates) and outputs a TreeGrid. + .. _Pointer: Pointer @@ -145,9 +155,9 @@ Struct, Structure Symbol This is used in many different contexts, as a short term for many things. Within Volatility, a symbol is a - construct that usually encompasses a specific type :ref:`type` at a specific :ref:`offset`, + construct that usually encompasses a specific :ref:`type` at a specific :ref:`offset`, representing a particular instance of that type within the memory of a compiled and running program. An example - would be the location in memory of a list of active tcp endpoints maintained by the networking stack + would be the location in memory of a list of active TCP endpoints maintained by the networking stack within an operating system. T diff --git a/doc/source/simple-plugin.rst b/doc/source/simple-plugin.rst index 39670a62d..d855e319d 100644 --- a/doc/source/simple-plugin.rst +++ b/doc/source/simple-plugin.rst @@ -41,24 +41,36 @@ to be able to run properly. Any that are defined as optional need not necessari @classmethod def get_requirements(cls): - return [requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process IDs to include (all other processes are excluded)", - optional = True), - requirements.PluginRequirement(name = 'pslist', - plugin = pslist.PsList, - version = (2, 0, 0))] + return [ + requirements.ModuleRequirement( + name = 'kernel', + description = 'Windows kernel', + architectures = ["Intel32", "Intel64"] + ), + requirements.ListRequirement( + name = 'pid', + element_type = int, + description = "Process IDs to include (all other processes are excluded)", + optional = True + ), + requirements.VersionRequirement( + name = 'pslist', + component = pslist.PsList, + version = (2, 0, 0) + ), + ] -This is a classmethod, because it is called before the specific plugin object has been instantiated (in order to know how +This is a classmethod, so it can be called before the specific plugin object has been instantiated (in order to know how to instantiate the plugin). At the moment these requirements are fairly straightforward: :: - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name = 'kernel', + description = 'Windows kernel', + architectures = ["Intel32", "Intel64"] + ), This requirement specifies the need for a particular submodule. Each module requires a :py:class:`TranslationLayer ` and a @@ -85,9 +97,11 @@ not be requested directly from the user. :: - requirements.TranslationLayerRequirement(name = 'primary', - description = 'Memory layer for the kernel', - architectures = ["Intel32", "Intel64"]), + requirements.TranslationLayerRequirement( + name = 'primary', + description = 'Memory layer for the kernel', + architectures = ["Intel32", "Intel64"] + ), This requirement indicates that the plugin will operate on a single :py:class:`TranslationLayer `. The name of the @@ -110,8 +124,10 @@ not be requested directly from the user. :: - requirements.SymbolTableRequirement(name = "nt_symbols", - description = "Windows kernel symbols"), + requirements.SymbolTableRequirement( + name = "nt_symbols", + description = "Windows kernel symbols" + ), This requirement specifies the need for a particular :py:class:`SymbolTable ` @@ -127,10 +143,12 @@ not be requested directly from the user. :: - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), + requirements.ListRequirement( + name = 'pid', + description = 'Filter on specific process IDs', + element_type = int, + optional = True + ), The next requirement is a List Requirement, populated by integers. The description will be presented to the user to describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value @@ -138,9 +156,11 @@ being defined within the configuration tree at all. :: - requirements.PluginRequirement(name = 'pslist', - plugin = pslist.PsList, - version = (2, 0, 0))] + requirements.PluginRequirement( + name = 'pslist', + plugin = pslist.PsList, + version = (2, 0, 0) + ) This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major @@ -178,18 +198,24 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces. def run(self): filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - kernel = self.context.modules[self.config['kernel']] - return renderers.TreeGrid([("PID", int), - ("Process", str), - ("Base", format_hints.Hex), - ("Size", format_hints.Hex), - ("Name", str), - ("Path", str)], - self._generator(pslist.PsList.list_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Base", format_hints.Hex), + ("Size", format_hints.Hex), + ("Name", str), + ("Path", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config['kernel'], + filter_func = filter_func + ) + ) + ) In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters). It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if @@ -207,7 +233,7 @@ the :py:class:`~volatility3.plugins.windows.pslist.PsList` plugin. That plugin so that other plugins can call it. As such, it takes all the necessary parameters rather than accessing them from a configuration. Since it must be portable code, it takes a context, as well as the layer name, symbol table and optionally a filter. In this instance we unconditionally -pass it the values from the configuration for the layer and symbol table from the kernel module object, constructed from +pass it the value from the configuration for the kernel module name, constructed from the ``kernel`` configuration requirement. This will generate a list of :py:class:`~volatility3.framework.symbols.windows.extensions.EPROCESS` objects, as provided by the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin, and is not covered here but is used as an example for how to share code across plugins @@ -281,5 +307,3 @@ such as ``!_UNICODE``) and the parameters to that type. Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native type string) and could not have been a special Structure such as ``_UNICODE``. For the format hint columns, the format hint type must be used to ensure the error checking does not fail. - - diff --git a/doc/source/symbol-tables.rst b/doc/source/symbol-tables.rst index b7c26e046..59c1febcc 100644 --- a/doc/source/symbol-tables.rst +++ b/doc/source/symbol-tables.rst @@ -9,7 +9,7 @@ How Volatility finds symbol tables All files are stored as JSON data, they can be in pure JSON files as ``.json``, or compressed as ``.json.gz`` or ``.json.xz``. Volatility will automatically decompress them on use. It will also cache their contents (compressed) when used, located -under the user's home directory, in :file:`.cache/volatility3`, along with other useful data. The cache directory currently +under the user's home directory, in :file:`.cache/volatility3` or when `XDG_CACHE_HOME` is set in :file:`${XDG_CACHE_HOME}/volatility3`, along with other useful data. The cache directory currently cannot be altered. Symbol table JSON files live, by default, under the :file:`volatility3/symbols` directory. The symbols directory is @@ -25,9 +25,9 @@ as long as the symbol files stay in the same location. Windows symbol tables --------------------- -For Windows systems, Volatility accepts a string made up of the GUID and Age of the required PDB file. It then +For Windows systems, Volatility accepts a string made up of the GUID and age of the required PDB file. It then searches all files under the configured symbol directories under the windows subdirectory. Any that contain metadata -which matches the pdb name and GUID/age (or any compressed variant) will be used. If such a symbol table cannot be found, then +which matches the PDB name and GUID/age (or any compressed variant) will be used. If such a symbol table cannot be found, then the associated PDB file will be downloaded from Microsoft's Symbol Server and converted into the appropriate JSON format, and will be saved in the correct location. @@ -54,8 +54,8 @@ most Volatility plugins. Note that in most linux distributions, the standard ke and the kernel with debugging information is stored in a package that must be acquired separately. A generic table isn't guaranteed to produce accurate results, and would reduce the number of structures -that all plugins could rely on. As such, and because linux kernels with different configurations can produce different structures, -volatility 3 requires that the banners in the JSON file match the banners found in the image *exactly*, not just the version +that all plugins could rely on. As such, and because Linux kernels with different configurations can produce different structures, +Volatility 3 requires that the banners in the JSON file match the banners found in the image *exactly*, not just the version number. This can include elements such as the compilation time and even the version of gcc used for the compilation. The exact match is required to ensure that the results volatility returns are accurate, therefore there is no simple means provided to get the wrong JSON ISF file to easily match. @@ -63,8 +63,8 @@ provided to get the wrong JSON ISF file to easily match. To determine the string for a particular memory image, use the `banners` plugin. Once the specific banner is known, try to locate that exact kernel debugging package for the operating system. Unfortunately each distribution provides its debugging packages under different package names and there are so many that the distribution may not keep all old -versions of the debugging symbols, and therefore **it may not be possible to find the right symbols to analyze a linux -memory image with volatility**. With Macs there are far fewer kernels and only one distribution, making it easier to +versions of the debugging symbols, and therefore **it may not be possible to find the right symbols to analyze a Linux +memory image with Volatility**. With Macs there are far fewer kernels and only one distribution, making it easier to ensure that the right symbols can be found. Once a kernel with debugging symbols/appropriate DWARF file has been located, `dwarf2json `_ will convert it into an @@ -75,7 +75,7 @@ symbol offsets within the DWARF data, which dwarf2json can extract into the JSON The banners available for volatility to use can be found using the `isfinfo` plugin, but this will potentially take a long time to run depending on the number of JSON files available. This will list all the JSON (ISF) files that -volatility3 is aware of, and for linux/mac systems what banner string they search for. For volatility to use the JSON +Volatility 3 is aware of, and for linux/mac systems what banner string they search for. For volatility to use the JSON file, the banners must match exactly (down to the compilation date). .. note:: diff --git a/doc/source/using-as-a-library.rst b/doc/source/using-as-a-library.rst index 4acf35f98..144cae644 100644 --- a/doc/source/using-as-a-library.rst +++ b/doc/source/using-as-a-library.rst @@ -3,7 +3,7 @@ Using Volatility 3 as a Library This portion of the documentation discusses how to access the Volatility 3 framework from an external application. -The general process of using volatility as a library is to as follows: +The general process of using volatility as a library is as follows: 1. :ref:`create_context` 2. (Optional) :ref:`available_plugins` @@ -21,7 +21,7 @@ Creating a context First we make sure the volatility framework works the way we expect it (and is the version we expect). The versioning used is semantic versioning, meaning any version with the same major number and a higher or equal minor number will satisfy the requirement. An example is below since the CLI doesn't need any of the features -from versions 1.1 or 1.2: +from version 1.1 or later: :: @@ -86,7 +86,7 @@ List requirements are a list of simple types (integers, booleans, floats and str options, multiple requirements needs all their subrequirements fulfilled and the other types require the names of valid translation layers or symbol tables within the context, respectively. Luckily, each of these requirements can tell you whether they've been fulfilled or not later in the process. For now, they can be used to ask the user to -fill in any parameters they made need to. Some requirements are optional, others are not. +fill in any parameters they may need to. Some requirements are optional, others are not. The plugin is essentially a multiple requirement. It should also be noted that automagic classes can have requirements (as can translation layers). @@ -100,7 +100,7 @@ Once you know what requirements the plugin will need, you can populate them with The configuration is essentially a hierarchical tree of values, much like the windows registry. Each plugin is instantiated at a particular branch within the hierarchy and will look for its configuration options under that hierarchy (if it holds any configurable items, it will likely instantiate those at a point -underneaths its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed. +underneath its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed. For this example, we'll assume plugins' base_config_path is set as `plugins`, and that automagics are configured under the `automagic` tree. We'll see later how to ensure this matches up with the plugins and automagic when they're @@ -139,7 +139,7 @@ A suitable list of automagics for a particular plugin (based on operating system This will take the plugin module, extract the operating system (first level of the hierarchy) and then return just the automagics which apply to the operating system. Each automagic can exclude itself from being used for specific -operating systems, so that an automagic designed for linux is not used for windows or mac plugins. +operating systems, such that an automagic designed for linux is not used for windows or mac plugins. These automagics can then be run by providing the list, the context, the plugin to be run, the hierarchy name that the plugin will be constructed on ('plugins' by default) and a progress_callback. This is a callable which takes @@ -157,8 +157,8 @@ Any exceptions that occur during the execution of the automagic will be returned Run the plugin -------------- -Firstly, we should check whether the plugin will be able to run (ie, whether the configuration options it needs -have been successfully set). We do this as follow (where plugin_config_path is the base_config_path (which defaults +Firstly, we should check whether the plugin will be able to run (i.e., whether the configuration options it needs +have been successfully set). We do this as follows, where plugin_config_path is the base_config_path (which defaults to `plugins` and then the name of the class itself): :: @@ -166,7 +166,7 @@ to `plugins` and then the name of the class itself): unsatisfied = plugin.unsatisfied(context, plugin_config_path) If unsatisfied is an empty list, then the plugin has been given everything it requires. If not, it will be a -Dictionary of the hierarchy paths and their associated requirements that weren't satisfied. +dict of the hierarchy paths and their associated requirements that weren't satisfied. The plugin can then be instantiated with the context (containing the plugin's configuration) and the path that the plugin can find its configuration at. This configuration path only needs to be a unique value to identify where the diff --git a/doc/source/vol-cli.rst b/doc/source/vol-cli.rst index 7b91e815d..43ca33f04 100644 --- a/doc/source/vol-cli.rst +++ b/doc/source/vol-cli.rst @@ -58,7 +58,7 @@ Options EXTEND. Extensions must be of the form **configuration.item.name=value** -p PLUGIN_DIRS, --plugin-dirs PLUGIN_DIRS - Specified a semi-colon separated list of paths that contain directories + Specified as a semi-colon separated list of paths that contain directories where plugins may be found. These paths are searched before the default paths when loading python files for plugins. This can therefore be used to override built-in plugins. NOTE: All python code within this directory @@ -67,12 +67,12 @@ Options -s SYMBOL_DIRS, --symbol-dirs SYMBOL_DIRS SYMBOL_DIRS is a semi-colon separated list of paths that contain symbol files or symbol zip packs. Symbols must be within a particular directory - structure if they depending on the operating system of the symbols, + structure if they depend on the operating system of the symbols, whilst symbol packs must be in the root of the directory and named after - the after the operating system to which they apply. + the operating system to which they apply. -v, --verbose - A flag which can be used multiple times, each time increasing the level of + A flag which can be used multiple times (up to six, -vvvvvv), each time increasing the level of detail in the logs produced. -l LOG, --log LOG @@ -87,7 +87,7 @@ Options -q, --quiet When present, this flag mutes the progress feedback for operations. This can be beneficial when piping the output directly to a file or another - tool. This also removes the + tool. -r RENDERER, --renderer RENDERER Specifies the output format in which to display results. The default is @@ -120,9 +120,7 @@ Options Change the default path used to store the cache. --offline - Do not search online for additional JSON files. - Run offline mode (defaults to false) and for - remote windows symbol tables, linux/mac banner repositories. + Run offline mode (defaults to false). Do not search online for additional JSON files, remote windows symbol tables, nor linux/mac banner repositories. --single-location SINGLE_LOCATION This specifies a URL which will be downloaded if necessary, and built @@ -152,7 +150,7 @@ but can be overridden by creating a JSON file (`%APPDATA%/volatility3/vol.json` systems, or `~/.config/volatility3/vol.json` or `volshell.json` for all others). The format of this file is a JSON dictionary, containing the options above and their value. -It should be noted that the ordering is (`<` means is overridden by): +It should be noted that the ordering is (`x < y` means `x` is overridden by `y`): `in-built default value < config file value < command line parameter` diff --git a/doc/source/vol2to3.rst b/doc/source/vol2to3.rst index e768df0c2..9b5a739f8 100644 --- a/doc/source/vol2to3.rst +++ b/doc/source/vol2to3.rst @@ -27,7 +27,7 @@ The object model has changed as well, objects now inherit directly from their Py object is actually a Python integer (and has all the associated methods, and can be used wherever a normal int could). In Volatility 2, a complex proxy object was constructed which tried to emulate all the methods of the host object, but ultimately it was a different type and could not be used in the same places (critically, it could make the ordering of -operations important, since a + b might not work, but b + a might work fine). +operations important, since x + y might not work, but y + x might work fine). Volatility 3 has also had significant speed improvements, where Volatility 2 was designed to allow access to live memory images and situations in which the underlying data could change during the run of the plugin, in Volatility 3 the data @@ -36,11 +36,11 @@ This was because live memory analysis was barely ever used, and this feature cou re-read many times over for no benefit (particularly since each re-read could result in many additional image reads from following page table translations). -Finally, in order to provide Volatility specific information without impact on the ability for structures to have members +Further, in order to provide Volatility specific information without impact on the ability for structures to have members with arbitrary names, all the metadata about the object (such as its layer or offset) have been moved to a read-only :py:meth:`~volatility3.framework.interfaces.objects.ObjectInterface.vol` dictionary. -Further the distinction between a :py:class:`~volatility3.framework.interfaces.objects.Template` (the thing that +Finally, the distinction between a :py:class:`~volatility3.framework.interfaces.objects.Template` (the thing that constructs an object) and the :py:class:`Object ` itself has been made more explicit. In Volatility 2, some information (such as size) could only be determined from a constructed object, leading to instantiating a template on an empty buffer, just to determine the size. In Volatility 3, templates contain @@ -56,15 +56,14 @@ Volatility 2 were strictly limited to a stack, one on top of one other. In Vola Automagic --------- -In Volatility 2, we often tried to make this simpler for both users and developers. This resulted in something was -referred to as automagic, in that it was magic that happened automatically. We've now codified that more, so that the +In Volatility 2, we often tried to make this simpler for both users and developers. This resulted in something referred to as automagic, in that it was magic that happened automatically. We've now codified that more, so that the automagic processes are clearly defined and can be enabled or disabled as necessary for any particular run. We also included a stacker automagic to emulate the most common feature of Volatility 2, automatically stacking address spaces (now translation layers) on top of each other. -By default the automagic chosen to be run are determined based on the plugin requested, so that linux plugins get linux -specific automagic and windows plugins get windows specific automagic. This should reduce unnecessarily searching for -linux kernels in a windows image, for example. At the moment this is not user configurableS. +By default the automagic chosen to be run are determined based on the plugin requested, so that Linux plugins get Linux +specific automagic and Windows plugins get Windows specific automagic. This should reduce unnecessarily searching for +Linux kernels in a Windows image, for example. At the moment this is not user configurable. Searching and Scanning ---------------------- diff --git a/doc/source/volshell.rst b/doc/source/volshell.rst index 5a4b21ade..47ea2e905 100644 --- a/doc/source/volshell.rst +++ b/doc/source/volshell.rst @@ -36,7 +36,7 @@ operating system mode for volshell, and the current layer available for use. (primary) >>> -Volshell itself in essentially a plugin, but an interactive one. As such, most values are accessed through `self` +Volshell itself is essentially a plugin, but an interactive one. As such, most values are accessed through `self` although there is also a `context` object whenever a context must be provided. The prompt for the tool will indicate the name of the current layer (which can be accessed as `self.current_layer` @@ -92,7 +92,7 @@ It can also be provided with an object and will interpret the data for each in t 0x2e8 : UniqueProcessId symbol_table_name1!pointer 4 ... -These values can be accessed directory as attributes +These values can be accessed directly as attributes :: @@ -144,12 +144,12 @@ We can provide arguments via the `dpo` method call: 356 4 smss.exe 0x8c0bccf8d040 3 - N/A False 2021-03-13 17:25:33.000000 N/A Disabled ... -Here's we've provided the kernel name that was requested by the volshell plugin itself (the generic volshell does not +Here we've provided the kernel name that was requested by the volshell plugin itself (the generic volshell does not load a kernel module, and instead only has a TranslationLayerRequirement). A different module could be created and provided instead. The context used by the `dpo` method is always `context`. -Instead of print the results directly to screen, they can be gathered into a TreeGrid objects for direct access by +Instead of printing the results directly to screen, they can be gathered into a TreeGrid objects for direct access by using the `generate_treegrid` or `gt` command. :: @@ -180,15 +180,68 @@ used: layer = cc(mynewlayer.MyNewLayer, on_top_of = 'primary', other_parameter = 'important') with open('output.dmp', 'wb') as fp: - for i in range(0, 1073741824, 0x1000): + for i in range(0, 0x4000000, 0x1000): data = layer.read(i, 0x1000, pad = True) fp.write(data) As this demonstrates, all of the python is accessible, as are the volshell built in functions (such as `cc` which creates a constructable, like a layer or a symbol table). +User Convenience +---------------- + +There are functions available that make often-done tasks easier, and generally provide a shell-like experience. These can be listed using `help()` which, as already mentioned, is advertised when volshell starts. + Loading files -------------- +^^^^^^^^^^^^^ Files can be loaded as physical layers using the `load_file` or `lf` command, which takes a filename or a URI. This will be added to `context.layers` and can be accessed by the name returned by `lf`. + +Regex +^^^^^ + +It is easy to scan for some bytes or a pattern using `regex_scan` or `rx`. + +:: + + (layer_name) >>> rx(rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+") + 0x880001400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3. + 0x880001400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb + 0x880001400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists + 0x8800014000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc + 0x8800014000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3. + 0x8800014000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14 + 0x8800014000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia + 0x8800014000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u + + 0x880001769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3. + 0x880001769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb + 0x880001769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists + 0x880001769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc + 0x880001769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3. + 0x880001769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14 + 0x880001769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia + 0x880001769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u + + 0xffff81400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3. + 0xffff81400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb + 0xffff81400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists + 0xffff814000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc + 0xffff814000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3. + 0xffff814000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14 + 0xffff814000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia + 0xffff814000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u + + 0xffff81769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3. + 0xffff81769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb + 0xffff81769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists + 0xffff81769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc + 0xffff81769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3. + 0xffff81769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14 + 0xffff81769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia + 0xffff81769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u + +An optional size can be given for the displayed results as with the other fuctions (db, dw, dd, dq, etc). + +You can, of course, specify a different layer name as well. diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 6fb9f9ed3..000000000 --- a/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -mypy_path = ./stubs -show_traceback = True -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 2e1636a43..290a31abd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,68 @@ [project] name = "volatility3" description = "Memory forensics framework" -keywords = ["volatility", "memory", "forensics", "framework", "windows", "linux", "volshell"] +keywords = [ + "volatility", + "memory", + "forensics", + "framework", + "windows", + "linux", + "volshell", +] readme = "README.md" authors = [ { name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" }, ] requires-python = ">=3.8.0" license = { text = "VSL" } -dynamic = ["dependencies", "optional-dependencies", "version"] +dynamic = ["version"] + +dependencies = ["pefile>=2024.8.26"] + +[project.optional-dependencies] +full = [ + "yara-python>=4.5.1,<5", + "capstone>=5.0.3,<6", + "pycryptodome>=3.21.0,<4", + "leechcorepyc>=2.19.2,<3; sys_platform != 'darwin'", + # https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst + # 10.0.0 dropped support for Python3.7 + # 11.0.0 dropped support for Python3.8, which is still supported by Volatility3 + "pillow>=10.0.0,<11.0.0", +] + +cloud = ["gcsfs>=2024.10.0", "s3fs>=2024.10.0"] + +dev = [ + "volatility3[full,cloud]", + "jsonschema>=4.23.0,<5", + "pyinstaller>=6.5.0,<7", + "pyinstaller-hooks-contrib>=2024.9", + "types-jsonschema>=4.23.0,<5", +] + +arrow = ["pyarrow>=17.0.0"] + +test = [ + "volatility3[dev]", + "pytest>=8.3.3,<9", + "pytest-cov>=5,<7", + "yara-x>=0.10.0,<1", +] + +docs = [ + "volatility3[dev]", + "sphinx>=4.0.0,<9", + "sphinx-autodoc-typehints>=3.0.0,<4; python_version >= '3.11'", + "sphinx-rtd-theme>=3.0.1,<4", +] [project.urls] -Homepage = "https://github.com/volatilityfoundation/volatility3/" -"Bug Tracker" = "https://github.com/volatilityfoundation/volatility3/issues" -Documentation = "https://volatility3.readthedocs.io/" -"Source Code" = "https://github.com/volatilityfoundation/volatility3" +homepage = "https://github.com/volatilityfoundation/volatility3/" +documentation = "https://volatility3.readthedocs.io/" +repository = "https://github.com/volatilityfoundation/volatility3" +issues = "https://github.com/volatilityfoundation/volatility3/issues" [project.scripts] vol = "volatility3.cli:main" @@ -22,11 +70,32 @@ volshell = "volatility3.cli.volshell:main" [tool.setuptools.dynamic] version = { attr = "volatility3.framework.constants._version.PACKAGE_VERSION" } -dependencies = { file = "requirements-minimal.txt" } [tool.setuptools.packages.find] include = ["volatility3*"] +[tool.mypy] +mypy_path = "./stubs" +show_traceback = true + +[tool.ruff] +line-length = 88 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "F", # pyflakes + "E", # pycodestyle errors + "W", # pycodestyle warnings + "G", # flake8-logging-format + "PIE", # flake8-pie + "UP", # pyupgrade +] + +ignore = [ + "E501", # ignore due to conflict with formatter +] + [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index ae3482290..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ --r requirements.txt - -# This can improve error messages regarding improperly configured ISF files, -# but is only recommended for development -jsonschema>=2.3.0 - -# Used to build executable file -pyinstaller>=6.5.0 -pyinstaller-hooks-contrib>=2024.3 \ No newline at end of file diff --git a/requirements-minimal.txt b/requirements-minimal.txt deleted file mode 100644 index c030b332d..000000000 --- a/requirements-minimal.txt +++ /dev/null @@ -1,2 +0,0 @@ -# These packages are required for core functionality. -pefile>=2023.2.7 #foo \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e0d366391..000000000 --- a/requirements.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Include the minimal requirements --r requirements-minimal.txt - -# The following packages are optional. -# If certain packages are not necessary, place a comment (#) at the start of the line. - -# This is required for the yara plugins -yara-python>=3.8.0 - -# This is required for several plugins that perform malware analysis and disassemble code. -# It can also improve accuracy of Windows 8 and later memory samples. -# FIXME: Version 6.0.0 is incompatible (#1336) so we'll need an adaptor at some point -capstone>=3.0.5,<6.0.0 - -# This is required by plugins that decrypt passwords, password hashes, etc. -pycryptodome - -# This is required for memory acquisition via leechcore/pcileech. -leechcorepyc>=2.4.0; sys_platform != 'darwin' - -# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage -gcsfs>=2023.1.0 -s3fs>=2023.1.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 3af033160..000000000 --- a/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 setuptools - - -def get_requires(filename): - requirements = [] - with open(filename, "r", encoding="utf-8") as fh: - for line in fh.readlines(): - stripped_line = line.strip() - if stripped_line == "" or stripped_line.startswith(("#", "-r")): - continue - requirements.append(stripped_line) - return requirements - - -setuptools.setup( - extras_require={ - "dev": get_requires("requirements-dev.txt"), - "full": get_requires("requirements.txt"), - }, -) diff --git a/test/README.md b/test/README.md index dcbe289b0..5891d9508 100644 --- a/test/README.md +++ b/test/README.md @@ -2,14 +2,12 @@ ## Requirements -The Volatility 3 Testing Framework requires the same version of Python as Volatility3 itself. To install the current set of dependencies that the framework requires, use a command like this: +The Volatility 3 Testing Framework requires the same version of Python as Volatility 3 itself. To install the current set of dependencies that the framework requires, use a command like this: ```shell -pip3 install -r requirements-testing.txt +pip3 install -e .[test] ``` -NOTE: `requirements-testing.txt` can be found in this current `test/` directory. - ## Quick Start: Manual Testing 1. To test Volatility 3 on an image, first download one with a command such as: diff --git a/test/__init__.py b/test/__init__.py index e69de29bb..b2cfa3139 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -0,0 +1,22 @@ +from enum import Enum +from pathlib import Path + +TESTS_ROOT_DIR = Path(__file__).parent +WINDOWS_TESTS_DATA_DIR = TESTS_ROOT_DIR / "plugins" / "windows" / "test_data" + + +class Sample: + def __init__(self, path: str): + self.path = path + + +class WindowsSamples(Enum): + WINDOWSXP_GENERIC = Sample("./test_images/win-xp-laptop-2005-06-25.img") + """WindowsXP sample from early Volatility training.""" + WINDOWS10_GENERIC = Sample("./test_images/win-10_19041-2025_03.dmp") + """Windows10 CrashDump sample.""" + + +class LinuxSamples(Enum): + LINUX_GENERIC = Sample("./test_images/linux-sample-1.bin") + """Linux Debian 3.2.0-4 sample from early Volatility training.""" diff --git a/test/conftest.py b/test/conftest.py index 4ad63065b..0115fade9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -35,16 +35,39 @@ def pytest_addoption(parser): def pytest_generate_tests(metafunc): """Parameterize tests based on image names""" - images = metafunc.config.getoption("image") + images = metafunc.config.getoption("image").copy() for image_dir in metafunc.config.getoption("image_dir"): - images = images + [ - os.path.join(image_dir, dir) for dir in os.listdir(image_dir) + images += [ + os.path.join(image_dir, dir_name) for dir_name in os.listdir(image_dir) ] - # tests with "image" parameter are run against images + # tests with "image" parameter are run against image if "image" in metafunc.fixturenames: + filtered_images = [] + ids = [] + for image in images: + image_base = os.path.basename(image) + test_name = metafunc.definition.originalname + if test_name.startswith("test_windows_") and not image_base.startswith( + "win-" + ): + continue + elif test_name.startswith("test_linux_") and not image_base.startswith( + "linux-" + ): + continue + elif test_name.startswith("test_mac_") and not image_base.startswith( + "mac-" + ): + continue + + filtered_images.append(image) + ids.append(image_base) + metafunc.parametrize( - "image", images, ids=[os.path.basename(image) for image in images] + "image", + filtered_images, + ids=ids, ) diff --git a/test/plugins/linux/__init__.py b/test/plugins/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py new file mode 100644 index 000000000..e81277087 --- /dev/null +++ b/test/plugins/linux/linux.py @@ -0,0 +1,654 @@ +import contextlib +import tempfile +import os +import re +from test import test_volatility, LinuxSamples + + +class TestLinuxVolshell: + def test_linux_volshell(self, image, volatility, python): + out = test_volatility.basic_volshell_test( + image, volatility, python, volshellargs=("-l",) + ) + assert out.count(b" 100 + + +class TestLinuxPslist: + def test_linux_generic_pslist(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pslist.PsList", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1) + assert out.find(b"watchdog") != -1 + assert out.count(b"\n") > 10 + + +class TestLinuxCheckIdt: + def test_linux_generic_check_idt(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.check_idt.Check_idt", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.count(b"__kernel__") >= 10 + assert out.count(b"\n") > 10 + + +class TestLinuxCheckSyscall: + def test_linux_generic_check_syscall(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.check_syscall.Check_syscall", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.find(b"sys_close") != -1 + assert out.find(b"sys_open") != -1 + assert out.count(b"\n") > 100 + + +class TestLinuxLsmod: + def test_linux_generic_lsmod(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.lsmod.Lsmod", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.count(b"\n") > 10 + + +class TestLinuxLsof: + def test_linux_generic_lsof(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.lsof.Lsof", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.count(b"socket:") >= 10 + assert out.count(b"\n") > 35 + + +class TestLinuxProcMaps: + def test_linux_generic_proc_maps(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.proc.Maps", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.count(b"anonymous mapping") >= 10 + assert out.count(b"\n") > 100 + + +class TestLinuxTtyCheck: + def test_linux_generic_tty_check(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.tty_check.Tty_Check", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.find(b"__kernel__") != -1 + assert out.count(b"\n") >= 5 + + +class TestLinuxSockstat: + def test_linux_generic_sockstat(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.sockstat.Sockstat", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"AF_UNIX") >= 354 + assert out.count(b"AF_BLUETOOTH") >= 5 + assert out.count(b"AF_INET") >= 32 + assert out.count(b"AF_INET6") >= 20 + assert out.count(b"AF_PACKET") >= 1 + assert out.count(b"AF_NETLINK") >= 43 + + +class TestLinuxLibraryList: + def test_linux_specific_library_list(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "linux.library_list.LibraryList", + image, + volatility, + python, + pluginargs=("--pids", "2363"), + ) + + assert rc == 0 + assert re.search( + rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2", + out, + ) + + assert out.count(b"\n") > 10 + + +class TestLinuxPstree: + def test_linux_generic_pstree(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pstree.PsTree", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1) + assert out.count(b"\n") > 10 + + +class TestLinuxPidhashtable: + def test_linux_generic_pidhashtable(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pidhashtable.PIDHashTable", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1) + assert out.count(b"\n") > 10 + + +class TestLinuxBash: + def test_linux_bash(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.bash.Bash", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxBoottime: + def test_linux_generic_boottime(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.boottime.Boottime", image, volatility, python + ) + + assert rc == 0 + out = out.lower() + assert out.count(b"utc") >= 1 + + +class TestLinuxCapabilities: + def test_linux_generic_capabilities(self, image, volatility, python): + rc, out, err = test_volatility.runvol_plugin( + "linux.capabilities.Capabilities", + image, + volatility, + python, + globalargs=("-vvv",), + ) + + if rc != 0 and err.count(b"Unsupported kernel capabilities implementation") > 0: + # The linux-sample-1.bin kernel implementation isn't supported. + # However, we can still check that the plugin requirements are met. + return None + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxCheckCreds: + def test_linux_generic_check_creds(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.check_creds.Check_creds", image, volatility, python + ) + + # linux-sample-1.bin has no processes sharing credentials. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxElfs: + def test_linux_generic_elfs(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.elfs.Elfs", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxEnvars: + def test_linux_generic_envars(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.envars.Envars", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxKthreads: + def test_linux_generic_kthreads(self, image, volatility, python): + rc, out, err = test_volatility.runvol_plugin( + "linux.kthreads.Kthreads", + image, + volatility, + python, + globalargs=("-vvv",), + ) + + if rc != 0 and err.count(b"Unsupported kthread implementation") > 0: + # The linux-sample-1.bin kernel implementation isn't supported. + # However, we can still check that the plugin requirements are met. + return None + + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxMalfind: + def test_linux_generic_malfind(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.malfind.Malfind", image, volatility, python + ) + + # linux-sample-1.bin has no process memory ranges with potential injected code. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxMountinfo: + def test_linux_generic_mountinfo(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.mountinfo.MountInfo", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxPsaux: + def test_linux_generic_psaux(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.psaux.PsAux", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 50 + + +class TestLinuxPtrace: + def test_linux_generic_ptrace(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.ptrace.Ptrace", image, volatility, python + ) + + # linux-sample-1.bin has no processes being ptraced. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxVmaregexscan: + def test_linux_generic_vmaregexscan(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.vmaregexscan.VmaRegExScan", + image, + volatility, + python, + pluginargs=("--pid", "1", "--pattern", "\\x7fELF"), + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxVmayarascanYaraRule: + def test_linux_specific_vmayarascan_yara_rule(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + yara_rule_01 = r""" + rule fullvmayarascan + { + strings: + $s1 = "_nss_files_parse_grent" + $s2 = "/lib64/ld-linux-x86-64.so.2" + $s3 = "(bufferend - (char *) 0) % sizeof (char *) == 0" + condition: + all of them + } + """ + + # FIXME: When the minimum Python version includes 3.12, replace the following with: + # with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ... + fd, filename = tempfile.mkstemp(suffix=".yar") + try: + with os.fdopen(fd, "w") as f: + f.write(yara_rule_01) + + rc, out, _err = test_volatility.runvol_plugin( + "linux.vmayarascan.VmaYaraScan", + image, + volatility, + python, + pluginargs=("--pid", "8600", "--yara-file", filename), + ) + finally: + with contextlib.suppress(FileNotFoundError): + os.remove(filename) + + assert rc == 0 + assert out.count(b"\n") > 4 + + +class TestLinuxVmayarascanYaraString: + def test_linux_generic_vmayarascan_yara_string(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.vmayarascan.VmaYaraScan", + image, + volatility, + python, + pluginargs=("--pid", "1", "--yara-string", "ELF"), + ) + + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestLinuxPageCacheFiles: + def test_linux_specific_page_cache_files(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "linux.pagecache.Files", + image, + volatility, + python, + pluginargs=("--find", "/etc/passwd"), + ) + + assert rc == 0 + assert out.count(b"\n") > 4 + + # inode_num inode_addr ... file_path + assert re.search( + rb"146829\s0x88001ab5c270.*?/etc/passwd", + out, + ) + + +class TestLinuxPageCacheInodepages: + def test_linux_specific_page_cache_inodepages(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + inode_address = hex(0x88001AB5C270) + inode_dump_filename = f"inode_{inode_address}.dmp" + + rc, out, _err = test_volatility.runvol_plugin( + "linux.pagecache.InodePages", + image, + volatility, + python, + pluginargs=("--inode", inode_address), + ) + + assert rc == 0 + assert out.count(b"\n") > 4 + + # PageVAddr PagePAddr MappingAddr .. DumpSafe + assert re.search( + rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True", + out, + ) + + try: + rc, out, _err = test_volatility.runvol_plugin( + "linux.pagecache.InodePages", + image, + volatility, + python, + pluginargs=("--inode", inode_address, "--dump"), + ) + + assert rc == 0 + assert out.count(b"\n") >= 4 + + assert os.path.exists(inode_dump_filename) + with open(inode_dump_filename, "rb") as fp: + inode_contents = fp.read() + assert inode_contents.count(b"\n") > 30 + assert inode_contents.count(b"root:x:0:0:root:/root:/bin/bash") > 0 + finally: + with contextlib.suppress(FileNotFoundError): + os.remove(inode_dump_filename) + + +class TestLinuxCheckAfinfo: + def test_linux_generic_check_afinfo(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.check_afinfo.Check_afinfo", image, volatility, python + ) + + # linux-sample-1.bin has no suspicious results. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxCheckModules: + def test_linux_generic_check_modules(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.check_modules.Check_modules", image, volatility, python + ) + + # linux-sample-1.bin has no suspicious results. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxEbpf: + def test_linux_generic_ebpf_progs(self, image, volatility, python): + rc, out, err = test_volatility.runvol_plugin( + "linux.ebpf.EBPF", + image, + volatility, + python, + globalargs=("-vvv",), + ) + + if rc != 0 and err.count(b"Unsupported kernel") > 0: + # The linux-sample-1.bin kernel implementation isn't supported. + # However, we can still check that the plugin requirements are met. + return None + + assert rc == 0 + assert out.count(b"\n") > 4 + + +class TestLinuxIomem: + def test_linux_generic_iomem(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.iomem.IOMem", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 100 + + +class TestLinuxKeyboardNotifiers: + def test_linux_generic_keyboard_notifiers(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.keyboard_notifiers.Keyboard_notifiers", + image, + volatility, + python, + ) + + # linux-sample-1.bin has no suspicious results for this plugin. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxKmesg: + def test_linux_generic_kmesg(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.kmsg.Kmsg", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 100 + + +class TestLinuxNetfilter: + def test_linux_generic_netfilter(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.netfilter.Netfilter", image, volatility, python + ) + + # linux-sample-1.bin has no suspicious results for this plugin. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxPsscan: + def test_linux_generic__psscan(self, image, volatility, python): + rc, out, _err = test_volatility.runvol_plugin( + "linux.psscan.PsScan", image, volatility, python + ) + + assert rc == 0 + assert out.count(b"\n") > 100 + + +class TestLinuxHiddenModules: + def test_linux_specific_hidden_modules(self, volatility, python): + # TODO: this check should be specific, against a distinct infected sample + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "linux.malware.hidden_modules.Hidden_modules", image, volatility, python + ) + + # linux-sample-1.bin has no hidden modules. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") >= 4 + + +class TestLinuxIpAddr: + def test_linux_specific_ip_addr(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, err = test_volatility.runvol_plugin( + "linux.ip.Addr", image, volatility, python + ) + + assert re.search( + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+192.168.201.161\s+24\s+global\s+UP", + out, + ) + assert re.search( + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+UP", + out, + ) + assert out.count(b"\n") >= 8 + assert rc == 0 + + +class TestLinuxIpLink: + def test_linux_specific_ip_link(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, err = test_volatility.runvol_plugin( + "linux.ip.Link", image, volatility, python + ) + + assert re.search( + rb"-\s+lo\s+00:00:00:00:00:00\s+UNKNOWN\s+16436\s+noqueue\s+0\s+LOOPBACK,LOWER_UP,UP", + out, + ) + assert re.search( + rb"-\s+eth0\s+00:0c:29:8f:ed:ca\s+UP\s+1500\s+pfifo_fast\s+1000\s+BROADCAST,LOWER_UP,MULTICAST,UP", + out, + ) + assert out.count(b"\n") >= 6 + assert rc == 0 + + +class TestLinuxKallsyms: + def test_linux_specific_kallsyms(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "linux.kallsyms.Kallsyms", + image, + volatility, + python, + pluginargs=("--modules",), + ) + # linux-sample-1.bin has no hidden modules. + # This validates that plugin requirements are met and exceptions are not raised. + assert rc == 0 + assert out.count(b"\n") > 1000 + + # Addr Type Size Exported SubSystem ModuleName SymbolName Description + # 0xffffa009eba9 t 28 False module usbcore usb_mon_register Symbol is in the text (code) section + assert re.search( + rb"0xffffa009eba9\s+t\s+28\s+False\s+module\s+usbcore\s+usb_mon_register\s+Symbol is in the text \(code\) section", + out, + ) + + +class TestLinuxPscallstack: + def test_linux_specific_pscallstack(self, volatility, python): + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "linux.pscallstack.PsCallStack", + image, + volatility, + python, + pluginargs=("--pid", "1"), + ) + + assert rc == 0 + assert out.count(b"\n") > 30 + + # TID Comm Position Address Value Name Type Module + # 1 init 39 0x88001f999a40 0xffff81109039 do_select T kernel + assert re.search( + rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel", + out, + ) + + +class TestLinuxSockscan: + def test_linux_sockscan(self, volatility, python): + # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, err = test_volatility.runvol_plugin( + "linux.sockscan.Sockscan", image, volatility, python + ) + + # ensure that multiple unix paths for sockets have been found + assert ( + len( + re.findall( + rb"(/[ -~]+?){1,8}", + out, + ) + ) + >= 10 + ) + + # ensure that multiple IPv4 addresses have been found + assert ( + len( + re.findall( + rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}", + out, + ) + ) + >= 10 + ) + + assert out.count(b"\n") >= 50 + assert rc == 0 diff --git a/test/plugins/windows/test_data/windows.driverirp.DriverIrp.json b/test/plugins/windows/test_data/windows.driverirp.DriverIrp.json new file mode 100644 index 000000000..f026e54a2 --- /dev/null +++ b/test/plugins/windows/test_data/windows.driverirp.DriverIrp.json @@ -0,0 +1,32 @@ +{ + "GENERIC": [ + "IRP_MJ_CREATE", + "IRP_MJ_CREATE_NAMED_PIPE", + "IRP_MJ_CLOSE", + "IRP_MJ_READ", + "IRP_MJ_WRITE", + "IRP_MJ_QUERY_INFORMATION", + "IRP_MJ_SET_INFORMATION", + "IRP_MJ_QUERY_EA", + "IRP_MJ_SET_EA", + "IRP_MJ_FLUSH_BUFFERS", + "IRP_MJ_QUERY_VOLUME_INFORMATION", + "IRP_MJ_SET_VOLUME_INFORMATION", + "IRP_MJ_DIRECTORY_CONTROL", + "IRP_MJ_FILE_SYSTEM_CONTROL", + "IRP_MJ_DEVICE_CONTROL", + "IRP_MJ_INTERNAL_DEVICE_CONTROL", + "IRP_MJ_SHUTDOWN", + "IRP_MJ_LOCK_CONTROL", + "IRP_MJ_CLEANUP", + "IRP_MJ_CREATE_MAILSLOT", + "IRP_MJ_QUERY_SECURITY", + "IRP_MJ_SET_SECURITY", + "IRP_MJ_POWER", + "IRP_MJ_SYSTEM_CONTROL", + "IRP_MJ_DEVICE_CHANGE", + "IRP_MJ_QUERY_QUOTA", + "IRP_MJ_SET_QUOTA", + "IRP_MJ_PNP" + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.info.Info.json b/test/plugins/windows/test_data/windows.info.Info.json new file mode 100644 index 000000000..ef350389f --- /dev/null +++ b/test/plugins/windows/test_data/windows.info.Info.json @@ -0,0 +1,96 @@ +{ + "WINDOWS10_GENERIC": [ + { + "Value": "0xf8043601f000", + "Variable": "Kernel Base" + }, + { + "Value": "0x6d4000", + "Variable": "DTB" + }, + { + "Value": "True", + "Variable": "Is64Bit" + }, + { + "Value": "False", + "Variable": "IsPAE" + }, + { + "Value": "0 WindowsIntel32e", + "Variable": "layer_name" + }, + { + "Value": "1 WindowsCrashDump64Layer", + "Variable": "memory_layer" + }, + { + "Value": "2 FileLayer", + "Variable": "base_layer" + }, + { + "Value": "0xf80436c1fb20", + "Variable": "KdDebuggerDataBlock" + }, + { + "Value": "19041.1.amd64fre.vb_release.1912", + "Variable": "NTBuildLab" + }, + { + "Value": "0", + "Variable": "CSDVersion" + }, + { + "Value": "0xf80436c2e420", + "Variable": "KdVersionBlock" + }, + { + "Value": "15.19041", + "Variable": "Major/Minor" + }, + { + "Value": "34404", + "Variable": "MachineType" + }, + { + "Value": "1", + "Variable": "KeNumberProcessors" + }, + { + "Value": "2025-03-06 17:59:20+00:00", + "Variable": "SystemTime" + }, + { + "Value": "C:\\Windows", + "Variable": "NtSystemRoot" + }, + { + "Value": "NtProductWinNt", + "Variable": "NtProductType" + }, + { + "Value": "10", + "Variable": "NtMajorVersion" + }, + { + "Value": "0", + "Variable": "NtMinorVersion" + }, + { + "Value": "10", + "Variable": "PE MajorOperatingSystemVersion" + }, + { + "Value": "0", + "Variable": "PE MinorOperatingSystemVersion" + }, + { + "Value": "34404", + "Variable": "PE Machine" + }, + { + "Value": "Tue Sep 26 06:53:33 2023", + "Variable": "PE TimeDateStamp" + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.pstree.PsTree.json b/test/plugins/windows/test_data/windows.pstree.PsTree.json new file mode 100644 index 000000000..139931730 --- /dev/null +++ b/test/plugins/windows/test_data/windows.pstree.PsTree.json @@ -0,0 +1,375 @@ +{ + "WINDOWS10_GENERIC": { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\winlogon.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:49:34+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "winlogon.exe", + "Offset(V)": 145201769754752, + "PID": 3616, + "PPID": 3568, + "Path": null, + "SessionId": 2, + "Threads": 3, + "Wow64": false, + "__children": [ + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\userinit.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:50:15+00:00", + "ExitTime": "2025-03-06T17:50:32+00:00", + "Handles": null, + "ImageFileName": "userinit.exe", + "Offset(V)": 145201786712256, + "PID": 4832, + "PPID": 3616, + "Path": null, + "SessionId": 2, + "Threads": 0, + "Wow64": false, + "__children": [ + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\explorer.exe", + "Cmd": "C:\\Windows\\Explorer.EXE", + "CreateTime": "2025-03-06T17:50:17+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "explorer.exe", + "Offset(V)": 145201787191488, + "PID": 4912, + "PPID": 4832, + "Path": "C:\\Windows\\Explorer.EXE", + "SessionId": 2, + "Threads": 57, + "Wow64": false, + "__children": [ + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\SecurityHealthSystray.exe", + "Cmd": "\"C:\\Windows\\System32\\SecurityHealthSystray.exe\" ", + "CreateTime": "2025-03-06T17:51:05+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "SecurityHealth", + "Offset(V)": 145201826054336, + "PID": 1860, + "PPID": 4912, + "Path": "C:\\Windows\\System32\\SecurityHealthSystray.exe", + "SessionId": 2, + "Threads": 2, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --no-startup-window --win-session-start", + "CreateTime": "2025-03-06T17:51:05+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201826906304, + "PID": 3952, + "PPID": 4912, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 61, + "Wow64": false, + "__children": [ + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=renderer --string-annotations --instant-process --pdf-upsell-enabled --video-capture-use-gpu-memory-buffer --lang=en-US --js-flags=--ms-user-locale= --device-scale-factor=1 --num-raster-threads=1 --renderer-client-id=21 --time-ticks-at-unix-epoch=-1741283277737123 --launch-time-ticks=556708795 --always-read-main-dll --field-trial-handle=3928,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=3996 /prefetch:1", + "CreateTime": "2025-03-06T17:57:14+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201787768960, + "PID": 5348, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 19, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=utility --utility-sub-type=edge_xpay_wallet.mojom.EdgeXPayWalletService --lang=en-US --service-sandbox-type=utility --string-annotations --always-read-main-dll --field-trial-handle=6740,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=6732 /prefetch:8", + "CreateTime": "2025-03-06T17:57:52+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201832050880, + "PID": 3876, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 8, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=utility --utility-sub-type=edge_search_indexer.mojom.SearchIndexerInterfaceBroker --lang=en-US --service-sandbox-type=search_indexer --message-loop-type-ui --string-annotations --always-read-main-dll --field-trial-handle=7016,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=7128 /prefetch:8", + "CreateTime": "2025-03-06T17:57:59+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201827619008, + "PID": 5604, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 14, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=utility --utility-sub-type=entity_extraction_service.mojom.Extractor --lang=en-US --service-sandbox-type=entity_extraction --onnx-enabled-for-ee --string-annotations --always-read-main-dll --field-trial-handle=5520,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=5608 /prefetch:8", + "CreateTime": "2025-03-06T17:57:16+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201827774656, + "PID": 1000, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 9, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=renderer --string-annotations --pdf-upsell-enabled --disable-gpu-compositing --video-capture-use-gpu-memory-buffer --lang=en-US --js-flags=--ms-user-locale= --device-scale-factor=1 --num-raster-threads=1 --renderer-client-id=31 --time-ticks-at-unix-epoch=-1741283277737123 --launch-time-ticks=596993509 --always-read-main-dll --field-trial-handle=6904,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=7092 /prefetch:1", + "CreateTime": "2025-03-06T17:57:54+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201839592000, + "PID": 7080, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 15, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=renderer --string-annotations --pdf-upsell-enabled --disable-gpu-compositing --video-capture-use-gpu-memory-buffer --lang=en-US --js-flags=--ms-user-locale= --device-scale-factor=1 --num-raster-threads=1 --renderer-client-id=35 --time-ticks-at-unix-epoch=-1741283277737123 --launch-time-ticks=625057436 --always-read-main-dll --field-trial-handle=5592,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=5688 /prefetch:1", + "CreateTime": "2025-03-06T17:58:23+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201828201216, + "PID": 5132, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 17, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=renderer --string-annotations --pdf-upsell-enabled --disable-gpu-compositing --video-capture-use-gpu-memory-buffer --lang=en-US --js-flags=--ms-user-locale= --device-scale-factor=1 --num-raster-threads=1 --renderer-client-id=36 --time-ticks-at-unix-epoch=-1741283277737123 --launch-time-ticks=625148972 --always-read-main-dll --field-trial-handle=7112,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=6396 /prefetch:1", + "CreateTime": "2025-03-06T17:58:23+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201839919232, + "PID": 5388, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 15, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --string-annotations --always-read-main-dll --field-trial-handle=2180,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=2512 /prefetch:3", + "CreateTime": "2025-03-06T17:51:14+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201835704512, + "PID": 6448, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 16, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:51:16+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201668321408, + "PID": 6672, + "PPID": 3952, + "Path": null, + "SessionId": 2, + "Threads": 9, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=utility --utility-sub-type=price_comparison_service.mojom.DataProcessor --lang=en-US --service-sandbox-type=entity_extraction --string-annotations --always-read-main-dll --field-trial-handle=6188,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=6580 /prefetch:8", + "CreateTime": "2025-03-06T17:58:44+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201830273728, + "PID": 6064, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 9, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": "\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --type=gpu-process --string-annotations --gpu-preferences=UAAAAAAAAADgAAAEAAAAAAAAAAAAAAAAAABgAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --always-read-main-dll --field-trial-handle=2472,i,15721868202469256575,6833569417170289141,262144 --variations-seed-version --mojo-platform-channel-handle=2468 /prefetch:2", + "CreateTime": "2025-03-06T17:51:14+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201834049728, + "PID": 6456, + "PPID": 3952, + "Path": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "SessionId": 2, + "Threads": 15, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:51:11+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "msedge.exe", + "Offset(V)": 145201786340096, + "PID": 6204, + "PPID": 3952, + "Path": null, + "SessionId": 2, + "Threads": 8, + "Wow64": false, + "__children": [] + } + ] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Users\\generic-user\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe", + "Cmd": "\"C:\\Users\\generic-user\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe\" /background", + "CreateTime": "2025-03-06T17:51:11+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "OneDrive.exe", + "Offset(V)": 145201834340544, + "PID": 6160, + "PPID": 4912, + "Path": "C:\\Users\\generic-user\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe", + "SessionId": 2, + "Threads": 22, + "Wow64": true, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\cmd.exe", + "Cmd": "\"C:\\Windows\\system32\\cmd.exe\" ", + "CreateTime": "2025-03-06T17:51:44+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "cmd.exe", + "Offset(V)": 145201834332288, + "PID": 784, + "PPID": 4912, + "Path": "C:\\Windows\\system32\\cmd.exe", + "SessionId": 2, + "Threads": 1, + "Wow64": false, + "__children": [ + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\conhost.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:51:49+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "conhost.exe", + "Offset(V)": 145201834446976, + "PID": 3896, + "PPID": 784, + "Path": null, + "SessionId": 2, + "Threads": 3, + "Wow64": false, + "__children": [] + } + ] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\notepad.exe", + "Cmd": "\"C:\\Windows\\system32\\notepad.exe\" ", + "CreateTime": "2025-03-06T17:52:33+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "notepad.exe", + "Offset(V)": 145201839497344, + "PID": 2968, + "PPID": 4912, + "Path": "C:\\Windows\\system32\\notepad.exe", + "SessionId": 2, + "Threads": 4, + "Wow64": false, + "__children": [] + } + ] + } + ] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\fontdrvhost.exe", + "Cmd": null, + "CreateTime": "2025-03-06T17:49:42+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "fontdrvhost.ex", + "Offset(V)": 145201770697088, + "PID": 3812, + "PPID": 3616, + "Path": null, + "SessionId": 2, + "Threads": 5, + "Wow64": false, + "__children": [] + }, + { + "Audit": "\\Device\\HarddiskVolume4\\Windows\\System32\\dwm.exe", + "Cmd": "\"dwm.exe\"", + "CreateTime": "2025-03-06T17:49:42+00:00", + "ExitTime": null, + "Handles": null, + "ImageFileName": "dwm.exe", + "Offset(V)": 145201770352768, + "PID": 3860, + "PPID": 3616, + "Path": "C:\\Windows\\system32\\dwm.exe", + "SessionId": 2, + "Threads": 16, + "Wow64": false, + "__children": [] + } + ] + } +} diff --git a/test/plugins/windows/test_data/windows.registry.hivescan.HiveScan.json b/test/plugins/windows/test_data/windows.registry.hivescan.HiveScan.json new file mode 100644 index 000000000..67d36af04 --- /dev/null +++ b/test/plugins/windows/test_data/windows.registry.hivescan.HiveScan.json @@ -0,0 +1,100 @@ +{ + "WINDOWS10_GENERIC": [ + { + "Offset": 213323072327680, + "__children": [] + }, + { + "Offset": 213323069669376, + "__children": [] + }, + { + "Offset": 213323011252224, + "__children": [] + }, + { + "Offset": 213322964488192, + "__children": [] + }, + { + "Offset": 213323011387392, + "__children": [] + }, + { + "Offset": 213322962362368, + "__children": [] + }, + { + "Offset": 213323041546240, + "__children": [] + }, + { + "Offset": 213323013046272, + "__children": [] + }, + { + "Offset": 213323061571584, + "__children": [] + }, + { + "Offset": 213323079548928, + "__children": [] + }, + { + "Offset": 213323067502592, + "__children": [] + }, + { + "Offset": 213323081900032, + "__children": [] + }, + { + "Offset": 213322954362880, + "__children": [] + }, + { + "Offset": 213322954346496, + "__children": [] + }, + { + "Offset": 213323014123520, + "__children": [] + }, + { + "Offset": 213323067707392, + "__children": [] + }, + { + "Offset": 213323070193664, + "__children": [] + }, + { + "Offset": 213323078791168, + "__children": [] + }, + { + "Offset": 213323069112320, + "__children": [] + }, + { + "Offset": 213323047776256, + "__children": [] + }, + { + "Offset": 213323013799936, + "__children": [] + }, + { + "Offset": 213322954985472, + "__children": [] + }, + { + "Offset": 213322954969088, + "__children": [] + }, + { + "Offset": 213323048636416, + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.registry.printkey.PrintKey.json b/test/plugins/windows/test_data/windows.registry.printkey.PrintKey.json new file mode 100644 index 000000000..ee069ec30 --- /dev/null +++ b/test/plugins/windows/test_data/windows.registry.printkey.PrintKey.json @@ -0,0 +1,61 @@ +{ + "WINDOWS10_GENERIC": [ + { + "Data": "", + "Hive Offset": 213322954346496, + "Key": "[NONAME]", + "Last Write Time": "2025-03-06T17:59:19+00:00", + "Name": "A", + "Type": "Key", + "Volatile": false + }, + { + "Data": "", + "Hive Offset": 213322954362880, + "Key": "\\REGISTRY\\MACHINE\\SYSTEM", + "Last Write Time": "2019-12-07T09:15:07+00:00", + "Name": "ControlSet001", + "Type": "Key", + "Volatile": false + }, + { + "Data": "", + "Hive Offset": 213322964488192, + "Key": "\\SystemRoot\\System32\\Config\\SOFTWARE", + "Last Write Time": "2025-03-06T17:38:01+00:00", + "Name": "Classes", + "Type": "Key", + "Volatile": false + }, + { + "Data": "", + "Hive Offset": 213323011252224, + "Key": "\\SystemRoot\\System32\\Config\\SAM", + "Last Write Time": "2025-01-31T13:01:49+00:00", + "Name": "SAM", + "Type": "Key", + "Volatile": false, + "__children": [] + }, + { + "Data": "", + "Hive Offset": 213323048636416, + "Key": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Last Write Time": "2025-03-06T17:50:04+00:00", + "Name": "SOFTWARE", + "Type": "Key", + "Volatile": false, + "__children": [] + }, + { + "Data": "", + "Hive Offset": 213323047776256, + "Key": "\\??\\C:\\Users\\generic-user\\AppData\\Local\\Microsoft\\Windows\\UsrClass.dat", + "Last Write Time": "2025-03-05T18:36:09+00:00", + "Name": ".eip", + "Type": "Key", + "Volatile": false, + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json b/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json new file mode 100644 index 000000000..fd1c997b0 --- /dev/null +++ b/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json @@ -0,0 +1,123 @@ +{ + "WINDOWS10_GENERIC": { + "Count": null, + "Focus Count": null, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": null, + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": null, + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "N/A", + "Time Focused": null, + "Type": "Key", + "__children": [ + { + "Count": 7, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-05T18:34:13+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Accessories\\Paint.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 07 00 00 00 00 00 00 00 07 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 90 86 6b 31 fd 8d db 01 00 00 00 00", + "Time Focused": "0:00:00.507000", + "Type": "Value", + "__children": [] + }, + { + "Count": 1, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T12:46:34+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools\\Registry Editor.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff f0 82 cf ca 95 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.501000", + "Type": "Value", + "__children": [] + }, + { + "Count": 4, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T17:36:33+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Windows PowerShell\\Windows PowerShell.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 04 00 00 00 00 00 00 00 04 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 10 67 cf 4d be 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.504000", + "Type": "Value", + "__children": [] + }, + { + "Count": 1, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T17:51:44+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\System Tools\\Command Prompt.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff d0 99 66 6c c0 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.501000", + "Type": "Value", + "__children": [] + }, + { + "Count": 1, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T17:52:33+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Accessories\\Notepad.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 00 62 ba 89 c0 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.501000", + "Type": "Value", + "__children": [] + }, + { + "Count": 2, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T17:56:50+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools\\Task Scheduler.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 02 00 00 00 00 00 00 00 02 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff b0 24 49 23 c1 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.502000", + "Type": "Value", + "__children": [] + }, + { + "Count": 1, + "Focus Count": 0, + "Hive Name": "\\??\\C:\\Users\\generic-user\\ntuser.dat", + "Hive Offset": 213323048636416, + "ID": null, + "Last Updated": "2025-03-06T17:57:09+00:00", + "Last Write Time": "2025-03-06T17:57:09+00:00", + "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk", + "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 60 3d 89 2e c1 8e db 01 00 00 00 00", + "Time Focused": "0:00:00.501000", + "Type": "Value", + "__children": [] + } + ] + } +} diff --git a/test/plugins/windows/test_data/windows.sessions.Sessions.json b/test/plugins/windows/test_data/windows.sessions.Sessions.json new file mode 100644 index 000000000..30b202adb --- /dev/null +++ b/test/plugins/windows/test_data/windows.sessions.Sessions.json @@ -0,0 +1,36 @@ +{ + "WINDOWS10_GENERIC": [ + { + "Create Time": "2025-03-06T17:48:02+00:00", + "Process": "System", + "Process ID": 4 + }, + { + "Create Time": "2025-03-06T17:48:37+00:00", + "Process": "lsass.exe", + "Process ID": 696, + "Session ID": 0, + "Session Type": null, + "User Name": "/SYSTEM", + "__children": [] + }, + { + "Create Time": "2025-03-06T17:49:03+00:00", + "Process": "MsMpEng.exe", + "Process ID": 1956, + "Session ID": 0, + "Session Type": null, + "User Name": "WORKGROUP/Windows-generic$", + "__children": [] + }, + { + "Create Time": "2025-03-06T17:50:08+00:00", + "Process": "rdpclip.exe", + "Process ID": 4180, + "Session ID": 2, + "Session Type": null, + "User Name": "Windows-generic/generic-user", + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.shimcachemem.ShimcacheMem.json b/test/plugins/windows/test_data/windows.shimcachemem.ShimcacheMem.json new file mode 100644 index 000000000..b671f4c4b --- /dev/null +++ b/test/plugins/windows/test_data/windows.shimcachemem.ShimcacheMem.json @@ -0,0 +1,41 @@ +{ + "WINDOWS10_GENERIC": + [ + { + "Exec Flag": null, + "File Path": "C:\\Windows\\System32\\cmdl32.exe", + "File Size": null, + "Last Modified": "2019-12-07T09:09:07+00:00", + "Last Update": null, + "Order": 0, + "__children": [] + }, + { + "Exec Flag": null, + "File Path": "C:\\Windows\\System32\\cmdkey.exe", + "File Size": null, + "Last Modified": "2019-12-07T09:09:09+00:00", + "Last Update": null, + "Order": 1, + "__children": [] + }, + { + "Exec Flag": null, + "File Path": "C:\\Windows\\system32\\whoami.exe", + "File Size": null, + "Last Modified": "2019-12-07T09:09:51+00:00", + "Last Update": null, + "Order": 2, + "__children": [] + }, + { + "Exec Flag": null, + "File Path": null, + "File Size": null, + "Last Modified": "2023-12-13T16:43:28+00:00", + "Last Update": null, + "Order": 3, + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.timers.Timers.json b/test/plugins/windows/test_data/windows.timers.Timers.json new file mode 100644 index 000000000..88e8f339a --- /dev/null +++ b/test/plugins/windows/test_data/windows.timers.Timers.json @@ -0,0 +1,684 @@ +{ + "WINDOWSXP_GENERIC": [ + { + "DueTime": "0x00000001:0xb6912640", + "Module": "ntoskrnl", + "Offset": 2180960760, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xe2e5d9fc", + "Module": "ks", + "Offset": 2181100944, + "Period(ms)": 0, + "Routine": 4162614588, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000001:0x838d66d0", + "Module": "ntoskrnl", + "Offset": 2180726816, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xbe54efd0", + "Module": "NDIS", + "Offset": 2182586488, + "Period(ms)": 60000, + "Routine": 4164630316, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000000:0x6d915dc0", + "Module": "ntoskrnl", + "Offset": 2182771520, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xd5616870", + "Module": "HTTP", + "Offset": 4122586976, + "Period(ms)": 0, + "Routine": 4122527634, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa92535f", + "Module": "USBPORT", + "Offset": 2179614512, + "Period(ms)": 0, + "Routine": 4163343596, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000000:0x28ec7d80", + "Module": "ntoskrnl", + "Offset": 2167895896, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xd5f1ddd0", + "Module": "afd", + "Offset": 4289149728, + "Period(ms)": 0, + "Routine": 4146614848, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xe64e9c50", + "Module": "ntoskrnl", + "Offset": 4289091536, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000002:0x10691f50", + "Module": "BATTC", + "Offset": 2182647568, + "Period(ms)": 0, + "Routine": 4170619626, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000002:0x10691f50", + "Module": "BATTC", + "Offset": 2184852912, + "Period(ms)": 0, + "Routine": 4170619626, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xe650bd40", + "Module": "ntoskrnl", + "Offset": 4289091352, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xaf39aa70", + "Module": "watchdog", + "Offset": 2182133296, + "Period(ms)": 10000, + "Routine": 4170290884, + "Signaled": "Yes", + "Symbol": "_imp__KdSave", + "__children": [] + }, + { + "DueTime": "0x00000001:0xaf39aa70", + "Module": "watchdog", + "Offset": 2181849992, + "Period(ms)": 10000, + "Routine": 4170290884, + "Signaled": "Yes", + "Symbol": "_imp__KdSave", + "__children": [] + }, + { + "DueTime": "0x00000008:0x61e17090", + "Module": "ntoskrnl", + "Offset": 2153125248, + "Period(ms)": 0, + "Routine": 2152824977, + "Signaled": "-", + "Symbol": "ExpTimeRefreshDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xa9537f40", + "Module": "ntoskrnl", + "Offset": 2153083360, + "Period(ms)": 0, + "Routine": 2152814203, + "Signaled": "-", + "Symbol": "CmpLazyFlushDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xc5addbe0", + "Module": "TDI", + "Offset": 4147138000, + "Period(ms)": 0, + "Routine": 4169831408, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaadb3be0", + "Module": "netbt", + "Offset": 2182155920, + "Period(ms)": 0, + "Routine": 4146697354, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaadb9040", + "Module": "TDI", + "Offset": 2182059040, + "Period(ms)": 0, + "Routine": 4169831408, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xa9537f40", + "Module": "ntoskrnl", + "Offset": 2153083360, + "Period(ms)": 0, + "Routine": 2152814203, + "Signaled": "-", + "Symbol": "CmpLazyFlushDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x80000000:0x19eae820", + "Module": "ntoskrnl", + "Offset": 2182169704, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa9b2a20", + "Module": "NDIS", + "Offset": 2179568032, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000002:0x1a97a160", + "Module": "Ntfs", + "Offset": 4164841808, + "Period(ms)": 0, + "Routine": 4164732734, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000000:0x2c5fb7e0", + "Module": "ntoskrnl", + "Offset": 2180369200, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000002:0x3dcf47a0", + "Module": "afd", + "Offset": 2183169664, + "Period(ms)": 0, + "Routine": 4146614848, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xc4906e60", + "Module": "rdbss", + "Offset": 4146422176, + "Period(ms)": 0, + "Routine": 4146381701, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa92535f", + "Module": "USBPORT", + "Offset": 2179614512, + "Period(ms)": 0, + "Routine": 4163343596, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa8619e0", + "Module": "TDI", + "Offset": 2182064136, + "Period(ms)": 0, + "Routine": 4169831408, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xc45ec240", + "Module": "tcpip", + "Offset": 4147157776, + "Period(ms)": 100, + "Routine": 4146861021, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000000:0x35b72850", + "Module": "ntoskrnl", + "Offset": 2180437784, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xaad6fab0", + "Module": "NDIS", + "Offset": 2181321392, + "Period(ms)": 1000, + "Routine": 4164630316, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaadb9040", + "Module": "TDI", + "Offset": 2182059040, + "Period(ms)": 0, + "Routine": 4169831408, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xa9537f40", + "Module": "ntoskrnl", + "Offset": 2153083360, + "Period(ms)": 0, + "Routine": 2152814203, + "Signaled": "-", + "Symbol": "CmpLazyFlushDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xe2e5ee70", + "Module": "ipsec", + "Offset": 4147285920, + "Period(ms)": 60000, + "Routine": 4147221715, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xe2e5ee70", + "Module": "ipsec", + "Offset": 4147284744, + "Period(ms)": 0, + "Routine": 4147221577, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xe2e5d9fc", + "Module": "ks", + "Offset": 2181100944, + "Period(ms)": 0, + "Routine": 4162614588, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000098:0xa67683e0", + "Module": "NDIS", + "Offset": 2183038120, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xa6beb810", + "Module": "ntoskrnl", + "Offset": 2153086208, + "Period(ms)": 1000, + "Routine": 2152611575, + "Signaled": "Yes", + "Symbol": "IopTimerDispatch", + "__children": [] + }, + { + "DueTime": "0x00000001:0xe2f53650", + "Module": "ipnat", + "Offset": 4146164320, + "Period(ms)": 60000, + "Routine": 4146134936, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000001:0x838d66d0", + "Module": "ntoskrnl", + "Offset": 2180726816, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xa6c34da0", + "Module": "ntoskrnl", + "Offset": 2153118912, + "Period(ms)": 1000, + "Routine": 2152615232, + "Signaled": "Yes", + "Symbol": "PopScanIdleList", + "__children": [] + }, + { + "DueTime": "0x00000098:0xa67683e0", + "Module": "NDIS", + "Offset": 2183038120, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xa6d41cb0", + "Module": "ntoskrnl", + "Offset": 2153080200, + "Period(ms)": 0, + "Routine": 2152616496, + "Signaled": "-", + "Symbol": "CcScanDpc", + "__children": [] + }, + { + "DueTime": "0x80000000:0x14500b10", + "Module": "ntoskrnl", + "Offset": 2182113448, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xaba81b20", + "Module": "NDIS", + "Offset": 2182656416, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xab110bd0", + "Module": "ntoskrnl", + "Offset": 2181090272, + "Period(ms)": 0, + "Routine": 2152754200, + "Signaled": "-", + "Symbol": "CcPfTraceTimerRoutine", + "__children": [] + }, + { + "DueTime": "0x00000098:0xa6ad86a0", + "Module": "NDIS", + "Offset": 2182615456, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xd56a01e0", + "Module": "HTTP", + "Offset": 4122576520, + "Period(ms)": 60000, + "Routine": 4122481076, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xd56a7ca0", + "Module": "HTTP", + "Offset": 4122577120, + "Period(ms)": 30000, + "Routine": 4122510208, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xd56a7ca0", + "Module": "HTTP", + "Offset": 4122586816, + "Period(ms)": 0, + "Routine": 4122536296, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x80000000:0x14500b10", + "Module": "ntoskrnl", + "Offset": 2182113448, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000002:0x10691f50", + "Module": "BATTC", + "Offset": 2184852912, + "Period(ms)": 0, + "Routine": 4170619626, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000002:0x10691f50", + "Module": "BATTC", + "Offset": 2182647568, + "Period(ms)": 0, + "Routine": 4170619626, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa90b010", + "Module": "sr", + "Offset": 2184659528, + "Period(ms)": 0, + "Routine": 4165384494, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000131:0x3cf80b10", + "Module": "NDIS", + "Offset": 2181526704, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa9b2a20", + "Module": "NDIS", + "Offset": 2179568032, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa9cb150", + "Module": "NDIS", + "Offset": 2179569720, + "Period(ms)": 0, + "Routine": 4164628447, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa9e70c0", + "Module": "ntoskrnl", + "Offset": 2182248232, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xd56bd340", + "Module": "srv", + "Offset": 4128143248, + "Period(ms)": 0, + "Routine": 4128080773, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xbae86a40", + "Module": "ntoskrnl", + "Offset": 2182222184, + "Period(ms)": 0, + "Routine": 2152618407, + "Signaled": "-", + "Symbol": "ExpTimerDpcRoutine", + "__children": [] + }, + { + "DueTime": "0x00000001:0xacafcdd0", + "Module": "Ntfs", + "Offset": 4164841712, + "Period(ms)": 0, + "Routine": 4164719155, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaa92535f", + "Module": "USBPORT", + "Offset": 2179614512, + "Period(ms)": 0, + "Routine": 4163343596, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xad54f9d0", + "Module": "ntoskrnl", + "Offset": 2153085904, + "Period(ms)": 60000, + "Routine": 2152638914, + "Signaled": "Yes", + "Symbol": "IopIrpStackProfilerTimer", + "__children": [] + }, + { + "DueTime": "0x00000008:0x73b44670", + "Module": "ipsec", + "Offset": 4147284848, + "Period(ms)": 0, + "Routine": 4147221577, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaad6c270", + "Module": "NDIS", + "Offset": 2179663392, + "Period(ms)": 0, + "Routine": 4164642677, + "Signaled": "-", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000001:0xaad6fab0", + "Module": "NDIS", + "Offset": 2181321392, + "Period(ms)": 1000, + "Routine": 4164630316, + "Signaled": "Yes", + "Symbol": null, + "__children": [] + }, + { + "DueTime": "0x00000008:0x73bef8c0", + "Module": "netbt", + "Offset": 2182155520, + "Period(ms)": 0, + "Routine": 4146697354, + "Signaled": "-", + "Symbol": null, + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.unloadedmodules.UnloadedModules.json b/test/plugins/windows/test_data/windows.unloadedmodules.UnloadedModules.json new file mode 100644 index 000000000..e287d3c14 --- /dev/null +++ b/test/plugins/windows/test_data/windows.unloadedmodules.UnloadedModules.json @@ -0,0 +1,67 @@ +{ + "WINDOWS10_GENERIC": [ + { + "EndAddress": 18446735295750344704, + "Name": "hwpolicy.sys", + "StartAddress": 18446735295750275072, + "Time": "2025-03-06T17:47:55+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295728652288, + "Name": "WdBoot.sys", + "StartAddress": 18446735295728582656, + "Time": "2025-03-06T17:47:56+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295786053632, + "Name": "dam.sys", + "StartAddress": 18446735295785926656, + "Time": "2025-03-06T17:48:02+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295788797952, + "Name": "serial.sys", + "StartAddress": 18446735295788679168, + "Time": "2025-03-06T17:48:21+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295788875776, + "Name": "serenum.sys", + "StartAddress": 18446735295788810240, + "Time": "2025-03-06T17:48:21+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295761932288, + "Name": "dump_dumpfve.sys", + "StartAddress": 18446735295761809408, + "Time": "2025-03-06T17:48:27+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295761629184, + "Name": "dump_vmbkmcl.sys", + "StartAddress": 18446735295761481728, + "Time": "2025-03-06T17:48:27+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295761481728, + "Name": "dump_storvsc.sys", + "StartAddress": 18446735295761416192, + "Time": "2025-03-06T17:48:27+00:00", + "__children": [] + }, + { + "EndAddress": 18446735295761350656, + "Name": "dump_storport.sys", + "StartAddress": 18446735295761285120, + "Time": "2025-03-06T17:48:27+00:00", + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.vadinfo.VadInfo.json b/test/plugins/windows/test_data/windows.vadinfo.VadInfo.json new file mode 100644 index 000000000..d980b21a2 --- /dev/null +++ b/test/plugins/windows/test_data/windows.vadinfo.VadInfo.json @@ -0,0 +1,94 @@ +{ + "WINDOWS10_GENERIC": [ + { + "CommitCharge": 1, + "End VPN": 2147381247, + "File": null, + "File output": "Disabled", + "Offset": 18446607800399740208, + "PID": 4, + "Parent": 0, + "PrivateMemory": 1, + "Process": "System", + "Protection": "PAGE_READONLY", + "Start VPN": 2147377152, + "Tag": "VadS", + "__children": [] + }, + { + "CommitCharge": 1, + "End VPN": 2147356671, + "File": null, + "File output": "Disabled", + "Offset": 18446607800399739968, + "PID": 4, + "Parent": 18446607800399740208, + "PrivateMemory": 1, + "Process": "System", + "Protection": "PAGE_READONLY", + "Start VPN": 2147352576, + "Tag": "VadS", + "__children": [] + }, + { + "CommitCharge": 9, + "End VPN": 2004303871, + "File": "\\Windows\\SysWOW64\\ntdll.dll", + "File output": "Disabled", + "Offset": 18446607800410763840, + "PID": 4, + "Parent": 18446607800399739968, + "PrivateMemory": 0, + "Process": "System", + "Protection": "PAGE_EXECUTE_WRITECOPY", + "Start VPN": 2002583552, + "Tag": "Vad ", + "__children": [] + }, + { + "CommitCharge": 9, + "End VPN": 140703785033727, + "File": "\\Windows\\System32\\vertdll.dll", + "File output": "Disabled", + "Offset": 18446607800410768000, + "PID": 4, + "Parent": 18446607800399740208, + "PrivateMemory": 0, + "Process": "System", + "Protection": "PAGE_EXECUTE_WRITECOPY", + "Start VPN": 140703784828928, + "Tag": "Vad ", + "__children": [] + }, + { + "CommitCharge": 0, + "End VPN": 2873390796799, + "File": null, + "File output": "Disabled", + "Offset": 18446607800474375600, + "PID": 4, + "Parent": 18446607800410768000, + "PrivateMemory": 0, + "Process": "System", + "Protection": "PAGE_READWRITE", + "Start VPN": 2873390792704, + "Tag": "Vad ", + "__children": [] + }, + { + "CommitCharge": 16, + "End VPN": 140703787155455, + "File": "\\Windows\\System32\\ntdll.dll", + "File output": "Disabled", + "Offset": 18446607800410767520, + "PID": 4, + "Parent": 18446607800410768000, + "PrivateMemory": 0, + "Process": "System", + "Protection": "PAGE_EXECUTE_WRITECOPY", + "Start VPN": 140703785091072, + "Tag": "Vad ", + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.vadwalk.VadWalk.json b/test/plugins/windows/test_data/windows.vadwalk.VadWalk.json new file mode 100644 index 000000000..d7c884fca --- /dev/null +++ b/test/plugins/windows/test_data/windows.vadwalk.VadWalk.json @@ -0,0 +1,76 @@ +{ + "WINDOWS10_GENERIC": [ + { + "End": 2147381247, + "Left": 145201666899008, + "Offset": 145201666899248, + "PID": 4, + "Parent": 0, + "Process": "System", + "Right": 145201677927040, + "Start": 2147377152, + "Tag": "VadS", + "__children": [] + }, + { + "End": 2147356671, + "Left": 145201677922880, + "Offset": 145201666899008, + "PID": 4, + "Parent": 145201666899248, + "Process": "System", + "Right": 0, + "Start": 2147352576, + "Tag": "VadS", + "__children": [] + }, + { + "End": 2004303871, + "Left": 0, + "Offset": 145201677922880, + "PID": 4, + "Parent": 145201666899008, + "Process": "System", + "Right": 0, + "Start": 2002583552, + "Tag": "Vad ", + "__children": [] + }, + { + "End": 140703785033727, + "Left": 145201741534640, + "Offset": 145201677927040, + "PID": 4, + "Parent": 145201666899248, + "Process": "System", + "Right": 145201677926560, + "Start": 140703784828928, + "Tag": "Vad ", + "__children": [] + }, + { + "End": 2873390796799, + "Left": 0, + "Offset": 145201741534640, + "PID": 4, + "Parent": 145201677927040, + "Process": "System", + "Right": 0, + "Start": 2873390792704, + "Tag": "Vad ", + "__children": [] + }, + { + "End": 140703787155455, + "Left": 0, + "Offset": 145201677926560, + "PID": 4, + "Parent": 145201677927040, + "Process": "System", + "Right": 0, + "Start": 140703785091072, + "Tag": "Vad ", + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_data/windows.virtmap.VirtMap.json b/test/plugins/windows/test_data/windows.virtmap.VirtMap.json new file mode 100644 index 000000000..6b80068f2 --- /dev/null +++ b/test/plugins/windows/test_data/windows.virtmap.VirtMap.json @@ -0,0 +1,82 @@ +{ + "WINDOWS10_GENERIC": [ + { + "End offset": 17592186044416, + "Region": "MiVaBootLoaded", + "Start offset": 238594023227392, + "__children": [] + }, + { + "End offset": 549755813888, + "Region": "MiVaDriverImages", + "Start offset": 272678883688448, + "__children": [] + }, + { + "End offset": 549755813888, + "Region": "MiVaHal", + "Start offset": 258385232527360, + "__children": [] + }, + { + "End offset": 3848290697216, + "Region": "MiVaNonPagedPool", + "Start offset": 266081813921792, + "__children": [] + }, + { + "End offset": 2748779069440, + "Region": "MiVaPagedPool", + "Start offset": 278135644927560, + "__children": [] + }, + { + "End offset": 17592186044416, + "Region": "MiVaPfnDatabase", + "Start offset": 166026255794176, + "__children": [] + }, + { + "End offset": 17592186044416, + "Region": "MiVaProcessSpace", + "Start offset": 191315023233024, + "__children": [] + }, + { + "End offset": 549755813888, + "Region": "MiVaSessionGlobalSpace", + "Start offset": 186367220908032, + "__children": [] + }, + { + "End offset": 17592186044416, + "Region": "MiVaSessionSpace", + "Start offset": 213305255788544, + "__children": [] + }, + { + "End offset": 1099511627776, + "Region": "MiVaSpecialPoolPaged", + "Start offset": 261683767410688, + "__children": [] + }, + { + "End offset": 1099511627776, + "Region": "MiVaSystemCache", + "Start offset": 208907209277440, + "__children": [] + }, + { + "End offset": 549755813888, + "Region": "MiVaSystemPtes", + "Start offset": 274328151130112, + "__children": [] + }, + { + "End offset": 17592186044416, + "Region": "MiVaUnused", + "Start offset": 145135534866432, + "__children": [] + } + ] +} \ No newline at end of file diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py index 8f771b323..6f8180000 100644 --- a/test/plugins/windows/test_scheduled_tasks.py +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -2,8 +2,10 @@ import sys import struct 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): def test_decode_exe_action(self): @@ -84,8 +86,7 @@ class TestActionsDecoding(unittest.TestCase): self.assertEqual(actions[0].action_type, scheduled_tasks.ActionType.Exe) except Exception: self.fail( - "ActionDecoder.decode should not raise exception:\n%s" - % traceback.format_exc() + f"ActionDecoder.decode should not raise exception:\n{traceback.format_exc()}" ) @@ -100,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 new file mode 100644 index 000000000..ff31ac109 --- /dev/null +++ b/test/plugins/windows/windows.py @@ -0,0 +1,1473 @@ +import contextlib +import hashlib +import json +import os +import shutil +import tempfile + +from test import WindowsSamples, test_volatility + + +class TestWindowsVolshell: + def test_windows_volshell(self, image, volatility, python): + out = test_volatility.basic_volshell_test( + image, volatility, python, volshellargs=("-w",) + ) + assert out.count(b" 40 + + +class TestWindowsPslist: + def test_windows_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.pslist.PsList", + image, + volatility, + python, + # Notice that this is needed to hit lru_cache when "specific" will run + globalargs=("-r", "json"), + ) + assert rc == 0 + out = out.lower() + assert out.find(b"system") != -1 + assert out.find(b"csrss.exe") != -1 + assert out.find(b"svchost.exe") != -1 + assert out.count(b"\n") > 10 + + def test_windows_specific_pslist(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + expected_row = { + "CreateTime": None, + "ExitTime": None, + "File output": "Disabled", + "Handles": 1140, + "ImageFileName": "System", + "Offset(V)": 2185004992, + "PID": 4, + "PPID": 0, + "SessionId": None, + "Threads": 61, + "Wow64": False, + "__children": [], + } + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + +class TestWindowsTimeliner: + def test_windows_specific_timeliner(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "timeliner.Timeliner", image, volatility, python + ) + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestWindowsPsscan: + def test_windows_specific_psscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.psscan.PsScan", image, volatility, python + ) + assert rc == 0 + out = out.lower() + assert out.find(b"system") != -1 + assert out.find(b"csrss.exe") != -1 + assert out.find(b"svchost.exe") != -1 + assert out.count(b"\n") > 10 + + +class TestWindowsDlllist: + def test_windows_generic_dlllist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.dlllist.DllList", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 2000 + expected_rows = [ + { + "Path": "C:\\Windows\\SYSTEM32\\kernel32.dll", + "Process": "csrss.exe", + }, + { + "Path": "C:\\Windows\\system32\\USER32.dll", + "Process": "csrss.exe", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row( + expected_row, json_out, case_sensitive=False + ) + + +class TestWindowsModules: + def test_windows_specific_modules(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.modules.Modules", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 110 + expected_rows = [ + { + "Name": "ntoskrnl.exe", + "Offset": 2185216944, + "Path": "\\WINDOWS\\system32\\ntoskrnl.exe", + "Size": 2179328, + }, + { + "Name": "hal.dll", + "Offset": 2185216840, + "Path": "\\WINDOWS\\system32\\hal.dll", + "Size": 81280, + }, + { + "Name": "netbios.sys", + "Offset": 2182050616, + "Path": "\\SystemRoot\\System32\\DRIVERS\\netbios.sys", + "Size": 36864, + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsDumpfiles: + def test_windows_specific_dumpfiles(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + with open("./test/known_files.json") as json_file: + known_files = json.load(json_file) + + failed_chksms = 0 + file_name = os.path.basename(image) + + try: + for addr in known_files["windows_dumpfiles"][file_name]: + path = tempfile.mkdtemp() + + rc, _out, _err = test_volatility.runvol_plugin( + "windows.dumpfiles.DumpFiles", + image, + volatility, + python, + globalargs=("-o", path), + pluginargs=("--virtaddr", addr), + ) + + for file in os.listdir(path): + with open(os.path.join(path, file), "rb") as fp: + if ( + hashlib.md5(fp.read()).hexdigest() + not in known_files["windows_dumpfiles"][file_name][addr] + ): + failed_chksms += 1 + + shutil.rmtree(path) + json_file.close() + + assert failed_chksms == 0 + assert rc == 0 + except Exception as e: + json_file.close() + print("Key Error raised on " + str(e)) + assert False + + +class TestWindowsHandles: + def test_windows_generic_handles(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.handles.Handles", + image, + volatility, + python, + pluginargs=("--pid", "4"), + ) + assert rc == 0 + assert out.find(b"System Pid 4") != -1 + assert ( + out.find( + b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS" + ) + != -1 + ) + assert out.find(b"MACHINE\\SYSTEM\\SETUP") != -1 + assert out.count(b"\n") > 500 + + +class TestWindowsSvcList: + def test_windows_generic_svclist(self, volatility, python, image): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.svclist.SvcList", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert len(json_out) > 250 + expected_row = { + "Binary": "\\Driver\\ACPI", + "Display": "ACPI", + "Name": "ACPI", + "Start": "SERVICE_BOOT_START", + "State": "SERVICE_RUNNING", + "Type": "SERVICE_KERNEL_DRIVER", + } + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsSvcScan: + def test_windows_generic_svcscan(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.svcscan.SvcScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert len(json_out) > 250 + expected_rows = [ + {"Name": "ACPI", "Type": "SERVICE_KERNEL_DRIVER"}, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsThrdscan: + def test_windows_specific_thrdscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.thrdscan.ThrdScan", image, volatility, python + ) + assert rc == 0 + assert out.count(b"\n") > 700 + assert out.find(b"\t1812\t2768\t0x7c810856") != -1 + assert out.find(b"\t840\t2964\t0x7c810856") != -1 + assert out.find(b"\t2536\t2552\t0x7c810856") != -1 + + +class TestWindowsPrivileges: + def test_windows_generic_privileges(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.privileges.Privs", + image, + volatility, + python, + pluginargs=("--pid", "4"), + ) + assert rc == 0 + assert out.find(b"SeCreateTokenPrivilege") != -1 + assert out.find(b"SeCreateGlobalPrivilege") != -1 + assert out.find(b"SeAssignPrimaryTokenPrivilege") != -1 + assert out.count(b"\n") > 20 + + +class TestWindowsGetSIDs: + def test_windows_generic_getsids(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.getsids.GetSIDs", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 400 + expected_rows = [ + { + "Name": "Local System", + "Process": "csrss.exe", + "SID": "S-1-5-18", + }, + { + "Name": "Administrators", + "Process": "csrss.exe", + "SID": "S-1-5-32-544", + }, + { + "Name": "Everyone", + "Process": "csrss.exe", + "SID": "S-1-1-0", + }, + { + "Name": "Authenticated Users", + "Process": "csrss.exe", + "SID": "S-1-5-11", + }, + { + "Name": "System Mandatory Level", + "Process": "csrss.exe", + "SID": "S-1-16-16384", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsEnvars: + def test_windows_generic_envars(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.envars.Envars", image, volatility, python + ) + assert rc == 0 + assert out.find(b"PATH") != -1 + assert out.find(b"PROCESSOR_ARCHITECTURE") != -1 + assert out.find(b"USERNAME") != -1 + assert out.find(b"SystemRoot") != -1 + assert out.find(b"CommonProgramFiles") != -1 + assert out.count(b"\n") > 500 + + +class TestWindowsCallbacks: + def test_windows_specific_callbacks(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.callbacks.Callbacks", image, volatility, python + ) + assert rc == 0 + assert out.find(b"PspCreateProcessNotifyRoutine") != -1 + assert out.find(b"KeBugCheckCallbackListHead") != -1 + assert out.find(b"KeBugCheckReasonCallbackListHead") != -1 + assert out.count(b"KeBugCheckReasonCallbackListHead ") > 5 + + +class TestWindowsVadwalk: + def test_windows_specific_vadwalk(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.vadwalk.VadWalk", + image, + volatility, + python, + globalargs=("-r", "json"), + pluginargs=("--pid", "4"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.vadwalk.VadWalk", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsDevicetree: + def test_windows_specific_devicetree(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.devicetree.DeviceTree", image, volatility, python + ) + assert rc == 0 + assert out.find(b"DEV") != -1 + assert out.find(b"DRV") != -1 + assert out.find(b"ATT") != -1 + assert out.find(b"FILE_DEVICE_CONTROLLER") != -1 + assert out.find(b"FILE_DEVICE_DISK") != -1 + assert out.find(b"FILE_DEVICE_DISK_FILE_SYSTEM") != -1 + + +class TestWindowsVadyarascan: + def test_windows_specific_vadyarascan_yara_rule(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + yara_rule_01 = r""" + rule fullvadyarascan + { + strings: + $s1 = "!This program cannot be run in DOS mode." + $s2 = "Qw))Pw" + $s3 = "W_wD)Pw" + $s4 = "1Xw+2Xw" + $s5 = "xd`wh``w" + $s6 = "0g`w0g`w8g`w8g`w@g`w@g`wHg`wHg`wPg`wPg`wXg`wXg`w`g`w`g`whg`whg`wpg`wpg`wxg`wxg`w" + condition: + all of them + } + """ + fd, filename = tempfile.mkstemp(suffix=".yar") + try: + with os.fdopen(fd, "w") as f: + f.write(yara_rule_01) + rc, out, _err = test_volatility.runvol_plugin( + "windows.vadyarascan.VadYaraScan", + image, + volatility, + python, + pluginargs=("--pid", "4012", "--yara-file", filename), + ) + finally: + with contextlib.suppress(FileNotFoundError): + os.remove(filename) + assert rc == 0 + assert out.count(b"\n") > 4 + + def test_windows_specific_vadyarascan_yara_string(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.vadyarascan.VadYaraScan", + image, + volatility, + python, + pluginargs=("--pid", "4012", "--yara-string", "MZ"), + ) + assert rc == 0 + assert out.count(b"\n") > 10 + + +class TestWindowsAmcache: + def test_windows_generic_amcache(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.amcache.Amcache", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 100 + # Win10+ expected package names + expected_rows = [ + { + "Path": "C:\\Windows\\SystemApps\\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy", + "ProductName": "Microsoft.Windows.StartMenuExperienceHost", + }, + { + "Path": "C:\\Windows\\SystemApps\\Microsoft.Windows.FileExplorer_cw5n1h2txyewy", + "ProductName": "c5e2524a-ea46-4f67-841f-6a9465d9d515", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsBigPools: + def test_windows_generic_bigpools(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.bigpools.BigPools", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 2000 + expected_rows = [ + { + "PoolType": "PagedPool", + }, + { + "PoolType": "PagedPoolCacheAligned", + }, + { + "PoolType": "NonPagedPoolNx", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +# FIXME: Empty on WIN10 and XP samples +# class TestWindowsCachedump: +# def test_windows_generic_cachedump(self, volatility, python, image): +# rc, out, _err = test_volatility.runvol_plugin( +# "windows.registry.cachedump.Cachedump", +# image, +# volatility, +# python, +# globalargs=("-r", "json"), +# ) +# assert rc == 0 +# json_out = json.loads(out) + + +class TestWindowsCmdLine: + def test_windows_generic_cmdline(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.cmdline.CmdLine", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 20 + out = out.lower() + assert ( + out.find(b"C:\\Windows\\system32\\svchost.exe -k DcomLaunch -p".lower()) + != -1 + ) + assert ( + out.count( + b"C:\\Windows\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p".lower() + ) + > 3 + ) + + +class TestWindowsCmdScan: + def test_windows_specific_cmdscan(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.cmdscan.CmdScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + expected_row = { + "Process": "conhost.exe", + "Property": "_COMMAND_HISTORY", + } + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + +class TestWindowsConsoles: + def test_windows_specific_consoles(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.consoles.Consoles", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + expected_row = { + "Process": "conhost.exe", + "Property": "_CONSOLE_INFORMATION", + } + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + +class TestWindowsCrashinfo: + def test_windows_specific_crashinfo(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.crashinfo.Crashinfo", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + expected_row = { + "BitmapHeaderSize": 176128, + "BitmapPages": 511191, + "BitmapSize": 1310720, + "Comment": "PAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGEPAGE", + "DirectoryTableBase": 4610162688, + "DumpType": "Bitmap Dump (0x5)", + "MachineImageType": 34404, + "MajorVersion": 15, + "MinorVersion": 19041, + "NumberProcessors": 1, + "Signature": "PAGE", + "SystemTime": "2025-03-06T17:59:20+00:00", + "SystemUpTime": "0:11:23.199374", + "__children": [], + } + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + +class TestWindowsDriverIrp: + def test_windows_specific_driverirp(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.driverirp.DriverIrp", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 2000 + assert out.count(b"ntoskrnl") > 400 + for irp in test_volatility.load_test_data( + "windows.driverirp.DriverIrp", "GENERIC" + ): + assert out.find(irp.encode()) != -1 + + +class TestWindowsDriverScan: + def test_windows_specific_driverscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.driverscan.DriverScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 50 + expected_rows = [ + { + "Name": "\\Driver\\ACPI_HAL", + "Service Key": "\\Driver\\ACPI_HAL", + }, + { + "Name": "\\Driver\\Tcpip", + "Service Key": "Tcpip", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsGetServiceSIDs: + def test_windows_generic_getservicesids(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.getservicesids.GetServiceSIDs", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"S-1-5-80-") > 90 + + +class TestWindowsIAT: + def test_windows_generic_iat(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.iat.IAT", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 2000 + expected_rows = [ + { + "Function": "NtTerminateProcess", + "Library": "ntdll.dll", + "Name": "csrss.exe", + }, + { + "Function": "RtlSetHeapInformation", + "Library": "ntdll.dll", + "Name": "csrss.exe", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsInfo: + def test_windows_specific_info(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.info.Info", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.info.Info", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsJobLinks: + def test_windows_specific_joblinks(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.joblinks.JobLinks", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 30 + expected_row = { + "Active": 1, + "JobLink": None, + "JobSess": 2, + "Name": "taskhostw.exe", + "Offset(V)": 145201782567040, + "PID": 4304, + "PPID": 1008, + "Process": "(Original Process)", + "Sess": 2, + "Term": 0, + "Total": 1, + "Wow64": False, + "__children": [ + { + "Active": 0, + "JobLink": "Yes", + "JobSess": 0, + "Name": "taskhostw.exe", + "Offset(V)": 145201782567040, + "PID": 4304, + "PPID": 1008, + "Process": "C:\\Windows\\system32\\taskhostw.exe", + "Sess": 2, + "Term": 0, + "Total": 0, + "Wow64": False, + "__children": [], + } + ], + } + + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsKPCRs: + def test_windows_generic_kpcrs(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.kpcrs.KPCRs", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + assert test_volatility.count_entries_flat(json.loads(out)) > 0 + + +class TestWindowsSymlinkScan: + def test_windows_generic_symlinkscan(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.symlinkscan.SymlinkScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + assert test_volatility.count_entries_flat(json.loads(out)) > 0 + + def test_windows_specific_symlinkscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.symlinkscan.SymlinkScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 5 + expected_rows = [ + { + "CreateTime": "2005-06-25T16:47:28+00:00", + "From Name": "AUX", + "Offset": 453082584, + "To Name": "\\DosDevices\\COM1", + "__children": [], + }, + { + "CreateTime": "2005-06-25T16:47:28+00:00", + "From Name": "UNC", + "Offset": 453176664, + "To Name": "\\Device\\Mup", + "__children": [], + }, + ] + + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsLdrModules: + def test_windows_specific_ldrmodules(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.malware.ldrmodules.LdrModules", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 800 + out = out.lower() + assert out.find(b"\\Windows\\System32\\ntdll.dll".lower()) > 10 + + +class TestWindowsLsadump: + def test_windows_specific_lsadump(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.lsadump.Lsadump", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 5 + expected_row = { + "Hex": "01 00 00 00 2b 2b f1 09 a3 b3 4b af 02 19 5a 61 2f 09 3a 88 03 52 51 64 8a 6c d2 a8 34 07 cb 61 41 ca a4 5d f1 fb 4c e0 41 72 69 32", + "Key": "DPAPI_SYSTEM", + } + + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsMBRScan: + def test_windows_specific_mbrscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mbrscan.MBRScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 4300 + expected_rows = [ + { + "Bootcode MD5": "dbcef88b4d770658b0050bf20b2d3061", + "Disk Signature": "82-78-77-32", + "Full MBR MD5": "8eea93bb1c63863f6e7f95b084411672", + "Potential MBR at Physical Offset": 154029739, + }, + { + "Bootcode MD5": "591213a9dfef595735e419eff6eeb39d", + "Disk Signature": "7a-74-60-53", + "Full MBR MD5": "4e00711a5014941f5ad8b3a4cde69c9c", + "Potential MBR at Physical Offset": 437808348, + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsMemmap: + def test_windows_specific_memmap(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.memmap.Memmap", + image, + volatility, + python, + pluginargs=("--pid", "504"), + ) + assert rc == 0 + assert out.count(b"\n") > 12000 + + +class TestWindowsMFTscan: + def test_windows_specific_mftscan_ads_xp(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mftscan.ADS", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = [ + { + "ADS Filename": "Zone.Identifier", + "Filename": "libby_hoeler_part1.wmv", + "Hexdump": "5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a 5a 6f 6e 65 49 64 3d 33 0d 0a", + "MFT Type": "DATA", + "Offset": 55926304, + "Record Number": 323, + "Record Type": "FILE", + "__children": [], + }, + { + "ADS Filename": "Zone.Identifier", + "Filename": "NetZeroQuickHelpLite.exe", + "Hexdump": "5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a 5a 6f 6e 65 49 64 3d 33 0d 0a", + "MFT Type": "DATA", + "Offset": 56102400, + "Record Number": 347, + "Record Type": "FILE", + "__children": [], + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + def test_windows_specific_mftscan_ads_win10(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mftscan.ADS", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = [ + { + "ADS Filename": "$Max", + "Filename": "$UsnJrnl", + "Hexdump": "00 00 00 02 00 00 00 00 00 00 80 00 00 00 00 00 b9 dd f0 cc df 73 db 01 00 00 00 00 00 00 00 00", + "MFT Type": "DATA", + "Offset": 26235616, + "Record Number": 107240, + "Record Type": "FILE", + "__children": [], + }, + { + "ADS Filename": "$SRAT", + "Filename": "$Bitmap", + "Hexdump": "a4 5f fd 60 38 00 01 03 10 00 0c 00 04 00 00 00 01 00 00 00 01 00 00 00 8d 4e 16 00 02 00 00 00 a0 00 00 00 00 00 06 00 03 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4a 7b 01 00 00 00 00 00", + "MFT Type": "DATA", + "Offset": 1052277088, + "Record Number": 6, + "Record Type": "FILE", + "__children": [], + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + def test_windows_specific_mftscan_mftscan(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mftscan.MFTScan", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 15000 + assert out.count(b"STANDARD_INFORMATION") > 5000 + assert out.count(b"FILE_NAME") > 11000 + + def test_windows_specific_mftscan_residentdata_win10(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mftscan.ResidentData", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 850 + expected_rows = [ + { + "Filename": "index", + "Hexdump": "30 5c 72 a7 1b 6d fb fc 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + "MFT Type": "DATA", + "Offset": 4961536280, + "Record Number": 116474, + "Record Type": "FILE", + }, + { + "Filename": "0.2.filtertrie.intermediate.txt", + "Hexdump": "30 09 32 0d 0a", + "MFT Type": "DATA", + "Offset": 619242944, + "Record Number": 113013, + "Record Type": "FILE", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsModScan: + def test_windows_generic_modscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.modscan.ModScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 90 + expected_rows = [ + { + "Name": "ntoskrnl.exe", + "Offset": 37733296, + "Path": "\\WINDOWS\\system32\\ntoskrnl.exe", + "Size": 2179328, + }, + { + "Name": "hal.dll", + "Offset": 37733192, + "Path": "\\WINDOWS\\system32\\hal.dll", + "Size": 81280, + }, + { + "Name": "netbios.sys", + "Offset": 34566968, + "Path": "\\SystemRoot\\System32\\DRIVERS\\netbios.sys", + "Size": 36864, + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsMutantScan: + def test_windows_specific_mutantscan(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.mutantscan.MutantScan", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 350 + + +class TestWindowsNetScan: + def test_windows_specific_netscan(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.netscan.NetScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 100 + expected_rows = [ + { + "Created": "2025-03-06T17:56:53+00:00", + "ForeignAddr": "13.107.246.254", + "ForeignPort": 443, + "LocalAddr": "10.0.0.4", + "LocalPort": 49929, + "Offset": 145201667934000, + "Owner": "SearchApp.exe", + "PID": 5644, + "Proto": "TCPv4", + "State": "CLOSE_WAIT", + }, + { + "Created": "2025-03-06T17:50:02+00:00", + "ForeignAddr": "168.63.129.16", + "ForeignPort": 80, + "LocalAddr": "10.0.0.4", + "LocalPort": 49689, + "Offset": 145201778694688, + "Owner": "WindowsAzureGu", + "PID": 1944, + "Proto": "TCPv4", + "State": "CLOSED", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsNetStat: + def test_windows_specific_netstat(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.netstat.NetStat", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 70 + expected_rows = [ + { + "Created": "2025-03-06T17:56:53+00:00", + "ForeignAddr": "13.107.246.254", + "ForeignPort": 443, + "LocalAddr": "10.0.0.4", + "LocalPort": 49929, + "Offset": 145201667934000, + "Owner": "SearchApp.exe", + "PID": 5644, + "Proto": "TCPv4", + "State": "CLOSE_WAIT", + }, + { + "Created": "2025-03-06T17:50:02+00:00", + "ForeignAddr": "168.63.129.16", + "ForeignPort": 80, + "LocalAddr": "10.0.0.4", + "LocalPort": 49688, + "Offset": 145201778506032, + "Owner": "WindowsAzureGu", + "PID": 1944, + "Proto": "TCPv4", + "State": "ESTABLISHED", + }, + ] + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsPESymbols: + def test_windows_specific_pe_symbols_processes(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.pe_symbols.PESymbols", + image, + volatility, + python, + globalargs=("-r", "json"), + pluginargs=( + "--source", + "processes", + "--module", + "ntdll.dll", + "--symbol", + "NtProtectVirtualMemory", + ), + ) + assert rc == 0 + expected_row = { + "Address": 2089868982, + "Module": "ntdll.dll", + "Symbol": "NtProtectVirtualMemory", + } + + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + def test_windows_specific_pe_symbols_kernel(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.pe_symbols.PESymbols", + image, + volatility, + python, + globalargs=("-r", "json"), + pluginargs=( + "--source", + "kernel", + "--module", + "ntoskrnl.exe", + "--symbol", + "ZwOpenThread", + ), + ) + assert rc == 0 + expected_row = { + "Address": 2152583356, + "Module": "ntoskrnl.exe", + "Symbol": "ZwOpenThread", + } + + assert test_volatility.match_output_row(expected_row, json.loads(out)) + + +class TestWindowsPoolScanner: + def test_windows_specific_poolscanner(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.poolscanner.PoolScanner", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 4800 + assert out.find(b"_FILE_OBJECT") != -1 + assert out.find(b"_ETHREAD") != -1 + assert out.find(b"_RTL_ATOM_TABLE") != -1 + assert out.find(b"_KMUTANT") != -1 + + +class TestWindowsPsTree: + def test_windows_specific_pstree(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.pstree.PsTree", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 110 + expected_row = test_volatility.load_test_data( + "windows.pstree.PsTree", "WINDOWS10_GENERIC" + ) + + assert test_volatility.match_output_row( + expected_row, json_out, children_recursive=True + ) + + +class TestWindowsRegistry: + def test_windows_specific_registry_certificates(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.certificates.Certificates", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 30 + expected_row = { + "Certificate ID": "ProtectedRoots", + "Certificate path": "Software\\Microsoft\\SystemCertificates", + "Certificate section": "Root", + } + assert test_volatility.match_output_row(expected_row, json_out) + + def test_windows_generic_registry_hivelist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.hivelist.HiveList", image, volatility, python + ) + assert rc == 0 + out = out.lower() + + not_xp = out.find(b"\\systemroot\\system32\\config\\software") + if not_xp == -1: + assert ( + out.find( + b"\\device\\harddiskvolume1\\windows\\system32\\config\\software" + ) + != -1 + ) + assert out.count(b"\n") > 10 + + def test_windows_specific_registry_hivescan(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.hivescan.HiveScan", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.registry.hivescan.HiveScan", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + def test_windows_specific_registry_printkey(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.printkey.PrintKey", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 450 + expected_rows = test_volatility.load_test_data( + "windows.registry.printkey.PrintKey", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + def test_windows_specific_registry_userassist(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.registry.userassist.UserAssist", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 35 + expected_row = test_volatility.load_test_data( + "windows.registry.userassist.UserAssist", "WINDOWS10_GENERIC" + ) + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsSessions: + def test_windows_specific_sessions(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.sessions.Sessions", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 115 + expected_rows = test_volatility.load_test_data( + "windows.sessions.Sessions", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsShimcacheMem: + def test_windows_specific_shimcachemem(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.shimcachemem.ShimcacheMem", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.shimcachemem.ShimcacheMem", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsSSDT: + def test_windows_specific_ssdt(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.ssdt.SSDT", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 770 + assert out.find(b"ntoskrnl") != -1 + assert out.find(b"Nt") != -1 + assert out.find(b"xHal") != -1 + + +class TestWindowsThreads: + def test_windows_specific_threads(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.threads.Threads", + image, + volatility, + python, + ) + assert rc == 0 + assert out.count(b"\n") > 1730 + + +class TestWindowsTimers: + def test_windows_specific_timers(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.timers.Timers", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.timers.Timers", "WINDOWSXP_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsVadInfo: + def test_windows_specific_vadinfo(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.vadinfo.VadInfo", + image, + volatility, + python, + globalargs=("-r", "json"), + pluginargs=("--pid", "4"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.vadinfo.VadInfo", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsVerInfo: + def test_windows_specific_verinfo(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.verinfo.VerInfo", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + assert test_volatility.count_entries_flat(json_out) > 125 + expected_row = { + "Base": 2152558592, + "Build": 2622, + "Major": 5, + "Minor": 1, + "Name": "ntoskrnl.exe", + "Product": 2600, + "__children": [], + } + assert test_volatility.match_output_row(expected_row, json_out) + + +class TestWindowsVirtMap: + def test_windows_specific_virtmap(self, volatility, python): + image = WindowsSamples.WINDOWS10_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "windows.virtmap.VirtMap", + image, + volatility, + python, + globalargs=("-r", "json"), + ) + assert rc == 0 + json_out = json.loads(out) + expected_rows = test_volatility.load_test_data( + "windows.virtmap.VirtMap", "WINDOWS10_GENERIC" + ) + for expected_row in expected_rows: + assert test_volatility.match_output_row(expected_row, json_out) 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/requirements-testing.txt b/test/requirements-testing.txt deleted file mode 100644 index 51c8f602c..000000000 --- a/test/requirements-testing.txt +++ /dev/null @@ -1,11 +0,0 @@ -# These packages are required for core functionality. -pefile>=2017.8.1 #foo - -# The following packages are optional. -# If certain packages are not necessary, place a comment (#) at the start of the line. - -# This is required for the yara plugins -yara-python>=3.8.0 -yara-x>=0.5.0 - -pytest>=7.0.0 diff --git a/test/test_volatility.py b/test/test_volatility.py index 847be88d9..9b7aff4be 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -6,25 +6,31 @@ # import os -import re import subprocess import sys -import shutil import tempfile -import hashlib -import ntpath +import contextlib +import functools import json +import logging +from typing import List, Tuple + +from test import WINDOWS_TESTS_DATA_DIR + +test_logger = logging.getLogger(__name__) + # # HELPER FUNCTIONS # +@functools.lru_cache def runvol(args, volatility, python): volpy = volatility python_cmd = python - cmd = [python_cmd, volpy] + args + cmd = (python_cmd, volpy) + args print(" ".join(cmd)) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() @@ -38,352 +44,203 @@ def runvol(args, volatility, python): return p.returncode, stdout, stderr -def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]): +@functools.lru_cache +def runvol_plugin( + plugin, img, volatility, python, pluginargs: Tuple = (), globalargs: Tuple = () +): args = ( globalargs - + [ + + ( "--single-location", img, "-q", plugin, - ] + ) + pluginargs ) return runvol(args, volatility, python) +def runvolshell( + img, volshell, python, volshellargs: Tuple = (), globalargs: Tuple = () +): + args = ( + globalargs + + ( + "--single-location", + img, + "-q", + ) + + volshellargs + ) + + return runvol(args, volshell, python) + + +def load_test_data(plugin: str, test_key: str): + if plugin.startswith("windows."): + data_path = WINDOWS_TESTS_DATA_DIR / f"{plugin}.json" + # TODO: add Linux and macOS when any of these requires this API + else: + raise Exception(f"Cannot determine OS of plugin: {plugin}") + + if not data_path.exists(): + raise FileNotFoundError( + f"Test data not found for plugin {plugin} at {data_path}" + ) + + with open(data_path) as f: + # This will raise an explicit exception by itself on failures + return json.load(f)[test_key] + + +def dict_lower_strvalues(dict_to_convert: dict): + """Lower each value of type string of a dictionary + + Args: + dict_to_convert: The dictionary in which to lower the string values + Returns: + A copy of the dictionary with lowered string values + """ + + converted = {} + for key, value in dict_to_convert.items(): + if isinstance(value, str): + converted[key] = value.lower() + else: + converted[key] = value + return converted + + +def match_output_row( + expected_row: dict, + plugin_json_out: List[dict], + exact_match: bool = False, + case_sensitive: bool = True, + children_recursive: bool = False, +): + """Search each row in a plugin's JSON output for a matching row. + This method supports recursive comparisons using the "__children" key, making it useful for testing hierarchical plugins like windows.pstree. + It also maintains case sensitivity and exact matching behavior when traversing nested structures. + + Args: + expected_row: The expected row to be found in the output + plugin_json_out: The plugin's output in JSON format (typically obtained through -r json and json.loads) + exact_match: Require exactly the expected row, no more no less, or anticipate columns' addition by checking only + the expected row keys and values + case_sensitive: Operate case sensitive match for str values of both dictionaries or not + children_recursive: Perform a recursive match by inspecting "__children" keys of each expected_row + + Returns: + A boolean indicating whether a match was found or not + """ + + # Lower each string value of both dicts + if not case_sensitive: + expected_row = dict_lower_strvalues(expected_row) + plugin_json_out_tmp = [] + for row in plugin_json_out: + plugin_json_out_tmp.append(dict_lower_strvalues(row)) + plugin_json_out = plugin_json_out_tmp + + if not exact_match: + for row in plugin_json_out: + if all( + expected_item in row.items() + for expected_item in expected_row.items() + if not expected_item[0] == "__children" + ): + if ( + children_recursive + and "__children" in expected_row + and "__children" in row + ): + for children_expected_row in expected_row["__children"]: + if not match_output_row( + children_expected_row, + row["__children"], + case_sensitive=case_sensitive, + children_recursive=True, + ): + break + else: + # We matched all the children keys + return True + else: + # No recursion required and we already matched the row + return True + else: + # No "__children" recursion here as we want to match the whole tree at once + for row in plugin_json_out: + if expected_row == row: + return True + + return False + + +def count_entries_flat(plugin_json_out: List[dict]): + """Count the number of entries as if -r json wasn't specified. Allows to get a non-hierarchical count, without running a plugin twice + (once with "-r json" and once without) while still preserving JSON features. + + Args: + plugin_json_out: The plugin's output in JSON format (typically obtained through -r json and json.loads) + """ + # Remove whitespaces between entries + # If a value contains {", it will be represented by {\" so no confusion + return json.dumps(plugin_json_out, separators=(",", ":")).count('{"') + + # # TESTS # -# WINDOWS +def basic_volshell_test( + image, volatility, python, volshellargs: Tuple = (), globalargs: Tuple = () +): + # Basic VolShell test to verify requirements and ensure VolShell runs without crashing -def test_windows_pslist(image, volatility, python): - rc, out, err = runvol_plugin("windows.pslist.PsList", image, volatility, python) - out = out.lower() - assert out.find(b"system") != -1 - assert out.find(b"csrss.exe") != -1 - assert out.find(b"svchost.exe") != -1 - assert out.count(b"\n") > 10 - assert rc == 0 - - rc, out, err = runvol_plugin( - "windows.pslist.PsList", image, volatility, python, pluginargs=["--pid", "4"] - ) - out = out.lower() - assert out.find(b"system") != -1 - assert out.count(b"\n") < 10 - assert rc == 0 - - -def test_windows_psscan(image, volatility, python): - rc, out, err = runvol_plugin("windows.psscan.PsScan", image, volatility, python) - out = out.lower() - assert out.find(b"system") != -1 - assert out.find(b"csrss.exe") != -1 - assert out.find(b"svchost.exe") != -1 - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_windows_dlllist(image, volatility, python): - rc, out, err = runvol_plugin("windows.dlllist.DllList", image, volatility, python) - out = out.lower() - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_windows_modules(image, volatility, python): - rc, out, err = runvol_plugin("windows.modules.Modules", image, volatility, python) - out = out.lower() - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_windows_hivelist(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.registry.hivelist.HiveList", image, volatility, python - ) - out = out.lower() - - not_xp = out.find(b"\\systemroot\\system32\\config\\software") - if not_xp == -1: - assert ( - out.find(b"\\device\\harddiskvolume1\\windows\\system32\\config\\software") - != -1 - ) - - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_windows_dumpfiles(image, volatility, python): - - with open("./test/known_files.json") as json_file: - known_files = json.load(json_file) - - failed_chksms = 0 - - if sys.platform == "win32": - file_name = ntpath.basename(image) - else: - file_name = os.path.basename(image) + volshell_commands = [ + "print(ps())", + "exit()", + ] + # FIXME: When the minimum Python version includes 3.12, replace the following with: + # with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ... + fd, filename = tempfile.mkstemp(suffix=".txt") try: - for addr in known_files["windows_dumpfiles"][file_name]: + volshell_script = "\n".join(volshell_commands) + with os.fdopen(fd, "w") as f: + f.write(volshell_script) - path = tempfile.mkdtemp() - - rc, out, err = runvol_plugin( - "windows.dumpfiles.DumpFiles", - image, - volatility, - python, - globalargs=["-o", path], - pluginargs=["--virtaddr", addr], - ) - - for file in os.listdir(path): - with open(os.path.join(path, file), "rb") as fp: - if ( - hashlib.md5(fp.read()).hexdigest() - not in known_files["windows_dumpfiles"][file_name][addr] - ): - failed_chksms += 1 - - shutil.rmtree(path) - - json_file.close() - - assert failed_chksms == 0 - assert rc == 0 - except Exception as e: - json_file.close() - print("Key Error raised on " + str(e)) - assert False - - -def test_windows_handles(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.handles.Handles", image, volatility, python, pluginargs=["--pid", "4"] - ) - - assert out.find(b"System Pid 4") != -1 - assert ( - out.find( - b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS" + rc, out, _err = runvolshell( + img=image, + volshell=volatility, + python=python, + volshellargs=("--script", filename) + volshellargs, + globalargs=globalargs, ) - != -1 - ) - assert out.find(b"MACHINE\\SYSTEM\\SETUP") != -1 - assert out.count(b"\n") > 500 + finally: + with contextlib.suppress(FileNotFoundError): + os.remove(filename) + assert rc == 0 + assert out.count(b"\n") >= 4 - -def test_windows_svcscan(image, volatility, python): - rc, out, err = runvol_plugin("windows.svcscan.SvcScan", image, volatility, python) - - assert out.find(b"Microsoft ACPI Driver") != -1 - assert out.count(b"\n") > 250 - assert rc == 0 - - -def test_windows_thrdscan(image, volatility, python): - rc, out, err = runvol_plugin("windows.thrdscan.ThrdScan", image, volatility, python) - # find pid 4 (of system process) which starts with lowest tids - assert out.find(b"\t4\t8") != -1 - assert out.find(b"\t4\t12") != -1 - assert out.find(b"\t4\t16") != -1 - #assert out.find(b"this raieses AssertionError") != -1 - assert rc == 0 - - -def test_windows_privileges(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"] - ) - - assert out.find(b"SeCreateTokenPrivilege") != -1 - assert out.find(b"SeCreateGlobalPrivilege") != -1 - assert out.find(b"SeAssignPrimaryTokenPrivilege") != -1 - assert out.count(b"\n") > 20 - assert rc == 0 - - -def test_windows_getsids(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.getsids.GetSIDs", image, volatility, python, pluginargs=["--pid", "4"] - ) - - assert out.find(b"Local System") != -1 - assert out.find(b"Administrators") != -1 - assert out.find(b"Everyone") != -1 - assert out.find(b"Authenticated Users") != -1 - assert rc == 0 - - -def test_windows_envars(image, volatility, python): - rc, out, err = runvol_plugin("windows.envars.Envars", image, volatility, python) - - assert out.find(b"PATH") != -1 - assert out.find(b"PROCESSOR_ARCHITECTURE") != -1 - assert out.find(b"USERNAME") != -1 - assert out.find(b"SystemRoot") != -1 - assert out.find(b"CommonProgramFiles") != -1 - assert out.count(b"\n") > 500 - assert rc == 0 - - -def test_windows_callbacks(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.callbacks.Callbacks", image, volatility, python - ) - - assert out.find(b"PspCreateProcessNotifyRoutine") != -1 - assert out.find(b"KeBugCheckCallbackListHead") != -1 - assert out.find(b"KeBugCheckReasonCallbackListHead") != -1 - assert out.count(b"KeBugCheckReasonCallbackListHead ") > 5 - assert rc == 0 - - -def test_windows_vadwalk(image, volatility, python): - rc, out, err = runvol_plugin("windows.vadwalk.VadWalk", image, volatility, python) - - assert out.find(b"Vad") != -1 - assert out.find(b"VadS") != -1 - assert out.find(b"Vadl") != -1 - assert out.find(b"VadF") != -1 - assert out.find(b"0x0") != -1 - assert rc == 0 - - -def test_windows_devicetree(image, volatility, python): - rc, out, err = runvol_plugin( - "windows.devicetree.DeviceTree", image, volatility, python - ) - - assert out.find(b"DEV") != -1 - assert out.find(b"DRV") != -1 - assert out.find(b"ATT") != -1 - assert out.find(b"FILE_DEVICE_CONTROLLER") != -1 - assert out.find(b"FILE_DEVICE_DISK") != -1 - assert out.find(b"FILE_DEVICE_DISK_FILE_SYSTEM") != -1 - assert rc == 0 - - -# LINUX - - -def test_linux_pslist(image, volatility, python): - rc, out, err = runvol_plugin("linux.pslist.PsList", image, volatility, python) - out = out.lower() - - assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1) - assert out.find(b"watchdog") != -1 - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_linux_check_idt(image, volatility, python): - rc, out, err = runvol_plugin("linux.check_idt.Check_idt", image, volatility, python) - out = out.lower() - - assert out.count(b"__kernel__") >= 10 - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_linux_check_syscall(image, volatility, python): - rc, out, err = runvol_plugin( - "linux.check_syscall.Check_syscall", image, volatility, python - ) - out = out.lower() - - assert out.find(b"sys_close") != -1 - assert out.find(b"sys_open") != -1 - assert out.count(b"\n") > 100 - assert rc == 0 - - -def test_linux_lsmod(image, volatility, python): - rc, out, err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python) - out = out.lower() - - assert out.count(b"\n") > 10 - assert rc == 0 - - -def test_linux_lsof(image, volatility, python): - rc, out, err = runvol_plugin("linux.lsof.Lsof", image, volatility, python) - out = out.lower() - - assert out.count(b"socket:") >= 10 - assert out.count(b"\n") > 35 - assert rc == 0 - - -def test_linux_proc_maps(image, volatility, python): - rc, out, err = runvol_plugin("linux.proc.Maps", image, volatility, python) - out = out.lower() - - assert out.count(b"anonymous mapping") >= 10 - assert out.count(b"\n") > 100 - assert rc == 0 - - -def test_linux_tty_check(image, volatility, python): - rc, out, err = runvol_plugin("linux.tty_check.tty_check", image, volatility, python) - out = out.lower() - - assert out.find(b"__kernel__") != -1 - assert out.count(b"\n") >= 5 - assert rc == 0 - -def test_linux_sockstat(image, volatility, python): - rc, out, err = runvol_plugin("linux.sockstat.Sockstat", image, volatility, python) - - assert out.count(b"AF_UNIX") >= 354 - assert out.count(b"AF_BLUETOOTH") >= 5 - assert out.count(b"AF_INET") >= 32 - assert out.count(b"AF_INET6") >= 20 - assert out.count(b"AF_PACKET") >= 1 - assert out.count(b"AF_NETLINK") >= 43 - assert rc == 0 - - -def test_linux_library_list(image, volatility, python): - rc, out, err = runvol_plugin( - "linux.library_list.LibraryList", image, volatility, python - ) - - assert re.search( - rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2", - out, - ) - assert re.search( - rb"gnome-settings-\s3807\s0x7f7e660b5000\s/lib/x86_64-linux-gnu/libbz2.so.1.0", - out, - ) - assert re.search( - rb"gdu-notificatio\s3878\s0x7f25ce33e000\s/usr/lib/x86_64-linux-gnu/libXau.so.6", - out, - ) - assert re.search( - rb"bash\s8600\s0x7fe78a85f000\s/lib/x86_64-linux-gnu/libnss_files.so.2", - out, - ) - - assert out.count(b"\n") >= 2677 - assert rc == 0 + return out # MAC +# TODO: Migrate and integrate in testing (once analysis is fixed ?) + + +def test_mac_volshell(image, volatility, python): + basic_volshell_test(image, volatility, python, globalargs=["-m"]) def test_mac_pslist(image, volatility, python): - rc, out, err = runvol_plugin("mac.pslist.PsList", image, volatility, python) + rc, out, _err = runvol_plugin("mac.pslist.PsList", image, volatility, python) out = out.lower() assert (out.find(b"kernel_task") != -1) or (out.find(b"launchd") != -1) @@ -392,7 +249,7 @@ def test_mac_pslist(image, volatility, python): def test_mac_check_syscall(image, volatility, python): - rc, out, err = runvol_plugin( + rc, out, _err = runvol_plugin( "mac.check_syscall.Check_syscall", image, volatility, python ) out = out.lower() @@ -405,7 +262,7 @@ def test_mac_check_syscall(image, volatility, python): def test_mac_check_sysctl(image, volatility, python): - rc, out, err = runvol_plugin( + rc, out, _err = runvol_plugin( "mac.check_sysctl.Check_sysctl", image, volatility, python ) out = out.lower() @@ -416,7 +273,7 @@ def test_mac_check_sysctl(image, volatility, python): def test_mac_check_trap_table(image, volatility, python): - rc, out, err = runvol_plugin( + rc, out, _err = runvol_plugin( "mac.check_trap_table.Check_trap_table", image, volatility, python ) out = out.lower() @@ -427,7 +284,7 @@ def test_mac_check_trap_table(image, volatility, python): def test_mac_ifconfig(image, volatility, python): - rc, out, err = runvol_plugin("mac.ifconfig.Ifconfig", image, volatility, python) + rc, out, _err = runvol_plugin("mac.ifconfig.Ifconfig", image, volatility, python) out = out.lower() assert out.find(b"127.0.0.1") != -1 @@ -437,7 +294,7 @@ def test_mac_ifconfig(image, volatility, python): def test_mac_lsmod(image, volatility, python): - rc, out, err = runvol_plugin("mac.lsmod.Lsmod", image, volatility, python) + rc, out, _err = runvol_plugin("mac.lsmod.Lsmod", image, volatility, python) out = out.lower() assert out.find(b"com.apple") != -1 @@ -446,7 +303,7 @@ def test_mac_lsmod(image, volatility, python): def test_mac_lsof(image, volatility, python): - rc, out, err = runvol_plugin("mac.lsof.Lsof", image, volatility, python) + rc, out, _err = runvol_plugin("mac.lsof.Lsof", image, volatility, python) out = out.lower() assert out.count(b"\n") > 50 @@ -454,7 +311,7 @@ def test_mac_lsof(image, volatility, python): def test_mac_malfind(image, volatility, python): - rc, out, err = runvol_plugin("mac.malfind.Malfind", image, volatility, python) + rc, out, _err = runvol_plugin("mac.malfind.Malfind", image, volatility, python) out = out.lower() assert out.count(b"\n") > 20 @@ -462,7 +319,7 @@ def test_mac_malfind(image, volatility, python): def test_mac_mount(image, volatility, python): - rc, out, err = runvol_plugin("mac.mount.Mount", image, volatility, python) + rc, out, _err = runvol_plugin("mac.mount.Mount", image, volatility, python) out = out.lower() assert out.find(b"/dev") != -1 @@ -471,7 +328,7 @@ def test_mac_mount(image, volatility, python): def test_mac_netstat(image, volatility, python): - rc, out, err = runvol_plugin("mac.netstat.Netstat", image, volatility, python) + rc, out, _err = runvol_plugin("mac.netstat.Netstat", image, volatility, python) assert out.find(b"TCP") != -1 assert out.find(b"UDP") != -1 @@ -481,7 +338,7 @@ def test_mac_netstat(image, volatility, python): def test_mac_proc_maps(image, volatility, python): - rc, out, err = runvol_plugin("mac.proc_maps.Maps", image, volatility, python) + rc, out, _err = runvol_plugin("mac.proc_maps.Maps", image, volatility, python) out = out.lower() assert out.find(b"[heap]") != -1 @@ -490,7 +347,7 @@ def test_mac_proc_maps(image, volatility, python): def test_mac_psaux(image, volatility, python): - rc, out, err = runvol_plugin("mac.psaux.Psaux", image, volatility, python) + rc, out, _err = runvol_plugin("mac.psaux.Psaux", image, volatility, python) out = out.lower() assert out.find(b"executable_path") != -1 @@ -499,7 +356,7 @@ def test_mac_psaux(image, volatility, python): def test_mac_socket_filters(image, volatility, python): - rc, out, err = runvol_plugin( + rc, out, _err = runvol_plugin( "mac.socket_filters.Socket_filters", image, volatility, python ) out = out.lower() @@ -509,7 +366,7 @@ def test_mac_socket_filters(image, volatility, python): def test_mac_timers(image, volatility, python): - rc, out, err = runvol_plugin("mac.timers.Timers", image, volatility, python) + rc, out, _err = runvol_plugin("mac.timers.Timers", image, volatility, python) out = out.lower() assert out.count(b"\n") > 6 @@ -517,7 +374,9 @@ def test_mac_timers(image, volatility, python): def test_mac_trustedbsd(image, volatility, python): - rc, out, err = runvol_plugin("mac.trustedbsd.Trustedbsd", image, volatility, python) + rc, out, _err = runvol_plugin( + "mac.trustedbsd.Trustedbsd", image, volatility, python + ) out = out.lower() assert out.count(b"\n") > 10 diff --git a/test/volatility3_code_analysis.py b/test/volatility3_code_analysis.py new file mode 100644 index 000000000..43fed459b --- /dev/null +++ b/test/volatility3_code_analysis.py @@ -0,0 +1,440 @@ +""" +This script performs syntax analysis on the volatility3 source tree through a combination of AST analysis and import-time introspection of classes. + +The current checks it implements are: + 1. Ensure that classes derived from `ConfigurableInterface` properly + declare all `VersionableInterface` classes that they make use of in their + `get_requirements()` classmethod. + + :WARNING: a notable exception to this are classes defined within factory + functions. Because these classes are not created until the factory function + is called, they therefore do no exist at import time and cannot be checked + by this script. It is important to keep in mind during code review that + this is a best-effort check and does not make guarantees about the + completeness of declared requirements. +""" + +import abc +import argparse +import ast +import importlib +import inspect +import logging +import pkgutil +import sys +import traceback +import types +from typing import Any, Iterator, List, Optional, Tuple, Type, Union + +from volatility3.framework import configuration, interfaces +from volatility3.framework.deprecation import PluginRenameClass + +logging.basicConfig(format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + + +class NodeVisitor: + def visit(self, node): + """Visit a node.""" + method = "visit_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + self.enter(node) + result = visitor(node) + self.leave(node) + return result + + def enter(self, node): + """Called when entering a node.""" + method = "enter_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_enter) + return visitor(node) + + def leave(self, node): + """Called when leaving a node.""" + method = "leave_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_leave) + return visitor(node) + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, ast.AST): + self.visit(item) + elif isinstance(value, ast.AST): + self.visit(value) + + def generic_enter(self, node): + """Default enter behavior.""" + + def generic_leave(self, node): + """Default leave behavior.""" + + +class CodeViolation(metaclass=abc.ABCMeta): + def __init__(self, module: types.ModuleType, node: ast.AST) -> None: + self.module = module + self.node = node + + def __str__(self): + return f"Issue in module {self.module.__name__}: line {self.node.lineno}, col {self.node.col_offset}" + + +class UnrequiredVersionableUsage(CodeViolation): + def __init__( + self, + module: types.ModuleType, + node: ast.AST, + consuming_class: str, + versionable_item_class: str, + ) -> None: + super().__init__(module, node) + self.consuming_class = consuming_class + self.versionable_item_class = versionable_item_class + + def __str__(self) -> str: + return ( + super().__str__() + + ": " + + ( + f"Found usage of {self.versionable_item_class} " + f"in class {self.consuming_class} that is not declared " + f"in {self.consuming_class}'s `get_requirements()` classmethod" + ) + ) + + +class DirectVolatilityImportUsage(CodeViolation): + def __init__( + self, + module: types.ModuleType, + node: ast.AST, + importing_module: str, + imported_item: object, + imported_name: str, + ) -> None: + self.imported_item = imported_item + self.imported_name = imported_name + self.importing_module = importing_module + super().__init__(module, node) + + def __str__(self) -> str: + components = self.importing_module.split(".") + return ( + super().__str__() + + ": " + + ( + f"Direct import of {self.imported_name} " + f"({type(self.imported_item)}) " + f"from module {self.importing_module} - " + "change to " + f"'from {'.'.join(components[:-1])} import {components[-1]} and using {components[-1]}.{self.imported_name}" + ) + ) + + +def is_versionable(var): + try: + return ( + issubclass(var, interfaces.configuration.VersionableInterface) + and var is not interfaces.configuration.VersionableInterface + and not inspect.isabstract(var) + and not (hasattr(var, "hidden") and getattr(var, "hidden") is True) + ) + except TypeError: + return False + + +def is_configurable(var): + try: + return issubclass(var, interfaces.configuration.ConfigurableInterface) + except TypeError: + return False + + +class ModuleVisitor(NodeVisitor): + def __init__(self, module: types.ModuleType) -> None: + self._module = module + self._scopes = [] + self._violations = [] + + @property + def violations(self): + return self._violations + + def _check_vol3_import_from(self, node: ast.ImportFrom): + """ + Ensure that the only thing imported from a volatility3 module (apart + from the root volatility3 module) are functions and modules. This + prevents re-exporting of classes and variables from modules that use + them. + """ + 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 + ): + for name in node.names: + try: + item = vars(self._module)[ + name.asname if name.asname is not None else name.name + ] + except KeyError: + logger.debug( + "Couldn't find imported name %s in module %s", + name.asname or name.name, + self._module.__name__, + ) + continue + + if not (isinstance(item, types.ModuleType) or inspect.isfunction(item)): + self._violations.append( + DirectVolatilityImportUsage( + self._module, + node, + node.module, + item, + name.asname or name.name, + ) + ) + + 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 + try: + clazz = vars(self._module)[str(node.name)] + except KeyError: + logger.debug( + "Failed to get %s from module scope: (%s)", + node.name, + self._module.__name__, + ) + if self._scopes: + try: + logger.debug( + "Attempting to get class %s from scope of %s", + node.name, + self._scopes[-1].__name__, + ) + clazz = getattr(self._scopes[-1], node.name) + except AttributeError: + logger.debug( + "Class not found in scope of %s", self._scopes[-1].__name__ + ) + if clazz: + self._scopes.append(clazz) + + if clazz and is_configurable(clazz): + logger.info("Checking configurable class %s", clazz.__name__) + visitor = ConfigurableClassVisitor(self._module, clazz) + visitor.visit(node) + self._violations += visitor.violations + + self.generic_visit(node) + + def leave_ClassDef(self, node: ast.ClassDef): + logger.debug("Leaving class %s", node.name) + try: + scoped_class = next( + scope for scope in self._scopes if scope.__name__ == node.name + ) + self._scopes.remove(scoped_class) + except StopIteration: + logger.debug("%s not found in scope list", node.name) + + +class ConfigurableClassVisitor(NodeVisitor): + def __init__( + self, + module: types.ModuleType, + clazz: Optional[Type[interfaces.configuration.ConfigurableInterface]], + ) -> None: + self._module = module + self._current_object = None + self._clazz = clazz + self._seen = set() + self._violations: List[CodeViolation] = [] + + @property + def versioned_classes(self): + return ( + [ + req._component + for req in self._clazz.get_requirements() + if isinstance(req, configuration.requirements.VersionRequirement) + ] + if self._clazz is not None + else [] + ) + + def check_item(self, item: Type, node: Union[ast.Name, ast.Attribute]): + if ( + is_versionable(item) + and self._clazz is not None + and item not in self.versioned_classes + and item is not self._clazz + and not issubclass(self._clazz, PluginRenameClass) + ): + logger.info( + "Found versionable item %s, checking against %s", + str(item), + str(self.versioned_classes), + ) + result = UnrequiredVersionableUsage( + self._module, node, self._clazz.__name__, item.__name__ + ) + self._violations.append(result) + + @property + def violations(self): + return self._violations + + def visit_Name(self, node: ast.Name): + try: + logger.debug( + "Checking module %s for name %s", self._module.__name__, node.id + ) + item = vars(self._module)[str(node.id)] + logger.debug("Found %s in %s namespace", node.id, self._module.__name__) + except KeyError: + return + + self.check_item(item, node) + + def visit_Attribute( + self, node: ast.Attribute + ) -> Optional[UnrequiredVersionableUsage]: + if self._clazz is None: + self.generic_visit(node) + return + + if (node.lineno, node.col_offset) in self._seen: + return + + self._seen.add((node.lineno, node.col_offset)) + + stack = [] + root = node + while True: + stack.append(node.attr) + if isinstance(node.value, ast.Attribute): + node = node.value + elif isinstance(node.value, ast.Name): + stack.append(node.value.id) + break + else: + break + + current = None + logger.debug("Checking %s", ".".join(stack[::-1])) + for item in stack[::-1]: + try: + current = ( + vars(self._module)[item] + if current is None + else getattr(current, item) + ) + except (KeyError, AttributeError) as exc: + logger.debug( + "Failed to get attribute %s (%s)%s", + item, + exc.__class__.__name__, + (" on" + str(current)) if current is not None else "", + ) + break + + self.check_item(current, root) + + +def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUsage]]: + vol3 = importlib.import_module("volatility3") + + for _, module_name, _ in pkgutil.walk_packages( + vol3.__path__, vol3.__name__ + ".", onerror=lambda _: None + ): + modname = module_name.replace( + "volatility3.framework.plugins", "volatility3.plugins" + ) + try: + # import the module that we want to check + plugin_module = importlib.import_module(modname) + + except ImportError as exc: + logger.warning("Failed to import %s: %s", modname, str(exc)) + continue + except Exception as exc: + logger.warning( + "An unexpected exception occurred while importing %s: %s", + modname, + str(exc), + ) + traceback.print_exc() + continue + + logger.info("Checking module %s", plugin_module.__name__) + if plugin_module.__file__ is None: + logger.warning("Plugin module %s has no source file", modname) + continue + + try: + with open(plugin_module.__file__, "rb") as f: + source = f.read() + except OSError: + logger.warning( + "Failed to read file contents for %s", plugin_module.__file__ + ) + continue + + try: + module_ast_root = ast.parse(source) + except (SyntaxError, ValueError) as exc: + logger.warning( + "Failed to parse source for %s: %s", plugin_module.__file__, str(exc) + ) + raise + + mod_visitor = ModuleVisitor(plugin_module) + mod_visitor.visit(module_ast_root) + + if mod_visitor.violations: + yield from ( + (plugin_module.__name__, res) for res in iter(mod_visitor.violations) + ) + + +def perform_review(): + found = 0 + for mod, usage in report_missing_requirements(): + found += 1 + print(str(usage)) + + if found: + print(f"Found {found} issues") + sys.exit(1) + + print("All configurable classes passed validation!") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", action="count", dest="verbosity", default=0) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + if args.verbosity == 0: + logger.setLevel(logging.WARNING) + elif args.verbosity == 1: + logger.setLevel(logging.INFO) + elif args.verbosity > 1: + logger.setLevel(logging.DEBUG) + + perform_review() 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 75b62abf6..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 @@ -19,7 +20,7 @@ import os import sys import tempfile import traceback -from typing import Any, Dict, List, Tuple, Type, Union +from typing import Any, Dict, List, Optional, Tuple, Type, Union from urllib import parse, request try: @@ -57,14 +58,14 @@ formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s") console.setFormatter(formatter) -class PrintedProgress(object): +class PrintedProgress: """A progress handler that prints the progress value and the description onto the command line.""" def __init__(self): self._max_message_len = 0 - def __call__(self, progress: Union[int, float], description: str = None): + def __call__(self, progress: Union[int, float], description: Optional[str] = None): """A simple function for providing text-based feedback. .. warning:: Only for development use. @@ -81,14 +82,14 @@ class PrintedProgress(object): class MuteProgress(PrintedProgress): """A dummy progress handler that produces no output when called.""" - def __call__(self, progress: Union[int, float], description: str = None): + def __call__(self, progress: Union[int, float], description: Optional[str] = None): pass class CommandLine: """Constructs a command-line interface object for users to run plugins.""" - CLI_NAME = "volatility" + CLI_NAME = os.path.basename(sys.argv[0]) # vol or volatility def __init__(self): self.setup_logging() @@ -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") @@ -126,9 +120,7 @@ class CommandLine: "--help", action="help", default=argparse.SUPPRESS, - help="Show this help message and exit, for specific plugin options use '{} --help'".format( - parser.prog - ), + help=f"Show this help message and exit, for specific plugin options use '{parser.prog} --help'", ) parser.add_argument( "-c", @@ -195,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", @@ -272,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) @@ -348,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: @@ -360,16 +357,26 @@ class CommandLine: subparser = parser.add_subparsers( title="Plugins", dest="plugin", - description="For plugin specific options, run '{} --help'".format( - self.CLI_NAME - ), + description=f"For plugin specific options, run '{self.CLI_NAME} --help'", action=volargparse.HelpfulSubparserAction, + metavar="PLUGIN", ) for plugin in sorted(plugin_list): + # First line of a plugin docstring will be the short description for -h. + # Text after the first two consecutive new lines will be + # the additional description (argparse epilog). + short_help = additional_help = None + if plugin_list[plugin].__doc__ is not None: + doc_split = plugin_list[plugin].__doc__.split("\n\n", 1) + short_help = doc_split[0].strip() + if len(doc_split) > 1: + additional_help = doc_split[1].strip() + plugin_parser = subparser.add_parser( plugin, - help=plugin_list[plugin].__doc__, - description=plugin_list[plugin].__doc__, + help=short_help, + description=short_help, + epilog=additional_help, ) self.populate_requirements_argparse(plugin_parser, plugin_list[plugin]) @@ -384,8 +391,17 @@ 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("Please select a plugin to run") + parser.error( + f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options" + ) vollog.log( constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}" @@ -413,7 +429,7 @@ class CommandLine: # UI fills in the config, here we load it from the config file and do it before we process the CL parameters if args.config: - with open(args.config, "r") as f: + with open(args.config) as f: json_val = json.load(f) ctx.config.splice( plugin_config_path, @@ -443,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 @@ -495,6 +512,9 @@ class CommandLine: try: # Construct and run the plugin if constructed: + vollog.debug( + f"Successfully constructed {args.plugin} {constructed.version}" + ) grid = constructed.run() renderer = renderers[args.renderer]() renderer.filter = text_filter.CLIFilter(grid, args.filters) @@ -556,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 @@ -573,6 +593,8 @@ class CommandLine: fulltrace = traceback.TracebackException.from_exception(excp).format(chain=True) vollog.debug("".join(fulltrace)) + file_a_bug_msg = f"Please re-run with -vvv and file a bug with the output at {constants.BUG_URL}" + if isinstance(excp, exceptions.InvalidAddressException): general = "Volatility was unable to read a requested page:" if isinstance(excp, exceptions.SwappedInvalidAddressException): @@ -617,9 +639,7 @@ class CommandLine: elif isinstance(excp, exceptions.LayerException): general = f"Volatility experienced a layer-related issue: {excp.layer_name}" detail = f"{excp}" - caused_by = [ - "A faulty layer implementation (re-run with -vvv and file a bug)" - ] + caused_by = [f"A faulty layer implementation. {file_a_bug_msg}"] elif isinstance(excp, exceptions.MissingModuleException): general = f"Volatility could not import a necessary module: {excp.module}" detail = f"{excp}" @@ -630,13 +650,17 @@ class CommandLine: general = "Volatility experienced an issue when rendering the output:" detail = f"{excp}" caused_by = ["An invalid renderer option, such as no visible columns"] + elif isinstance(excp, exceptions.VersionMismatchException): + general = "A version mismatch was detected between two components:" + detail = f"{excp}" + caused_by = [ + excp.failure_reason or "An outdated API caller, such as a method.", + file_a_bug_msg, + ] else: general = "Volatility encountered an unexpected situation." detail = "" - caused_by = [ - "Please re-run using with -vvv and file a bug with the output", - f"at {constants.BUG_URL}", - ] + caused_by = [file_a_bug_msg] # Code that actually renders the exception output = sys.stderr @@ -719,9 +743,7 @@ class CommandLine: if isinstance(requirement, requirements.ListRequirement): if not isinstance(value, list): raise TypeError( - "Configuration for ListRequirement was not a list: {}".format( - requirement.name - ) + f"Configuration for ListRequirement was not a list: {requirement.name}" ) value = [requirement.element_type(x) for x in value] if not inspect.isclass(configurables_list[configurable]): @@ -743,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 @@ -794,7 +816,7 @@ class CommandLine: fd, self._name = tempfile.mkstemp( suffix=".vol3", prefix="tmp_", dir=output_dir ) - self._file = io.open(fd, mode="w+b") + self._file = open(fd, mode="w+b") CLIFileHandler.__init__(self, filename) for item in dir(self._file): if not item.startswith("_") and item not in ( @@ -867,9 +889,7 @@ class CommandLine: requirement, interfaces.configuration.RequirementInterface ): raise TypeError( - "Plugin contains requirements that are not RequirementInterfaces: {}".format( - configurable.__name__ - ) + f"Plugin contains requirements that are not RequirementInterfaces: {configurable.__name__}" ) if isinstance(requirement, interfaces.configuration.SimpleTypeRequirement): additional["type"] = requirement.instance_type @@ -884,7 +904,7 @@ class CommandLine: volatility3.framework.configuration.requirements.ListRequirement, ): # Allow a list of integers, specified with the convenient 0x hexadecimal format - if requirement.element_type == int: + if requirement.element_type is int: additional["type"] = lambda x: int(x, 0) else: additional["type"] = requirement.element_type diff --git a/volatility3/cli/text_filter.py b/volatility3/cli/text_filter.py index 3d69934e9..b6f019da9 100644 --- a/volatility3/cli/text_filter.py +++ b/volatility3/cli/text_filter.py @@ -1,7 +1,8 @@ import logging -from typing import Any, List, Optional -from volatility3.framework import constants, interfaces import re +from typing import Any, List, Optional + +from volatility3.framework import constants, interfaces vollog = logging.getLogger(__name__) @@ -67,16 +68,16 @@ class ColumnFilter: ) -> None: self.column_num = column_num self.pattern = pattern - self.exclude = exclude self.regex = regex + self.exclude = exclude def find(self, item) -> bool: """Identifies whether an item is found in the appropriate column""" try: if self.regex: - return re.search(self.pattern, f"{item}") + return bool(re.search(self.pattern, f"{item}")) return self.pattern in f"{item}" - except IOError: + except OSError: return False def found(self, row: List[Any]) -> bool: diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 96970d3ce..d00c00bf4 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,9 +9,9 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Tuple -from volatility3.cli import text_filter +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union +from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints @@ -49,10 +49,12 @@ def hex_bytes_as_text(value: bytes, width: int = 16) -> str: output += "\n" printables = "" - # Handle leftovers when the lenght is not mutiple of width + # Handle leftovers when the length is not a multiple of width if printables: - output += " " * (width - len(printables)) + padding = width - len(printables) + output += " " * padding output += printables + output += " " * padding return output @@ -78,7 +80,12 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: return hex_bytes_as_text(value) -def optional(func: Callable) -> Callable: +T = TypeVar("T") + + +def optional( + func: Callable[[Union[interfaces.renderers.BaseAbsentValue, T]], str], +) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -108,7 +115,7 @@ def quoted_optional(func: Callable) -> Callable: return wrapped -def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: +def display_disassembly(disasm: renderers.Disassembly) -> str: """Renders a disassembly renderer type into string format. Args: @@ -130,18 +137,121 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: for i in disasm_types[disasm.architecture].disasm( disasm.data, disasm.offset ): - output += f"\n0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}" + output += f"\n{i.address:#x}:\t{i.mnemonic}\t{i.op_str}" return output return QuickTextRenderer._type_renderers[bytes](disasm.data) +class CLITypeRenderer(interfaces.renderers.TypeRendererInterface): + def __init__(self, func): + super().__init__(func=optional(func)) + + +class LayerDataRenderer(CLITypeRenderer): + """Renders a LayerData object into data/bytes""" + + def __init__(self): + self.context_byte_len = 0 + self.width = 16 + self.display_offset = False + self.display_hex = True + self.display_ascii = True + + def render( + data: Union[renderers.LayerData, interfaces.renderers.BaseAbsentValue], + ) -> str: + if isinstance(data, interfaces.renderers.BaseAbsentValue): + # FIXME: Do something cleverer here + return "" + + specific_data, error_bytes = self.render_bytes(data) + + printables = "" + output = "\n" + for count, byte in enumerate(specific_data): + if count not in error_bytes: + output += f"{byte:02x} " + char = chr(byte) + printables += char if 0x20 <= byte <= 0x7E else "." + else: + output += "__ " + printables += "." + if count % self.width == self.width - 1: + output += printables + if count < len(specific_data) - 1: + output += "\n" + printables = "" + + # Handle leftovers when the length is not a multiple of width + if printables: + padding = self.width - len(printables) + output += " " * padding + output += printables + output += " " * padding + + return output + + render_func = render + return super().__init__(render_func) + + def render_bytes(self, data: renderers.LayerData) -> Tuple[bytes, Set[int]]: + """Renders a valid LayerData into bytes (with context bytes)""" + context_byte_len = self.context_byte_len if not data.no_surrounding else 0 + + layer = data.context.layers[data.layer_name] + # Map of the holes + error_bytes = set() + start_offset = data.offset - context_byte_len + end_offset = data.offset + data.length + context_byte_len + if isinstance(layer, interfaces.layers.TranslationLayerInterface): + error_bytes = set() + mapping = iter(layer.mapping(start_offset, end_offset, True)) + current_map = next(mapping) + for i in range(start_offset, end_offset): + # Run through the bytes, check if they're present + offset, sublength, _, _, _ = current_map + if i < offset: + error_bytes.add(i - start_offset) + if i > offset + sublength: + try: + current_map = next(mapping) + except StopIteration: + pass + offset, sublength, _, _, _ = current_map + if i > offset + sublength: + error_bytes.add(i - start_offset) + + # Padded data + specific_data = data.context.layers[data.layer_name].read( + start_offset, + end_offset - start_offset, + True, + ) + + return specific_data, error_bytes + + class CLIRenderer(interfaces.renderers.Renderer): """Class to add specific requirements for CLI renderers.""" + _type_renderers = { + format_hints.Bin: CLITypeRenderer(lambda x: f"0b{x:b}"), + format_hints.Hex: CLITypeRenderer(lambda x: f"0x{x:x}"), + format_hints.HexBytes: CLITypeRenderer(hex_bytes_as_text), + format_hints.MultiTypeData: CLITypeRenderer(multitypedata_as_text), + renderers.Disassembly: CLITypeRenderer(display_disassembly), + bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)), + renderers.LayerData: LayerDataRenderer(), + datetime.datetime: CLITypeRenderer( + lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z") + ), + "default": CLITypeRenderer(lambda x: f"{x}"), + } + name = "unnamed" structured_output = False - filter: text_filter.CLIFilter = None - column_hide_list: list = None + filter: Optional[text_filter.CLIFilter] = None + column_hide_list: Optional[list] = None def ignored_columns( self, @@ -168,21 +278,10 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } - name = "quick" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -240,7 +339,7 @@ class NoneRenderer(CLIRenderer): name = "none" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: if not grid.populated: @@ -248,22 +347,11 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } - name = "csv" structured_output = True def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each row immediately to stdout. @@ -314,12 +402,10 @@ class CSVRenderer(CLIRenderer): class PrettyTextRenderer(CLIRenderer): - _type_renderers = QuickTextRenderer._type_renderers - name = "pretty" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -340,7 +426,7 @@ class PrettyTextRenderer(CLIRenderer): column_separator = " | " tree_indent_column = "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(20) + random.choices(string.ascii_uppercase + string.digits, k=20) ) max_column_widths = dict( [(column.name, len(column.name)) for column in grid.columns] @@ -378,7 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( + [] + ) if not grid.populated: grid.populate(visitor, final_output) else: @@ -445,10 +533,19 @@ class PrettyTextRenderer(CLIRenderer): class JsonRenderer(CLIRenderer): _type_renderers = { - format_hints.HexBytes: quoted_optional(hex_bytes_as_text), - interfaces.renderers.Disassembly: quoted_optional(display_disassembly), + format_hints.HexBytes: lambda x: ( + x.hex(" ") + if not isinstance(x, interfaces.renderers.BaseAbsentValue) + else "N/A" + ), + renderers.Disassembly: quoted_optional(display_disassembly), format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), - bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])), + renderers.LayerData: lambda x: ( + LayerDataRenderer().render_bytes(x)[0].hex(" ") + if not isinstance(x, interfaces.renderers.BaseAbsentValue) + else "N/A" + ), + bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), datetime.datetime: lambda x: ( x.isoformat() if not isinstance(x, interfaces.renderers.BaseAbsentValue) @@ -461,11 +558,11 @@ class JsonRenderer(CLIRenderer): structured_output = True def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - pass + return [] def output_result(self, outfd, result): """Outputs the JSON data to a file in a particular format""" - outfd.write("{}\n".format(json.dumps(result, indent=2, sort_keys=True))) + outfd.write(f"{json.dumps(result, indent=2, sort_keys=True)}\n") def render(self, grid: interfaces.renderers.TreeGrid): outfd = sys.stdout @@ -501,7 +598,8 @@ class JsonRenderer(CLIRenderer): if self.filter and self.filter.filter(line): return accumulator - if node.parent: + # Only add if the parent hasn't been filtered out + if node.parent and node.parent.path in acc_map: acc_map[node.parent.path]["__children"].append(node_dict) else: final_tree.append(node_dict) diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index fd61ddce0..dce9cafa6 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -5,7 +5,7 @@ import argparse import gettext import re -from typing import List, Optional, Sequence, Any, Union +from typing import Optional, Sequence, Any, Union # This effectively overrides/monkeypatches the core argparse module to provide more helpful output around choices diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 5172c5363..5bc29f220 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -49,7 +49,7 @@ class VolShell(cli.CommandLine): python terminal with all the volatility support calls available. """ - CLI_NAME = "volshell" + CLI_NAME = os.path.basename(sys.argv[0]) # volshell def __init__(self): super().__init__() @@ -282,9 +282,7 @@ class VolShell(cli.CommandLine): for plugin in volshell_plugin_list: subparser = parser.add_argument_group( title=plugin.capitalize(), - description="Configuration options based on {} options".format( - plugin.capitalize() - ), + description=f"Configuration options based on {plugin.capitalize()} options", ) self.populate_requirements_argparse(subparser, volshell_plugin_list[plugin]) configurables_list[plugin] = volshell_plugin_list[plugin] @@ -331,7 +329,7 @@ class VolShell(cli.CommandLine): # UI fills in the config, here we load it from the config file and do it before we process the CL parameters if args.config: - with open(args.config, "r") as f: + with open(args.config) as f: json_val = json.load(f) ctx.config.splice( plugin_config_path, @@ -346,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 82c470e1a..2a8039ff2 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -8,6 +8,7 @@ import random import string import struct import sys +import textwrap from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union from urllib import parse, request @@ -23,12 +24,24 @@ try: except ImportError: has_capstone = False +try: + from IPython import terminal + from traitlets import config as traitlets_config + + has_ipython = True +except ImportError: + has_ipython = False + +MAX_DEREFERENCE_COUNT = 4 # the max number of times display_type should follow pointers + class Volshell(interfaces.plugins.PluginInterface): """Shell environment to directly interact with a memory image.""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + DEFAULT_NUM_DISPLAY_BYTES = 128 def __init__(self, *args, **kwargs): @@ -43,24 +56,36 @@ class Volshell(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - reqs: List[interfaces.configuration.RequirementInterface] = [] + reqs: List[interfaces.configuration.RequirementInterface] = [ + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + ] if cls == Volshell: - reqs = [ + reqs += [ + requirements.TranslationLayerRequirement( + name="primary", description="Memory layer for the kernel" + ), requirements.URIRequirement( name="script", description="File to load and execute at start", default=None, optional=True, - ) + ), + requirements.BooleanRequirement( + name="script-only", + description="Exit volshell after the script specified in --script completes", + default=False, + optional=True, + ), ] - return reqs + [ - requirements.TranslationLayerRequirement( - name="primary", description="Memory layer for the kernel" - ), - ] + + return reqs def run( - self, additional_locals: Dict[str, Any] = {} + self, additional_locals: Dict[str, Any] = None ) -> interfaces.renderers.TreeGrid: """Runs the interactive volshell plugin. @@ -68,44 +93,74 @@ class Volshell(interfaces.plugins.PluginInterface): Return a TreeGrid but this is always empty since the point of this plugin is to run interactively """ - # Try to enable tab completion - try: - import readline - except ImportError: - pass - else: - import rlcompleter + if additional_locals is None: + additional_locals = {} - completer = rlcompleter.Completer(namespace=self._construct_locals_dict()) - readline.set_completer(completer.complete) - readline.parse_and_bind("tab: complete") - print("Readline imported successfully") + # Try to enable tab completion + if not has_ipython: + try: + import readline + import rlcompleter + + completer = rlcompleter.Completer( + namespace=self._construct_locals_dict() + ) + readline.set_completer(completer.complete) + readline.parse_and_bind("tab: complete") + print("Readline imported successfully") + except ImportError: + print( + "Readline or rlcompleter module could not be imported. Tab completion will not be available." + ) # TODO: provide help, consider generic functions (pslist?) and/or providing windows/linux functions mode = self.__module__.split(".")[-1] mode = mode[0].upper() + mode[1:] - banner = f""" - Call help() to see available functions + banner = textwrap.dedent( + f""" + Call help() to see available functions - Volshell mode : {mode} - Current Layer : {self.current_layer} - Current Symbol Table : {self.current_symbol_table} - Current Kernel Name : {self.current_kernel_name} -""" + Volshell mode : {mode} + Current Layer : {self.current_layer} + Current Symbol Table : {self.current_symbol_table} + Current Kernel Name : {self.current_kernel_name} + """ + ) sys.ps1 = f"({self.current_layer}) >>> " # Dict self._construct_locals_dict() will have priority on keys combined_locals = additional_locals.copy() combined_locals.update(self._construct_locals_dict()) - self.__console = code.InteractiveConsole(locals=combined_locals) + if has_ipython: + + class LayerNamePrompt(terminal.prompts.Prompts): + def in_prompt_tokens(self, cli=None): + slf = self.shell.user_ns.get("self") + layer_name = slf.current_layer if slf else "no_layer" + return [(terminal.prompts.Token.Prompt, f"[{layer_name}]> ")] + + c = traitlets_config.Config() + c.TerminalInteractiveShell.prompts_class = LayerNamePrompt + c.InteractiveShellEmbed.banner2 = banner + self.__console = terminal.embed.InteractiveShellEmbed( + config=c, user_ns=combined_locals + ) + else: + self.__console = code.InteractiveConsole(locals=combined_locals) # Since we have to do work to add the option only once for all different modes of volshell, we can't # rely on the default having been set if self.config.get("script", None) is not None: self.run_script(location=self.config["script"]) - self.__console.interact(banner=banner) + if self.config.get("script-only"): + exit() + + if has_ipython: + self.__console() + else: + self.__console.interact(banner=banner) return renderers.TreeGrid([("Terminating", str)], None) @@ -132,6 +187,9 @@ class Volshell(interfaces.plugins.PluginInterface): def construct_locals(self) -> List[Tuple[List[str], Any]]: """Returns a listing of the functions to be added to the environment.""" return [ + (["bc", "breakpoint_clear"], self.breakpoint_clear), + (["bl", "breakpoint_list"], self.breakpoint_list), + (["bp", "breakpoint"], self.breakpoint), (["dt", "display_type"], self.display_type), (["db", "display_bytes"], self.display_bytes), (["dw", "display_words"], self.display_words), @@ -203,7 +261,7 @@ class Volshell(interfaces.plugins.PluginInterface): connector = " " if chunk_size < 2: connector = "" - ascii_data = connector.join([self._ascii_bytes(x) for x in valid_data]) + ascii_data = connector.join(self._ascii_bytes(x) for x in valid_data) print(hex(offset), " ", hex_data, " ", ascii_data) offset += 16 @@ -240,7 +298,7 @@ class Volshell(interfaces.plugins.PluginInterface): return None return self.context.modules[self.current_kernel_name] - def change_layer(self, layer_name: str = None): + def change_layer(self, layer_name: Optional[str] = None): """Changes the current default layer""" if not layer_name: layer_name = self.current_layer @@ -250,7 +308,7 @@ class Volshell(interfaces.plugins.PluginInterface): self.__current_layer = layer_name sys.ps1 = f"({self.current_layer}) >>> " - def change_symbol_table(self, symbol_table_name: str = None): + def change_symbol_table(self, symbol_table_name: Optional[str] = None): """Changes the current_symbol_table""" if not symbol_table_name: print("No symbol table provided, not changing current symbol table") @@ -262,7 +320,7 @@ class Volshell(interfaces.plugins.PluginInterface): self.__current_symbol_table = symbol_table_name print(f"Current Symbol Table: {self.current_symbol_table}") - def change_kernel(self, kernel_name: str = None): + def change_kernel(self, kernel_name: Optional[str] = None): if not kernel_name: print("No kernel module name provided, not changing current kernel") if kernel_name not in self.context.modules: @@ -277,23 +335,25 @@ class Volshell(interfaces.plugins.PluginInterface): self._display_data(offset, remaining_data) def display_quadwords( - self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None + self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None, byteorder="@" ): """Displays quad-word values (8 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) - self._display_data(offset, remaining_data, format_string="Q") + self._display_data(offset, remaining_data, format_string=f"{byteorder}Q") def display_doublewords( - self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None + self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None, byteorder="@" ): """Displays double-word values (4 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) - self._display_data(offset, remaining_data, format_string="I") + self._display_data(offset, remaining_data, format_string=f"{byteorder}I") - def display_words(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None): + def display_words( + self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None, byteorder="@" + ): """Displays word values (2 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) - self._display_data(offset, remaining_data, format_string="H") + self._display_data(offset, remaining_data, format_string=f"{byteorder}H") def regex_scan(self, pattern, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None): """Scans for regex pattern in layer using RegExScanner.""" @@ -342,14 +402,39 @@ class Volshell(interfaces.plugins.PluginInterface): for i in disasm_types[architecture].disasm(remaining_data, offset): print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}") + def _get_type_name_with_pointer( + self, + member_type: Union[ + str, interfaces.objects.ObjectInterface, interfaces.objects.Template + ], + depth: int = 0, + ) -> str: + """Takes a member_type from and returns the subtype name with a * if the member_type is + a pointer otherwise it returns just the normal type name.""" + pointer_marker = "*" * depth + try: + if member_type.vol.object_class == objects.Pointer: + sub_member_type = member_type.vol.subtype + # follow at most MAX_DEREFERENCE_COUNT pointers. A guard against, hopefully unlikely, infinite loops + if depth < MAX_DEREFERENCE_COUNT: + return self._get_type_name_with_pointer(sub_member_type, depth + 1) + except AttributeError: + pass # not all objects get a `object_class`, and those that don't are not pointers. + finally: + member_type_name = pointer_marker + member_type.vol.type_name + return member_type_name + def display_type( self, object: Union[ str, interfaces.objects.ObjectInterface, interfaces.objects.Template ], - offset: int = None, + offset: Optional[int] = None, ): """Display Type describes the members of a particular object in alphabetical order""" + + MAX_TYPENAME_DISPLAY_LENGTH = 256 + if not isinstance( object, (str, interfaces.objects.ObjectInterface, interfaces.objects.Template), @@ -374,26 +459,56 @@ class Volshell(interfaces.plugins.PluginInterface): volobject.vol.type_name, layer_name=self.current_layer, offset=offset ) - if hasattr(volobject.vol, "size"): - print(f"{volobject.vol.type_name} ({volobject.vol.size} bytes)") - elif hasattr(volobject.vol, "data_format"): - data_format = volobject.vol.data_format - print( - "{} ({} bytes, {} endian, {})".format( - volobject.vol.type_name, - data_format.length, - data_format.byteorder, - "signed" if data_format.signed else "unsigned", - ) - ) + # add special case for pointer so that information about the struct the + # pointer is pointing to is shown rather than simply the fact this is a + # pointer object. The "dereference_count < MAX_DEREFERENCE_COUNT" is to + # guard against loops + dereference_count = 0 + while ( + isinstance(volobject, objects.Pointer) + and dereference_count < MAX_DEREFERENCE_COUNT + ): + # before defreerencing the pointer, show it's information + 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. + if volobject.is_readable() and volobject != 0: + # now deference the pointer and store this as the new volobject + volobject = volobject.dereference() + dereference_count = dereference_count + 1 + else: + # if we aren't able to follow the pointers anymore then there will + # be no more information to display as we've already printed the + # details of this pointer including the fact that we're not able to + # follow it anywhere + return 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)" + if isinstance(object, str) and offset is None: + suffix = ":" + else: + # this is an actual object or an offset was given so the offset should be displayed + suffix = f" @ {hex(volobject.vol.offset)}:" + print(struct_header + suffix) + + # it is a more complex type, so all members also need information displayed longest_member = longest_offset = longest_typename = 0 for member in volobject.vol.members: relative_offset, member_type = volobject.vol.members[member] longest_member = max(len(member), longest_member) longest_offset = max(len(hex(relative_offset)), longest_offset) - longest_typename = max(len(member_type.vol.type_name), longest_typename) + member_type_name = self._get_type_name_with_pointer( + member_type + ) # special case for pointers to show what they point to + + # find the longest typename + longest_typename = max(len(member_type_name), longest_typename) + + # if the typename is very long then limit it to MAX_TYPENAME_DISPLAY_LENGTH + longest_typename = min(longest_typename, MAX_TYPENAME_DISPLAY_LENGTH) for member in sorted( volobject.vol.members, key=lambda x: (volobject.vol.members[x][0], x) @@ -401,40 +516,148 @@ class Volshell(interfaces.plugins.PluginInterface): relative_offset, member_type = volobject.vol.members[member] len_offset = len(hex(relative_offset)) len_member = len(member) - len_typename = len(member_type.vol.type_name) + + member_type_name = self._get_type_name_with_pointer( + member_type + ) # special case for pointers to show what they point to + 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]}..." + if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data + try: + value = self._display_value(volobject.member(member)) + except exceptions.InvalidAddressException: + value = self._display_value(renderers.NotAvailableValue()) print( + " " * dereference_count, " " * (longest_offset - len_offset), hex(relative_offset), ": ", member, " " * (longest_member - len_member), " ", - member_type.vol.type_name, + member_type_name, " " * (longest_typename - len_typename), " ", - self._display_value(getattr(volobject, member)), + value, ) else: + # not provided with an actual object, nor an offset so just display the types print( + " " * dereference_count, " " * (longest_offset - len_offset), hex(relative_offset), ": ", member, " " * (longest_member - len_member), " ", - member_type.vol.type_name, + member_type_name, ) - @classmethod - def _display_value(cls, value: Any) -> str: - if isinstance(value, objects.PrimitiveObject): - return repr(value) - elif isinstance(value, objects.Array): - return repr([cls._display_value(val) for val in value]) + else: # simple type with no members, only one line to print + # if the original object was just a type string, display the type information + if isinstance(object, str) and offset is None: + print(self._display_simple_type(volobject, include_value=False)) + + # if the original object was an actual volobject or was a type string + # with an offset. Then append the actual data to the display. + else: + print(" " * dereference_count, self._display_simple_type(volobject)) + + def _display_simple_type( + self, + volobject: Union[ + interfaces.objects.ObjectInterface, interfaces.objects.Template + ], + include_value: bool = True, + ) -> str: + # build the display_type_string based on the available information + + if hasattr(volobject.vol, "size"): + # the most common type to display, this shows their full size, e.g.: + # (layer_name) >>> dt('task_struct') + # symbol_table_name1!task_struct (1784 bytes) + display_type_string = ( + f"{volobject.vol.type_name} ({volobject.vol.size} bytes)" + ) + elif hasattr(volobject.vol, "data_format"): + # this is useful for very simple types like ints, e.g.: + # (layer_name) >>> dt('int') + # symbol_table_name1!int (4 bytes, little endian, signed) + data_format = volobject.vol.data_format + display_type_string = "{} ({} bytes, {} endian, {})".format( + volobject.vol.type_name, + data_format.length, + data_format.byteorder, + "signed" if data_format.signed else "unsigned", + ) + elif hasattr(volobject.vol, "type_name"): + # types like void have almost no values to display other than their name, e.g.: + # (layer_name) >>> dt('void') + # symbol_table_name1!void + display_type_string = volobject.vol.type_name else: - return hex(value.vol.offset) + # it should not be possible to have a volobject without at least a type_name + raise AttributeError("Unable to find any details for object") + + if include_value: # if include_value is true also add the value to the display + if isinstance(volobject, objects.Pointer): + # for pointers include the location of the pointer and where it points to + return f"{display_type_string} @ {hex(volobject.vol.offset)} -> {self._display_value(volobject)}" + else: + return f"{display_type_string}: {self._display_value(volobject)}" + + else: + return display_type_string + + def _display_value(self, value: Any) -> str: + try: + # if value is a BaseAbsentValue they display N/A + if isinstance(value, interfaces.renderers.BaseAbsentValue): + return "N/A" + else: + # volobject branch + if isinstance( + value, + (interfaces.objects.ObjectInterface, interfaces.objects.Template), + ): + if isinstance(value, objects.Pointer): + # show pointers in hex to match output for struct addrs + # highlight null or unreadable pointers + try: + if value == 0: + suffix = " (null pointer)" + elif not value.is_readable(): + suffix = " (unreadable pointer)" + else: + suffix = "" + except exceptions.SymbolError as exc: + suffix = f" (unknown sized {exc.symbol_name})" + return f"{hex(value)}{suffix}" + elif isinstance(value, objects.PrimitiveObject): + return repr(value) + elif isinstance(value, objects.Array): + return repr([self._display_value(val) for val in value]) + else: + if self.context.layers[self.current_layer].is_valid( + value.vol.offset + ): + return f"offset: 0x{value.vol.offset:x}" + else: + return f"offset: 0x{value.vol.offset:x} (unreadable)" + else: + # non volobject + if value is None: + return "N/A" + else: + return repr(value) + + except exceptions.InvalidAddressException: + # if value causes an InvalidAddressException like BaseAbsentValue then display N/A + return "N/A" def generate_treegrid( self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs @@ -479,7 +702,7 @@ class Volshell(interfaces.plugins.PluginInterface): if treegrid is not None: self.render_treegrid(treegrid) - def display_symbols(self, symbol_table: str = None): + def display_symbols(self, symbol_table: Optional[str] = None): """Prints an alphabetical list of symbols for a symbol table""" if symbol_table is None: print("No symbol table provided") @@ -508,10 +731,13 @@ class Volshell(interfaces.plugins.PluginInterface): location = "file:" + request.pathname2url(location) print(f"Running code from {location}\n") accessor = resources.ResourceAccessor() - with accessor.open(url=location) as fp: - self.__console.runsource( - io.TextIOWrapper(fp, encoding="utf-8").read(), symbol="exec" - ) + with accessor.open(url=location) as handle, io.TextIOWrapper( + handle, encoding="utf-8" + ) as fp: + if has_ipython: + self.__console.ex(fp.read()) + else: + self.__console.runsource(fp.read(), symbol="exec") print("\nCode complete") def load_file(self, location: str): @@ -553,17 +779,16 @@ class Volshell(interfaces.plugins.PluginInterface): if argname in kwargs: del kwargs[argname] - for keyword in kwargs: - val = kwargs[keyword] - if not isinstance( - val, interfaces.configuration.BasicTypes - ) and not isinstance(val, list): - if not isinstance(val, list) or all( - isinstance(x, interfaces.configuration.BasicTypes) for x in val - ): - raise TypeError( - "Configurable values must be simple types (int, bool, str, bytes)" - ) + for keyword, val in kwargs.items(): + BasicType_or_list_of_BasicType = False # excludes list of lists + if isinstance(val, interfaces.configuration.BasicTypes): + BasicType_or_list_of_BasicType = True + if all(isinstance(x, interfaces.configuration.BasicTypes) for x in val): + BasicType_or_list_of_BasicType = True + if not BasicType_or_list_of_BasicType: + raise TypeError( + "Configurable values must be simple types (int, bool, str, bytes)" + ) self.context.config[config_path + "." + keyword] = val constructed = clazz(self.context, config_path, **constructor_args) @@ -575,6 +800,89 @@ class Volshell(interfaces.plugins.PluginInterface): return constructed + def breakpoint( + self, address: int, layer_name: Optional[str] = None, lowest: bool = False + ) -> None: + """Sets a breakpoint on a particular address (within a specific layer)""" + if layer_name is None: + if self.current_layer is None: + raise ValueError("Current layer must be set") + layer_name = self.current_layer + + layer: interfaces.layers.DataLayerInterface = self.context.layers[layer_name] + + if lowest: + while isinstance(layer, interfaces.layers.TranslationLayerInterface): + mapping = layer.mapping(address, 1) + if not mapping: + raise ValueError( + "Offset cannot be mapped lower, cannot break at lowest layer" + ) + _, _, mapped_offset, _, mapped_layer_name = next(mapping) + layer = self.context.layers[mapped_layer_name] + address = mapped_offset + + # Check if the read value is already overloaded + if not hasattr(layer.read, "breakpoints"): + # Layer read is not yet wrapped + def wrapped_read(offset: int, length: int, pad: bool = False) -> bytes: + original_read = getattr(wrapped_read, "original_read") + for breakpoint in getattr(wrapped_read, "breakpoints"): + if (offset <= breakpoint) and (breakpoint < offset + length): + print( + "Hit breakpoint, entering python debugger. To continue running without the debugger use the command continue" + ) + import pdb + + pdb.set_trace() + _ = "First statement after the breakpoint, use u(p), d(own) and list to navigate through the execution frames" + return original_read(offset, length, pad) + + setattr(wrapped_read, "breakpoints", set()) + setattr(wrapped_read, "original_read", layer.read) + setattr(layer, "read", wrapped_read) + + # Add the new breakpoint + print(f"Setting breakpoint {address:#x} on {layer.name}") + breakpoints = getattr(layer.read, "breakpoints") + breakpoints.add(address) + setattr(layer.read, "breakpoints", breakpoints) + + def breakpoint_list(self, layer_names: Optional[List[str]] = None): + """List available breakpoints for a set of layers""" + if not layer_names: + layer_names = [layer_name for layer_name in self.context.layers] + + print("Listing breakpoints:") + for layer_name in layer_names: + print(f" {layer_name}") + layer = self.context.layers.get(layer_name, None) + if layer and hasattr(layer.read, "breakpoints"): + for breakpoint in layer.read.breakpoints: + print(f" {breakpoint:#x}") + + def breakpoint_clear( + self, offset: Optional[int] = None, layer_name: Optional[str] = None + ): + """Clears a offset breakpoint on a layer (or all breakpoints if offset or layer not specified) + + Args: + offset: Address of the breakpoint to clear (or all if None) + layer_name: Layer to clear breakpoints from (or all if None) + """ + print("Clearing breakpoints:") + for candidate_layer_name in self.context.layers: + candidate_layer = self.context.layers[candidate_layer_name] + if layer_name is None or layer_name == candidate_layer_name: + print(f" {candidate_layer_name}") + if hasattr(candidate_layer.read, "breakpoints"): + breakpoints_to_remove = set() + for breakpoint in candidate_layer.read.breakpoints: + if offset is None or offset == breakpoint: + print(f" clearing {breakpoint:#x}") + breakpoints_to_remove.add(breakpoint) + candidate_layer.read.breakpoints -= breakpoints_to_remove + class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface): """Null FileHandler that swallows files whole without consuming memory""" @@ -585,7 +893,6 @@ class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface): def writelines(self, lines: Iterable[bytes]): """Dummy method""" - pass def write(self, b: bytes): """Dummy method""" diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index c5e555ec7..8b9e236b4 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Any, List, Tuple, Union +from typing import Any, List, Optional, Tuple, Union +from enum import Enum from volatility3.cli.volshell import generic from volatility3.framework import constants, interfaces @@ -10,6 +11,16 @@ from volatility3.framework.configuration import requirements from volatility3.plugins.linux import pslist +# Could import the enum from psscan.py to avoid code duplication +class DescExitStateEnum(Enum): + """Enum for linux task exit_state as defined in include/linux/sched.h""" + + TASK_RUNNING = 0x00000000 + EXIT_DEAD = 0x00000010 + EXIT_ZOMBIE = 0x00000020 + EXIT_TRACE = EXIT_ZOMBIE | EXIT_DEAD + + class Volshell(generic.Volshell): """Shell environment to directly interact with a linux memory image.""" @@ -19,13 +30,18 @@ class Volshell(generic.Volshell): requirements.ModuleRequirement( name="kernel", description="Linux kernel module" ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.IntRequirement( name="pid", description="Process ID", optional=True ), - ] + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), + ] + super().get_requirements() def change_task(self, pid=None): """Change the current process and layer, based on a process ID""" @@ -40,6 +56,71 @@ class Volshell(generic.Volshell): return None print(f"No task with task ID {pid} found") + def get_process(self, pid=None, virtaddr=None, physaddr=None): + """Return the task_struct object that matches the pid. If a physical or a virtual address is provided, construct the task_struct object at said address. Only one parameter is allowed. + + Args: + pid (int, optional): PID to search for + virtaddr (int, optional): Virtual address to construct object at + physaddr (int, optional): Physical address to construct object at + + Returns: + ObjectInterface: task_struct Object + """ + + if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1: + print("Only one parameter is accepted") + return None + + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] + + kernel_layer_name = vmlinux.layer_name + kernel_layer = self.context.layers[kernel_layer_name] + + memory_layer_name = kernel_layer.dependencies[0] + + task_struct_symbol = vmlinux.symbol_table_name + constants.BANG + "task_struct" + + if virtaddr is not None: + task = self.context.object( + task_struct_symbol, + layer_name=kernel_layer_name, + offset=virtaddr, + ) + + if physaddr is not None: + task = self.context.object( + task_struct_symbol, + layer_name=memory_layer_name, + offset=physaddr, + native_layer_name=kernel_layer_name, + ) + + if physaddr is not None or virtaddr is not None: + try: + DescExitStateEnum(task.exit_state) + except ValueError: + print( + f"task_struct @ {hex(task.vol.offset)} as exit_state {task.exit_state} is likely not valid" + ) + + if not (0 < task.pid < 65535): + print( + f"task_struct @ {hex(task.vol.offset)} as pid {task.pid} is likely not valid" + ) + + return task + + if pid is not None: + tasks = self.list_tasks() + for task in tasks: + if task.pid == pid: + return task + print(f"No task with task ID {pid} found") + + return None + def list_tasks(self): """Returns a list of task objects from the primary layer""" # We always use the main kernel memory and associated symbols @@ -50,6 +131,7 @@ class Volshell(generic.Volshell): result += [ (["ct", "change_task", "cp"], self.change_task), (["lt", "list_tasks", "ps"], self.list_tasks), + (["gp", "get_process", "get_task"], self.get_process), (["symbols"], self.context.symbol_space[self.current_symbol_table]), ] if self.config.get("pid", None) is not None: @@ -61,7 +143,7 @@ class Volshell(generic.Volshell): object: Union[ str, interfaces.objects.ObjectInterface, interfaces.objects.Template ], - offset: int = None, + offset: Optional[int] = None, ): """Display Type describes the members of a particular object in alphabetical order""" if isinstance(object, str): @@ -69,7 +151,7 @@ class Volshell(generic.Volshell): object = self.current_symbol_table + constants.BANG + object return super().display_type(object, offset) - def display_symbols(self, symbol_table: str = None): + def display_symbols(self, symbol_table: Optional[str] = None): """Prints an alphabetical list of symbols for a symbol table""" if symbol_table is None: symbol_table = self.current_symbol_table diff --git a/volatility3/cli/volshell/mac.py b/volatility3/cli/volshell/mac.py index 2b32ad677..190c7b9f7 100644 --- a/volatility3/cli/volshell/mac.py +++ b/volatility3/cli/volshell/mac.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Any, List, Tuple, Union +from typing import Any, List, Optional, Tuple, Union from volatility3.cli.volshell import generic from volatility3.framework import constants, interfaces @@ -19,13 +19,18 @@ class Volshell(generic.Volshell): requirements.ModuleRequirement( name="kernel", description="Darwin kernel module" ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.IntRequirement( name="pid", description="Process ID", optional=True ), - ] + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), + ] + super().get_requirements() def change_task(self, pid=None): """Change the current process and layer, based on a process ID""" @@ -63,7 +68,7 @@ class Volshell(generic.Volshell): object: Union[ str, interfaces.objects.ObjectInterface, interfaces.objects.Template ], - offset: int = None, + offset: Optional[int] = None, ): """Display Type describes the members of a particular object in alphabetical order""" if isinstance(object, str): @@ -71,7 +76,7 @@ class Volshell(generic.Volshell): object = self.current_symbol_table + constants.BANG + object return super().display_type(object, offset) - def display_symbols(self, symbol_table: str = None): + def display_symbols(self, symbol_table: Optional[str] = None): """Prints an alphabetical list of symbols for a symbol table""" if symbol_table is None: symbol_table = self.current_symbol_table diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index 5c2190c02..e7e37ed61 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Any, List, Tuple, Union +from typing import Any, List, Optional, Tuple, Union from volatility3.cli.volshell import generic from volatility3.framework import constants, interfaces @@ -17,13 +17,18 @@ class Volshell(generic.Volshell): def get_requirements(cls): return [ requirements.ModuleRequirement(name="kernel", description="Windows kernel"), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.IntRequirement( name="pid", description="Process ID", optional=True ), - ] + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), + ] + super().get_requirements() def change_process(self, pid=None): """Change the current process and layer, based on a process ID""" @@ -39,16 +44,70 @@ class Volshell(generic.Volshell): """Returns a list of EPROCESS objects from the primary layer""" # We always use the main kernel memory and associated symbols return list( - pslist.PsList.list_processes( - self.context, self.current_layer, self.current_symbol_table - ) + pslist.PsList.list_processes(self.context, self.current_kernel_name) ) + def get_process(self, pid=None, virtaddr=None, physaddr=None): + """Returns the _EPROCESS object that matches the pid. If a physical or a virtual address is provided, construct the _EPROCESS object at said address. Only one parameter is allowed. + + Args: + pid (int, optional): PID / UniqueProcessId to search for. + virtaddr (int, optional): Virtual address to construct object at + physaddr (int, optional): Physical address to construct object at + + Returns: + ObjectInterface: _EPROCESS Object + """ + + if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1: + print("Only one parameter is accepted") + return None + + kernel_name = self.config["kernel"] + kernel = self.context.modules[kernel_name] + + kernel_layer_name = kernel.layer_name + + kernel_layer = self.context.layers[kernel_layer_name] + memory_layer_name = kernel_layer.dependencies[0] + + eprocess_symbol = kernel.symbol_table_name + constants.BANG + "_EPROCESS" + + if virtaddr is not None: + eproc = self.context.object( + eprocess_symbol, + layer_name=kernel_layer_name, + offset=virtaddr, + ) + + return eproc + + if physaddr is not None: + eproc = self.context.object( + eprocess_symbol, + layer_name=memory_layer_name, + offset=physaddr, + native_layer_name=kernel_layer_name, + ) + + return eproc + + if pid is not None: + processes = self.list_processes() + for process in processes: + if process.UniqueProcessId == pid: + return process + print(f"No process with process ID {pid} found") + return None + + return None + def construct_locals(self) -> List[Tuple[List[str], Any]]: result = super().construct_locals() result += [ (["cp", "change_process"], self.change_process), (["lp", "list_processes", "ps"], self.list_processes), + (["gp", "get_process"], self.get_process), (["symbols"], self.context.symbol_space[self.current_symbol_table]), ] if self.config.get("pid", None) is not None: @@ -60,7 +119,7 @@ class Volshell(generic.Volshell): object: Union[ str, interfaces.objects.ObjectInterface, interfaces.objects.Template ], - offset: int = None, + offset: Optional[int] = None, ): """Display Type describes the members of a particular object in alphabetical order""" if isinstance(object, str): @@ -68,7 +127,7 @@ class Volshell(generic.Volshell): object = self.current_symbol_table + constants.BANG + object return super().display_type(object, offset) - def display_symbols(self, symbol_table: str = None): + def display_symbols(self, symbol_table: Optional[str] = None): """Prints an alphabetical list of symbols for a symbol table""" if symbol_table is None: symbol_table = self.current_symbol_table diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 51310bfa2..6943598cf 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -2,35 +2,31 @@ # 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 import zipfile - -required_python_version = (3, 8, 0) -if ( - sys.version_info.major != required_python_version[0] - or sys.version_info.minor < required_python_version[1] - or ( - sys.version_info.minor == required_python_version[1] - and sys.version_info.micro < required_python_version[2] - ) -): - raise RuntimeError( - "Volatility framework requires python version {}.{}.{} or greater".format( - *required_python_version - ) - ) - import importlib import inspect import logging import os import traceback -from typing import Any, Dict, Generator, List, Tuple, Type, TypeVar +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar -from volatility3.framework import constants, interfaces +from volatility3.framework import constants, interfaces, versionutils +if ( + sys.version_info.major != constants.REQUIRED_PYTHON_VERSION[0] + or sys.version_info.minor < constants.REQUIRED_PYTHON_VERSION[1] + or ( + sys.version_info.minor == constants.REQUIRED_PYTHON_VERSION[1] + and sys.version_info.micro < constants.REQUIRED_PYTHON_VERSION[2] + ) +): + raise RuntimeError( + f"Volatility framework requires python version {'.'.join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater" + ) # ## # @@ -53,30 +49,22 @@ vollog = logging.getLogger(__name__) def require_interface_version(*args) -> None: """Checks the required version of a plugin.""" - if len(args): - if args[0] != interface_version()[0]: - raise RuntimeError( - "Framework interface version {} is incompatible with required version {}".format( - interface_version()[0], args[0] - ) + if not versionutils.matches_required(args, interface_version()): + raise RuntimeError( + "Framework interface version {} is incompatible with required version {}".format( + ".".join(str(x) for x in interface_version()[0:2]), + ".".join(str(x) for x in args[0:2]), ) - if len(args) > 1: - if args[1] > interface_version()[1]: - raise RuntimeError( - "Framework interface version {} is an older revision than the required version {}".format( - ".".join([str(x) for x in interface_version()[0:2]]), - ".".join([str(x) for x in args[0:2]]), - ) - ) + ) -class NonInheritable(object): +class NonInheritable: def __init__(self, value: Any, cls: Type) -> None: self.default_value = value self.cls = cls - def __get__(self, obj: Any, get_type: Type = None) -> Any: - if type == self.cls: + def __get__(self, obj: Any, get_type: Type = Optional[None]) -> Any: + if type is self.cls: if hasattr(self.default_value, "__get__"): return self.default_value.__get__(obj, get_type) return self.default_value @@ -99,8 +87,7 @@ def class_subclasses(cls: Type[T]) -> Generator[Type[T], None, None]: # The typing system is not clever enough to realize that clazz has a hidden attr after the hasattr check if not hasattr(clazz, "hidden") or not clazz.hidden: # type: ignore yield clazz - for return_value in class_subclasses(clazz): - yield return_value + yield from class_subclasses(clazz) def import_files(base_module, ignore_errors: bool = False) -> List[str]: @@ -161,11 +148,7 @@ def import_files(base_module, ignore_errors: bool = False) -> List[str]: def _filter_files(filename: str): """Ensures that a filename traversed is an importable python file""" - return ( - filename.endswith(".py") - or filename.endswith(".pyc") - or filename.endswith(".pyo") - ) and not filename.startswith("__") + return (filename.endswith((".py", ".pyc"))) and not filename.startswith("__") def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str]: @@ -189,9 +172,7 @@ def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str traceback.TracebackException.from_exception(e).format(chain=True) ) ) - vollog.debug( - "Failed to import module {} based on file: {}".format(module, path) - ) + vollog.debug(f"Failed to import module {module} based on file: {path}") failures.append(module) if not ignore_errors: raise @@ -209,8 +190,7 @@ def _zipwalk(path: str): zip_results[os.path.join(path, os.path.dirname(file.filename))] = ( dirlist ) - for value in zip_results: - yield value, zip_results[value] + yield from zip_results.items() def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]: @@ -233,4 +213,4 @@ def clear_cache(complete=True): os.unlink(cache_filename) os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)) except FileNotFoundError: - vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache") + vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existent cache") diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index 52a73f45a..95703aaf7 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -3,8 +3,7 @@ # import logging -import os -from typing import Optional, Tuple, Type +from typing import Optional, Tuple from volatility3.framework import constants, interfaces from volatility3.framework.automagic import symbol_cache, symbol_finder @@ -27,16 +26,6 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): progress_callback: constants.ProgressCallback = None, ) -> Optional[interfaces.layers.DataLayerInterface]: """Attempts to identify linux within this layer.""" - # Version check the SQlite cache - required = (1, 0, 0) - if not requirements.VersionRequirement.matches_required( - required, symbol_cache.SqliteCache.version - ): - vollog.info( - f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}" - ) - return None - # Bail out by default unless we can stack properly layer = context.layers[layer_name] join = interfaces.configuration.path_join @@ -46,12 +35,9 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): if isinstance(layer, intel.Intel): return None - identifiers_path = os.path.join( - constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME + linux_banners = symbol_cache.load_cache_manager().get_identifier_dictionary( + operating_system="linux" ) - linux_banners = symbol_cache.SqliteCache( - identifiers_path - ).get_identifier_dictionary(operating_system="linux") # If we have no banners, don't bother scanning if not linux_banners: vollog.info( @@ -76,18 +62,27 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): isf_url=isf_path, ) context.symbol_space.append(table) + kaslr_shift, aslr_shift = cls.find_aslr( - context, table_name, layer_name, progress_callback=progress_callback + context, + table_name, + layer_name, + progress_callback=progress_callback, ) - layer_class: Type = intel.Intel if "init_top_pgt" in table.symbols: - layer_class = intel.Intel32e + layer_class = intel.LinuxIntel32e dtb_symbol_name = "init_top_pgt" elif "init_level4_pgt" in table.symbols: - layer_class = intel.Intel32e + layer_class = intel.LinuxIntel32e dtb_symbol_name = "init_level4_pgt" + elif "pkmap_count" in table.symbols and table.get_symbol( + "pkmap_count" + ).type.count in (512, 2048): + layer_class = intel.LinuxIntelPAE + dtb_symbol_name = "swapper_pg_dir" else: + layer_class = intel.LinuxIntel dtb_symbol_name = "swapper_pg_dir" dtb = cls.virtual_to_physical_address( @@ -126,7 +121,17 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): progress_callback: constants.ProgressCallback = None, ) -> Tuple[int, int]: """Determines the offset of the actual DTB in physical space and its - symbol offset.""" + symbol offset. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + symbol_table: The name of the kernel module on which to operate + layer_name: The layer within the context in which the module exists + progress_callback: A function that takes a percentage (and an optional description) that will be called periodically + + Returns: + kaslr_shirt and aslr_shift + """ init_task_symbol = symbol_table + constants.BANG + "init_task" init_task_json_address = context.symbol_space.get_symbol( init_task_symbol @@ -156,6 +161,18 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): and init_task.state.cast("unsigned int") != 0 ): continue + elif init_task.active_mm.cast("long unsigned int") == module.get_symbol( + "init_mm" + ).address and init_task.tasks.next.cast( + "long unsigned int" + ) == init_task.tasks.prev.cast( + "long unsigned int" + ): + # The idle task steals `mm` from previously running task, i.e., + # `init_mm` is only used as long as no CPU has ever been idle. + # This catches cases where we found a fragment of the + # unrelocated ELF file instead of the running kernel. + continue # This we get for free aslr_shift = ( @@ -174,9 +191,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): if aslr_shift & 0xFFF != 0 or kaslr_shift & 0xFFF != 0: continue vollog.debug( - "Linux ASLR shift values determined: physical {:0x} virtual {:0x}".format( - kaslr_shift, aslr_shift - ) + f"Linux ASLR shift values determined: physical {kaslr_shift:0x} virtual {aslr_shift:0x}" ) return kaslr_shift, aslr_shift @@ -184,8 +199,8 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): vollog.debug("Scanners could not determine any ASLR shifts, using 0 for both") return 0, 0 - @classmethod - def virtual_to_physical_address(cls, addr: int) -> int: + @staticmethod + def virtual_to_physical_address(addr: int) -> int: """Converts a virtual linux address to a physical one (does not account of ASLR)""" if addr > 0xFFFFFFFF80000000: @@ -199,5 +214,217 @@ class LinuxSymbolFinder(symbol_finder.SymbolFinder): banner_config_key = "kernel_banner" operating_system = "linux" symbol_class = "volatility3.framework.symbols.linux.LinuxKernelIntermedSymbols" - find_aslr = lambda cls, *args: LinuxIntelStacker.find_aslr(*args)[1] exclusion_list = ["mac", "windows"] + + @classmethod + def find_aslr(cls, *args): + return LinuxIntelStacker.find_aslr(*args)[1] + + +class LinuxIntelVMCOREINFOStacker(interfaces.automagic.StackerLayerInterface): + stack_order = 34 + exclusion_list = ["mac", "windows"] + + @staticmethod + def _check_versions() -> bool: + """Verify the versions of the required modules""" + # Check VMCOREINFO API version + vmcoreinfo_version_required = (1, 0, 0) + if not requirements.VersionRequirement.matches_required( + vmcoreinfo_version_required, linux.VMCoreInfo.version + ): + vollog.info( + "VMCOREINFO version not suitable: required %s found %s", + vmcoreinfo_version_required, + linux.VMCoreInfo.version, + ) + return False + + return True + + @classmethod + def stack( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + progress_callback: constants.ProgressCallback = None, + ) -> Optional[interfaces.layers.DataLayerInterface]: + """Attempts to identify linux within this layer.""" + + # Verify the versions of the required modules + if not cls._check_versions(): + return None + + # Bail out by default unless we can stack properly + layer = context.layers[layer_name] + + # Never stack on top of an intel layer + # FIXME: Find a way to improve this check + if isinstance(layer, intel.Intel): + return None + + linux_banners = symbol_cache.load_cache_manager().get_identifier_dictionary( + operating_system="linux" + ) + if not linux_banners: + # If we have no banners, don't bother scanning + vollog.info( + "No Linux banners found - if this is a linux plugin, please check your " + "symbol files location" + ) + return None + + vmcoreinfo_elf_notes_iter = linux.VMCoreInfo.search_vmcoreinfo_elf_note( + context=context, + layer_name=layer_name, + progress_callback=progress_callback, + ) + + # Iterate through each VMCOREINFO ELF note found, using the first one that is valid. + for _vmcoreinfo_offset, vmcoreinfo in vmcoreinfo_elf_notes_iter: + shifts = cls._vmcoreinfo_find_aslr(vmcoreinfo) + if not shifts: + # Let's try the next VMCOREINFO, in case this one isn't correct. + continue + + kaslr_shift, aslr_shift = shifts + + dtb = cls._vmcoreinfo_get_dtb(vmcoreinfo, aslr_shift, kaslr_shift) + if dtb is None: + # Discard this VMCOREINFO immediately + continue + + is_32bit, is_pae = cls._vmcoreinfo_is_32bit(vmcoreinfo) + if is_32bit: + layer_class = intel.IntelPAE if is_pae else intel.Intel + else: + layer_class = intel.Intel32e + + uts_release = vmcoreinfo["OSRELEASE"] + + # See how linux_banner constant is built in the linux kernel + linux_version_prefix = f"Linux version {uts_release} (".encode() + valid_banners = [ + x for x in linux_banners if x and x.startswith(linux_version_prefix) + ] + if not valid_banners: + # There's no banner matching this VMCOREINFO, keep trying with the next one + continue + elif len(valid_banners) == 1: + # Usually, we narrow down the Linux banner list to a single element. + # Using BytesScanner here is slightly faster than MultiStringScanner. + scanner = scanners.BytesScanner(valid_banners[0]) + else: + scanner = scanners.MultiStringScanner(valid_banners) + + join = interfaces.configuration.path_join + for match in layer.scan( + context=context, scanner=scanner, progress_callback=progress_callback + ): + # Unfortunately, the scanners do not maintain a consistent interface + banner = match[1] if isinstance(match, Tuple) else valid_banners[0] + + isf_path = linux_banners.get(banner, None) + if not isf_path: + vollog.warning( + "Identified banner %r, but no matching ISF is available.", + banner, + ) + continue + + vollog.debug("Identified banner: %r", banner) + table_name = context.symbol_space.free_table_name("LintelStacker") + table = linux.LinuxKernelIntermedSymbols( + context, + f"temporary.{table_name}", + name=table_name, + isf_url=isf_path, + ) + context.symbol_space.append(table) + + # Build the new layer + new_layer_name = context.layers.free_layer_name("primary") + config_path = join("vmcoreinfo", new_layer_name) + kernel_banner = LinuxSymbolFinder.banner_config_key + banner_str = banner.decode(encoding="latin-1") + context.config[join(config_path, kernel_banner)] = banner_str + context.config[join(config_path, "memory_layer")] = layer_name + context.config[join(config_path, "page_map_offset")] = dtb + context.config[join(config_path, "kernel_virtual_offset")] = aslr_shift + layer = layer_class( + context, + config_path=config_path, + name=new_layer_name, + metadata={"os": "Linux"}, + ) + + if layer: + vollog.debug( + "Values found in VMCOREINFO: KASLR=0x%x, ASLR=0x%x, DTB=0x%x", + kaslr_shift, + aslr_shift, + dtb, + ) + + return layer + + vollog.debug("No suitable linux banner could be matched") + return None + + @staticmethod + def _vmcoreinfo_find_aslr(vmcoreinfo) -> Tuple[int, int]: + phys_base = vmcoreinfo.get("NUMBER(phys_base)") + if phys_base is None: + # In kernel < 4.10, there may be a SYMBOL(phys_base), but as noted in the + # c401721ecd1dcb0a428aa5d6832ee05ffbdbffbbe commit comment, this value + # isn't useful for calculating the physical address. + # There's nothing we can do here, so let's try with the next VMCOREINFO or + # the next Stacker. + return None + + # kernels 3.14 (b6085a865762236bb84934161273cdac6dd11c2d) KERNELOFFSET was added + kerneloffset = vmcoreinfo.get("KERNELOFFSET") + if kerneloffset is None: + # kernels < 3.14 if KERNELOFFSET is missing, KASLR might not be implemented. + # Oddly, NUMBER(phys_base) is present without it. To be safe, proceed only + # if both are present. + return None + + aslr_shift = kerneloffset + kaslr_shift = phys_base + aslr_shift + + return kaslr_shift, aslr_shift + + @staticmethod + def _vmcoreinfo_get_dtb(vmcoreinfo, aslr_shift, kaslr_shift) -> int: + """Returns the page global directory physical address (a.k.a DTB or PGD)""" + # In x86-64, since kernels 2.5.22 swapper_pg_dir is a macro to the respective pgd. + # First, in e3ebadd95cb621e2c7436f3d3646447ac9d5c16d to init_level4_pgt, and later + # in kernels 4.13 in 65ade2f872b474fa8a04c2d397783350326634e6) to init_top_pgt. + # In x86-32, the pgd is swapper_pg_dir. So, in any case, for VMCOREINFO + # SYMBOL(swapper_pg_dir) will always have the right value. + dtb_vaddr = vmcoreinfo.get("SYMBOL(swapper_pg_dir)") + if dtb_vaddr is None: + # Abort, it should be present + return None + + dtb_paddr = ( + LinuxIntelStacker.virtual_to_physical_address(dtb_vaddr) + - aslr_shift + + kaslr_shift + ) + + return dtb_paddr + + @staticmethod + def _vmcoreinfo_is_32bit(vmcoreinfo) -> Tuple[bool, bool]: + """Returns a tuple of booleans with is_32bit and is_pae values""" + is_pae = vmcoreinfo.get("CONFIG_X86_PAE", "n") == "y" + if is_pae: + is_32bit = True + else: + # Check the swapper_pg_dir virtual address size + dtb_vaddr = vmcoreinfo["SYMBOL(swapper_pg_dir)"] + is_32bit = dtb_vaddr <= 2**32 + + return is_32bit, is_pae diff --git a/volatility3/framework/automagic/mac.py b/volatility3/framework/automagic/mac.py index e51753139..f3679d160 100644 --- a/volatility3/framework/automagic/mac.py +++ b/volatility3/framework/automagic/mac.py @@ -3,13 +3,11 @@ # import logging -import os import struct from typing import Optional from volatility3.framework import constants, exceptions, interfaces, layers from volatility3.framework.automagic import symbol_cache, symbol_finder -from volatility3.framework.configuration import requirements from volatility3.framework.layers import intel, scanners from volatility3.framework.symbols import mac @@ -28,16 +26,6 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface): progress_callback: constants.ProgressCallback = None, ) -> Optional[interfaces.layers.DataLayerInterface]: """Attempts to identify mac within this layer.""" - # Version check the SQlite cache - required = (1, 0, 0) - if not requirements.VersionRequirement.matches_required( - required, symbol_cache.SqliteCache.version - ): - vollog.info( - f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}" - ) - return None - # Bail out by default unless we can stack properly layer = context.layers[layer_name] new_layer = None @@ -48,12 +36,9 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface): if isinstance(layer, intel.Intel): return None - identifiers_path = os.path.join( - constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME + mac_banners = symbol_cache.load_cache_manager().get_identifier_dictionary( + operating_system="mac" ) - mac_banners = symbol_cache.SqliteCache( - identifiers_path - ).get_identifier_dictionary(operating_system="mac") # If we have no banners, don't bother scanning if not mac_banners: vollog.info( @@ -197,7 +182,7 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface): aslr_shift = 0 for offset, banner in offset_generator: - banner_major, banner_minor = [int(x) for x in banner[22:].split(b".")[0:2]] + banner_major, banner_minor = (int(x) for x in banner[22:].split(b".")[0:2]) tmp_aslr_shift = offset - cls.virtual_to_physical_address( version_json_address diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 7f38a23e1..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 @@ -17,7 +18,7 @@ from volatility3.framework import constants, exceptions, interfaces, layers from volatility3.framework.configuration import requirements from volatility3.framework.layers import intel, scanners from volatility3.framework.symbols import native -from volatility3.framework.symbols.windows.pdbutil import PDBUtility +from volatility3.framework.symbols.windows import pdbutil if __name__ == "__main__": import sys @@ -50,6 +51,21 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): max_pdb_size = 0x400000 exclusion_list = ["linux", "mac"] + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="pdb_utility", + component=pdbutil.PDBUtility, + version=(1, 0, 1), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), + ] + def find_virtual_layers_from_req( self, context: interfaces.context.ContextInterface, @@ -120,7 +136,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): ): raise TypeError("PDB name or GUID not a string value") - PDBUtility.load_windows_symbol_table( + pdbutil.PDBUtility.load_windows_symbol_table( context=context, guid=kernel["GUID"], age=kernel["age"], @@ -215,9 +231,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): return (virtual_layer_name, kvo, kernel) else: vollog.debug( - "Potential kernel_virtual_offset did not map to expected location: {}".format( - hex(kvo) - ) + f"Potential kernel_virtual_offset did not map to expected location: {hex(kvo)}" ) except exceptions.InvalidAddressException: vollog.debug( @@ -261,7 +275,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): bytes(name + ".pdb", "utf-8") for name in constants.windows.KERNEL_MODULE_NAMES ] - kernels = PDBUtility.pdbname_scan( + kernels = pdbutil.PDBUtility.pdbname_scan( ctx=context, layer_name=layer_to_scan, start=start_scan_address, @@ -270,6 +284,10 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): progress_callback=progress_callback, ) for kernel in kernels: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Testing potential kernel for {kernel.get('pdb_name', 'Unknown')} at {kernel.get('signature_offset', -1):x} with MZ offset at {(kernel.get('mz_offset', -1) or -1):x}", + ) valid_kernel = test_kernel(physical_layer_name, virtual_layer_name, kernel) if valid_kernel is not None: break @@ -360,7 +378,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): with contextlib.suppress(exceptions.InvalidAddressException): if vlayer.read(address, 0x2) == b"MZ": res = list( - PDBUtility.pdbname_scan( + pdbutil.PDBUtility.pdbname_scan( ctx=context, layer_name=vlayer.name, page_size=vlayer.page_size, @@ -374,8 +392,74 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): valid_kernel = (virtual_layer_name, address, res[0]) return valid_kernel + def method_low_stub_offset( + self, + context: interfaces.context.ContextInterface, + vlayer: layers.intel.Intel, + progress_callback: constants.ProgressCallback = None, + ) -> Optional[ValidKernelType]: + # This method is only valid for x64 systems + if not isinstance(vlayer, intel.Intel32e): + return None + kernel_hint = 0 + kernel_base = 0 + physical_layer = context.layers.get("memory_layer") + + # Try locating kernel base via x64 Low Stub in lower 1MB starting from second page (4KB) + # If "Discard Low Memory" setting is disabled in BIOS, the Low Stub may be at the third/fourth or further pages + for offset in range(0x1000, 0x100000, 0x1000): + try: + jmp_and_completion_values = int.from_bytes( + physical_layer.read(offset, 0x8), "little" + ) + if ( + 0xFFFFFFFFFFFF00FF & jmp_and_completion_values + != constants.windows.JMP_AND_COMPLETION_SIGNATURE + ): + continue + cr3_value = int.from_bytes( + physical_layer.read( + offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8 + ), + "little", + ) + + # Compare previously observed valid page table address that's stored in vlayer._initial_entry + # with PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3 + # which was observed to be an invalid page address, so add 1 (to make it valid too) + if (cr3_value + 1) != vlayer._initial_entry: + continue + potential_kernel_hint = int.from_bytes( + physical_layer.read( + offset + + constants.windows.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET, + 0x8, + ), + "little", + ) + if 0x3 & potential_kernel_hint: + continue + kernel_hint = potential_kernel_hint & 0xFFFFFFFFFFFF + kernel_base = kernel_hint & (~0x1FFFFF) & 0xFFFFFFFFFFFF + break + except exceptions.InvalidAddressException: + continue + + if kernel_base: + # Scanning 32mb in 2mb chunks for the 'ntoskrnl' base address + while (kernel_base + 0x2000000) > kernel_hint: + for i in range(0, 0x200000, 0x1000): + valid_kernel = self.check_kernel_offset( + context, vlayer, kernel_base + i, progress_callback + ) + if valid_kernel: + return valid_kernel + kernel_base -= 0x200000 + return None + # List of methods to be run, in order, to determine the valid kernels methods = [ + method_low_stub_offset, method_kdbg_offset, method_module_offset, method_fixed_mapping, diff --git a/volatility3/framework/automagic/stacker.py b/volatility3/framework/automagic/stacker.py index c251d3c46..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}" @@ -166,7 +167,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface): cls, context: interfaces.context.ContextInterface, initial_layer: str, - stack_set: List[Type[interfaces.automagic.StackerLayerInterface]] = None, + stack_set: Optional[ + List[Type[interfaces.automagic.StackerLayerInterface]] + ] = None, progress_callback: constants.ProgressCallback = None, ): """Stacks as many possible layers on top of the initial layer as can be done. diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index 3f399a777..f3cfb0584 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -104,9 +104,11 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface): for subclazz in framework.class_subclasses(IdentifierProcessor): self._classifiers[subclazz.operating_system] = subclazz + @abstractmethod def add_identifier(self, location: str, operating_system: str, identifier: str): """Adds an identifier to the store""" + @abstractmethod def find_location( self, identifier: bytes, operating_system: Optional[str] ) -> Optional[str]: @@ -120,15 +122,18 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface): The location of the symbols file that matches the identifier """ + @abstractmethod def get_local_locations(self) -> Iterable[str]: """Returns a list of all the local locations""" + @abstractmethod def update(self): """Locates all files under the symbol directories. Updates the cache with additions, modifications and removals. This also updates remote locations based on a cache timeout. """ + @abstractmethod def get_identifier_dictionary( self, operating_system: Optional[str] = None, local_only: bool = False ) -> Dict[bytes, str]: @@ -142,12 +147,15 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface): A dictionary of identifiers mapped to a location """ + @abstractmethod def get_identifier(self, location: str) -> Optional[bytes]: """Returns an identifier based on a specific location or None""" + @abstractmethod def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]: """Returns all identifiers for a particular operating system""" + @abstractmethod def get_location_statistics( self, location: str ) -> Optional[Tuple[int, int, int, int]]: @@ -157,6 +165,7 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface): A tuple of base_types, types, enums, symbols, or None is location not found """ + @abstractmethod def get_hash(self, location: str) -> Optional[str]: """Returns the hash of the JSON from within a location ISF""" @@ -296,6 +305,13 @@ class SqliteCache(CacheManagerInterface): This also updates remote locations based on a cache timeout. """ + if progress_callback is None: + + def dummy_progress(*args, **kargs) -> None: + return None + + progress_callback = dummy_progress + on_disk_locations = set( [ filename @@ -501,6 +517,21 @@ class SqliteCache(CacheManagerInterface): return output +def load_cache_manager(cache_file: Optional[str] = None) -> CacheManagerInterface: + """Loads a cache manager based on a specific cache file""" + if cache_file is None: + cache_file = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME) + # Different implementations of cache + if not os.path.exists(cache_file): + raise ValueError("Non-existent cache file provided") + with open(cache_file, "rb") as fp: + header = fp.read(4) + if header not in [b"SQLi"]: + raise ValueError("Identifier file not in recognized format") + # Currently only one choice, so use that + return SqliteCache(cache_file) + + ### Automagic diff --git a/volatility3/framework/automagic/symbol_finder.py b/volatility3/framework/automagic/symbol_finder.py index 21e594549..d7c6a22c1 100644 --- a/volatility3/framework/automagic/symbol_finder.py +++ b/volatility3/framework/automagic/symbol_finder.py @@ -4,7 +4,7 @@ import logging import os -from typing import Any, Callable, Iterable, List, Optional, Tuple +from typing import Callable, List, Optional, Tuple from volatility3.framework import constants, interfaces, layers from volatility3.framework.automagic import symbol_cache @@ -40,7 +40,12 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface): name="SQLiteCache", component=symbol_cache.SqliteCache, version=(1, 0, 0), - ) + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] @property @@ -142,11 +147,11 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface): ) for _, banner in banner_list: - vollog.debug(f"Identified banner: {repr(banner)}") - symbol_files = self.banners.get(banner, None) - if symbol_files: - isf_path = symbol_files - vollog.debug(f"Using symbol library: {symbol_files}") + vollog.debug(f"Identified banner: {banner!r}") + symbols_file = self.banners.get(banner, None) + if symbols_file: + isf_path = symbols_file + vollog.debug(f"Using symbol library: {symbols_file}") clazz = self.symbol_class # Set the discovered options path_join = interfaces.configuration.path_join @@ -160,8 +165,31 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface): path_join(config_path, requirement.name, "symbol_mask") ] = layer.address_mask + # Keep track of the existing table names so we know which ones were added + old_table_names = set(context.symbol_space) + # Construct the appropriate symbol table requirement.construct(context, config_path) + + new_table_names = set(context.symbol_space) - old_table_names + # It should add only one symbol table. Ignore the next steps if it doesn't + if len(new_table_names) == 1: + new_table_name = new_table_names.pop() + symbol_table = context.symbol_space[new_table_name] + producer_metadata = symbol_table.producer + vollog.debug( + f"producer_name: {producer_metadata.name}, producer_version: {producer_metadata.version_string}" + ) + + symbol_metadata = symbol_table.metadata + vollog.debug("Types:") + for types_source_dict in symbol_metadata.get_types_sources(): + vollog.debug(f"\t{types_source_dict}") + + vollog.debug("Symbols:") + for symbol_source_dict in symbol_metadata.get_symbols_sources(): + vollog.debug(f"\t{symbol_source_dict}") + break else: vollog.debug(f"Symbol library path not found for: {banner}") 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/__init__.py b/volatility3/framework/configuration/__init__.py index 7a84ee455..7b914cf16 100644 --- a/volatility3/framework/configuration/__init__.py +++ b/volatility3/framework/configuration/__init__.py @@ -2,4 +2,4 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from volatility3.framework.configuration import requirements +from volatility3.framework.configuration import requirements as requirements diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 86e1aac52..b1cc716e5 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -8,13 +8,14 @@ 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 -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type +from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type from urllib import parse, request -from volatility3.framework import constants, interfaces +from volatility3.framework import constants, interfaces, deprecation, versionutils vollog = logging.getLogger(__name__) @@ -111,7 +112,7 @@ class ListRequirement(interfaces.configuration.RequirementInterface): Args: element_type: The (requirement) type of each element within the list - max_elements; The maximum number of acceptable elements this list can contain + max_elements: The maximum number of acceptable elements this list can contain min_elements: The minimum number of acceptable elements this list can contain """ super().__init__(*args, **kwargs) @@ -314,11 +315,11 @@ class TranslationLayerRequirement( def __init__( self, name: str, - description: str = None, + description: Optional[str] = None, default: interfaces.configuration.ConfigSimpleType = None, optional: bool = False, - oses: List = None, - architectures: List = None, + oses: Optional[List] = None, + architectures: Optional[List[str]] = None, ) -> None: """Constructs a Translation Layer Requirement. @@ -526,19 +527,19 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): description: Optional[str] = None, default: bool = False, optional: bool = False, - component: Type[interfaces.configuration.VersionableInterface] = None, + component: Optional[Type[interfaces.configuration.VersionableInterface]] = None, version: Optional[Tuple[int, ...]] = None, ) -> None: + if version is None: + raise TypeError("Version cannot be None") + if component is None: + raise TypeError("Component cannot be None") if description is None: - description = f"Version {'.'.join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet" + description = f"Version {'.'.join(str(x) for x in version)} dependency on {component.__module__}.{component.__name__} unmet" super().__init__( name=name, description=description, default=default, optional=optional ) - if component is None: - raise TypeError("Component cannot be None") self._component: Type[interfaces.configuration.VersionableInterface] = component - if version is None: - raise TypeError("Version cannot be None") self._version = version def unsatisfied( @@ -546,12 +547,12 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): context: interfaces.context.ContextInterface, config_path: str, accumulator: Optional[ - List[interfaces.configuration.VersionableInterface] + Set[interfaces.configuration.VersionableInterface] ] = None, ) -> Dict[str, interfaces.configuration.RequirementInterface]: # Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type config_path = interfaces.configuration.path_join(config_path, self.name) - if not self.matches_required(self._version, self._component.version): + if not versionutils.matches_required(self._version, self._component.version): return {config_path: self} recurse = True @@ -580,7 +581,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): ) if result: - result.update({config_path: self}) + result[config_path] = self return result context.config[interfaces.configuration.path_join(config_path, self.name)] = ( @@ -593,21 +594,22 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): def matches_required( cls, required: Tuple[int, ...], version: Tuple[int, int, int] ) -> bool: - if len(required) > 0 and version[0] != required[0]: - return False - if len(required) > 1 and version[1] < required[1]: - return False - return True + return versionutils.matches_required(required, version) +@deprecation.renamed_class( + deprecated_class_name="PluginRequirement", + removal_date="2026-06-01", + message="PluginRequirement is to be deprecated. Use VersionRequirement instead.", +) class PluginRequirement(VersionRequirement): def __init__( self, name: str, - description: str = None, + description: Optional[str] = None, default: bool = False, optional: bool = False, - plugin: Type[interfaces.plugins.PluginInterface] = None, + plugin: Optional[Type[interfaces.plugins.PluginInterface]] = None, version: Optional[Tuple[int, ...]] = None, ) -> None: super().__init__( @@ -627,7 +629,7 @@ class ModuleRequirement( def __init__( self, name: str, - description: str = None, + description: Optional[str] = None, default: bool = False, architectures: Optional[List[str]] = None, optional: bool = False, @@ -664,9 +666,7 @@ class ModuleRequirement( if value is not None: vollog.log( constants.LOGLEVEL_V, - "TypeError - Module Requirement only accepts string labels: {}".format( - repr(value) - ), + f"TypeError - Module Requirement only accepts string labels: {repr(value)}", ) return {config_path: self} diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 27fae4ba1..689ef122b 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -6,22 +6,25 @@ Stores all the constant values that are generally fixed throughout volatility This includes default scanning block sizes, etc. """ + import enum import os.path import sys import warnings from typing import Callable, Optional -import volatility3.framework.constants.linux -import volatility3.framework.constants.windows +from volatility3.framework.constants import linux as linux +from volatility3.framework.constants import windows as windows from volatility3.framework.constants._version import ( - PACKAGE_VERSION, - VERSION_MAJOR, - VERSION_MINOR, - VERSION_PATCH, - VERSION_SUFFIX, + PACKAGE_VERSION as PACKAGE_VERSION, + VERSION_MAJOR as VERSION_MAJOR, + VERSION_MINOR as VERSION_MINOR, + VERSION_PATCH as VERSION_PATCH, + VERSION_SUFFIX as VERSION_SUFFIX, ) +REQUIRED_PYTHON_VERSION = (3, 8, 0) + PLUGINS_PATH = [ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")), os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")), @@ -37,6 +40,8 @@ SYMBOL_BASEPATHS = [ ISF_EXTENSIONS = [".json", ".json.xz", ".json.gz", ".json.bz2"] """List of accepted extensions for ISF files""" +SYMBOL_SERVER_URL = "http://msdl.microsoft.com/download/symbols" + if hasattr(sys, "frozen") and sys.frozen: # Ensure we include the executable's directory as the base for plugins and symbols PLUGINS_PATH = [ @@ -65,7 +70,11 @@ LOGLEVEL_VVV = 7 LOGLEVEL_VVVV = 6 """Logging level for four levels of detail: -vvvvvv""" -CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3") + +CACHE_PATH = os.path.join( + os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"), + "volatility3", +) """Default path to store cached data""" SQLITE_CACHE_PERIOD = "-3 days" @@ -113,6 +122,9 @@ OFFLINE = False REMOTE_ISF_URL = None # 'http://localhost:8000/banners.json' """Remote URL to query for a list of ISF addresses""" +DOWNLOAD_TIMEOUT = 30 +"""Length of time (in seconds) to wait for another process to download a resource before using it""" + ### # DEPRECATED VALUES ### diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index ce803a687..10dbf3cf1 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,11 +1,11 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 11 # Number of changes that only add to the interface +VERSION_MINOR = 28 # 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 = ( - ".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]]) + ".".join(str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]) + VERSION_SUFFIX ) """The canonical version of the volatility3 package""" diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index ef7d2de98..793ef11e8 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,7 +5,13 @@ Linux-specific values that aren't found in debug symbols """ -from enum import IntEnum, Flag + +import enum +from dataclasses import dataclass + +# Exec argument limits +# Ref: include/uapi/linux/binfmts.h (linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940) +MAX_ARG_STRLEN = 32 * 4096 KERNEL_NAME = "__kernel__" @@ -281,8 +287,54 @@ CAPABILITIES = ( ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1 +# For IFA_* below - Ref: include/net/ipv6.h +IPV6_ADDR_LOOPBACK = 0x0010 +IPV6_ADDR_LINKLOCAL = 0x0020 +IPV6_ADDR_SITELOCAL = 0x0040 +# For inet6_ifaddr - Ref: include/net/if_inet6.h +IFA_HOST = IPV6_ADDR_LOOPBACK +IFA_LINK = IPV6_ADDR_LINKLOCAL +IFA_SITE = IPV6_ADDR_SITELOCAL -class ELF_IDENT(IntEnum): +# Only for kernels < 3.15 when the net_device_flags enum didn't exist +# ref include/uapi/linux/if.h +NET_DEVICE_FLAGS = { + "IFF_UP": 0x1, + "IFF_BROADCAST": 0x2, + "IFF_DEBUG": 0x4, + "IFF_LOOPBACK": 0x8, + "IFF_POINTOPOINT": 0x10, + "IFF_NOTRAILERS": 0x20, + "IFF_RUNNING": 0x40, + "IFF_NOARP": 0x80, + "IFF_PROMISC": 0x100, + "IFF_ALLMULTI": 0x200, + "IFF_MASTER": 0x400, + "IFF_SLAVE": 0x800, + "IFF_MULTICAST": 0x1000, + "IFF_PORTSEL": 0x2000, + "IFF_AUTOMEDIA": 0x4000, + "IFF_DYNAMIC": 0x8000, + "IFF_LOWER_UP": 0x10000, + "IFF_DORMANT": 0x20000, + "IFF_ECHO": 0x40000, +} + + +# Kernels >= 2.6.17. See IF_OPER_* in include/uapi/linux/if.h +class IF_OPER_STATES(enum.Enum): + """RFC 2863 - Network interface operational status""" + + UNKNOWN = 0 + NOTPRESENT = 1 + DOWN = 2 + LOWERLAYERDOWN = 3 + TESTING = 4 + DORMANT = 5 + UP = 6 + + +class ELF_IDENT(enum.IntEnum): """ELF header e_ident indexes""" EI_MAG0 = 0 @@ -296,7 +348,7 @@ class ELF_IDENT(IntEnum): EI_PAD = 8 -class ELF_CLASS(IntEnum): +class ELF_CLASS(enum.IntEnum): """ELF header class types""" ELFCLASSNONE = 0 @@ -319,7 +371,7 @@ PTRACE_O_EXITKILL = 1 << 20 PTRACE_O_SUSPEND_SECCOMP = 1 << 21 -class PT_FLAGS(Flag): +class PT_FLAGS(enum.Flag): "PTrace flags" PT_PTRACED = 0x00001 @@ -353,3 +405,123 @@ NSEC_PER_SEC = 1e9 MODULE_MAXIMUM_CORE_SIZE = 20000000 MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000 MODULE_MINIMUM_SIZE = 4096 + +# Kallsyms +KSYM_NAME_LEN = 512 +NM_TYPES_DESC = { + "a": "Symbol is absolute and doesn't change during linking", + "b": "Symbol in the BSS section, typically holding zero-initialized or uninitialized data", + "c": "Symbol is common, typically holding uninitialized data", + "d": "Symbol is in the initialized data section", + "g": "Symbol is in an initialized data section for small objects", + "i": "Symbol is an indirect reference to another symbol", + "N": "Symbol is a debugging symbol", + "n": "Symbol is in a non-data, non-code, non-debug read-only section", + "p": "Symbol is in a stack unwind section", + "r": "Symbol is in a read only data section", + "s": "Symbol is in an uninitialized or zero-initialized data section for small objects", + "t": "Symbol is in the text (code) section", + "U": "Symbol is undefined", + "u": "Symbol is a unique global symbol", + "V": "Symbol is a weak object, with a default value", + "v": "Symbol is a weak object", + "W": "Symbol is a weak symbol but not marked as a weak object symbol, with a default value", + "w": "Symbol is a weak symbol but not marked as a weak object symbol", + "?": "Symbol type is unknown", +} + +# VMCOREINFO +VMCOREINFO_MAGIC = b"VMCOREINFO\x00" +# Aligned to 4 bytes. See storenote() in kernels < 4.19 or append_kcore_note() in kernels >= 4.19 +VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00" +OSRELEASE_TAG = b"OSRELEASE=" + +ATTRIBUTE_NAME_MAX_SIZE = 255 +""" +In 5.9-rc1+, the Linux kernel limits the READ size of a section bin_attribute name to MODULE_SECT_READ_SIZE: + +- https://elixir.bootlin.com/linux/v6.15-rc4/source/kernel/module/sysfs.c#L106 +- https://github.com/torvalds/linux/commit/11990a5bd7e558e9203c1070fc52fb6f0488e75b + +However, the raw section name loaded from the .ko ELF can in theory be thousands of characters, +and unless we do a NULL terminated search we can't set a perfect value. +""" + + +@dataclass +class TaintFlag: + shift: int + desc: str + when_present: bool + module: bool + + +TAINT_FLAGS = { + "P": TaintFlag( + shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=True, module=True + ), + "G": TaintFlag( + shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=False, module=True + ), + "F": TaintFlag(shift=1 << 1, desc="FORCED_MODULE", when_present=True, module=False), + "S": TaintFlag( + shift=1 << 2, desc="CPU_OUT_OF_SPEC", when_present=True, module=False + ), + "R": TaintFlag(shift=1 << 3, desc="FORCED_RMMOD", when_present=True, module=False), + "M": TaintFlag(shift=1 << 4, desc="MACHINE_CHECK", when_present=True, module=False), + "B": TaintFlag(shift=1 << 5, desc="BAD_PAGE", when_present=True, module=False), + "U": TaintFlag(shift=1 << 6, desc="USER", when_present=True, module=False), + "D": TaintFlag(shift=1 << 7, desc="DIE", when_present=True, module=False), + "A": TaintFlag( + shift=1 << 8, desc="OVERRIDDEN_ACPI_TABLE", when_present=True, module=False + ), + "W": TaintFlag(shift=1 << 9, desc="WARN", when_present=True, module=False), + "C": TaintFlag(shift=1 << 10, desc="CRAP", when_present=True, module=True), + "I": TaintFlag( + shift=1 << 11, desc="FIRMWARE_WORKAROUND", when_present=True, module=False + ), + "O": TaintFlag(shift=1 << 12, desc="OOT_MODULE", when_present=True, module=True), + "E": TaintFlag( + shift=1 << 13, desc="UNSIGNED_MODULE", when_present=True, module=True + ), + "L": TaintFlag(shift=1 << 14, desc="SOFTLOCKUP", when_present=True, module=False), + "K": TaintFlag(shift=1 << 15, desc="LIVEPATCH", when_present=True, module=True), + "X": TaintFlag(shift=1 << 16, desc="AUX", when_present=True, module=True), + "T": TaintFlag(shift=1 << 17, desc="RANDSTRUCT", when_present=True, module=True), + "N": TaintFlag(shift=1 << 18, desc="TEST", when_present=True, module=True), +} +"""Flags used to taint kernel and modules, for debugging purposes. + +Map based on 6.12-rc5. + +Documentation : + - https://www.kernel.org/doc/Documentation/admin-guide/sysctl/kernel.rst#:~:text=guide/sysrq.rst.-,tainted,-%3D%3D%3D%3D%3D%3D%3D%0A%0ANon%2Dzero%20if + - https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst#:~:text=More%20detailed%20explanation%20for%20tainting + - taint_flag kernel struct + - taint_flags kernel constant +""" + +## ELF related constants + +# Elf Symbol Bindings +STB_LOCAL = 0 +STB_GLOBAL = 1 + +# Elf Symbol Types +STT_NOTYPE = 0 +STT_OBJECT = 1 +STT_FUNC = 2 +STT_SECTION = 3 + +# Elf Section Types +SHT_NULL = 0 +SHT_PROGBITS = 1 +SHT_SYMTAB = 2 +SHT_STRTAB = 3 +SHT_RELA = 4 +SHT_NOTE = 7 + +# Elf Section Attributes +SHF_WRITE = 1 +SHF_ALLOC = 2 +SHF_EXECINSTR = 4 diff --git a/volatility3/framework/constants/windows/__init__.py b/volatility3/framework/constants/windows/__init__.py index 7face984a..b08713cc9 100644 --- a/volatility3/framework/constants/windows/__init__.py +++ b/volatility3/framework/constants/windows/__init__.py @@ -10,3 +10,23 @@ KERNEL_MODULE_NAMES = ["ntkrnlmp", "ntkrnlpa", "ntkrpamp", "ntoskrnl"] """The list of names that kernel modules can have within the windows OS""" PE_MAX_EXTRACTION_SIZE = 1024 * 1024 * 256 + +""" +The following constants represent the layout of the Low Stub which exists only on x64 machines with no virtualization/emulation, +responsible for transitioning from Real Mode(16 bit) to Protected Mode(32 bit) and Long Mode(64 bit) on boot/return from sleep. +Contains offsets to fields and structures within the undocumented structure _PROCESSOR_START_BLOCK. +Here's a reference: https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/inc/amd64.h#L3334 +""" +# Expected signature for validation, constructed from: +# PROCESSOR_START_BLOCK->Jmp->OpCode | PROCESSOR_START_BLOCK->Jmp->Offset | PROCESSOR_START_BLOCK->CompletionFlag +JMP_AND_COMPLETION_SIGNATURE = 0x00000001000600E9 + +# Address of LmTarget (Long Mode target) +PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = ( + 0x70 # PROCESSOR_START_BLOCK->LmTarget, PVOID 8 bytes +) + +# CR3 register within structures describing initial processor state to be started +PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes + +MAX_PID = 0xFFFFFFFC diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 6961d9328..125f41f8a 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -8,10 +8,12 @@ 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 -from typing import Callable, Iterable, List, Optional, Set, Tuple, Union +import re +from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union from volatility3.framework import constants, interfaces, symbols, exceptions from volatility3.framework.objects import templates @@ -129,7 +131,7 @@ class Context(interfaces.context.ContextInterface): object_info=interfaces.objects.ObjectInformation( layer_name=layer_name, offset=offset, - native_layer_name=native_layer_name, + native_layer_name=native_layer_name or layer_name, size=object_template.size, ), ) @@ -229,7 +231,7 @@ class Module(interfaces.context.ModuleInterface): def object( self, object_type: str, - offset: int = None, + offset: Optional[int] = None, native_layer_name: Optional[str] = None, absolute: bool = False, **kwargs, @@ -286,7 +288,7 @@ class Module(interfaces.context.ModuleInterface): symbol_name: Name of the symbol (within the module) to construct native_layer_name: Name of the layer in which constructed objects are made (for pointers) absolute: whether the symbol's address is absolute or relative to the module - object_type: Override for the type from the symobl to use (or if the symbol type is missing) + object_type: Override for the type from the symbol to use (or if the symbol type is missing) """ if constants.BANG not in symbol_name: symbol_name = self.symbol_table_name + constants.BANG + symbol_name @@ -337,7 +339,7 @@ class Module(interfaces.context.ModuleInterface): ) @property - def symbols(self): + def symbols(self) -> Iterable[str]: return self.context.symbol_space[self.symbol_table_name].symbols get_symbol = get_module_wrapper("get_symbol") @@ -356,7 +358,7 @@ class SizedModule(Module): return size or 0 @property # type: ignore # FIXME: mypy #5107 - @functools.lru_cache() + @functools.lru_cache def hash(self) -> str: """Hashes the module for equality checks. @@ -386,10 +388,8 @@ class ModuleCollection(interfaces.context.ModuleContainer): """Class to contain a collection of SizedModules and reason about their contents.""" - def __init__( - self, modules: Optional[List[interfaces.context.ModuleInterface]] = None - ) -> None: - self._prefix_count = {} + def __init__(self, modules: Optional[List[SizedModule]] = None) -> None: + self._modules: Dict[str, SizedModule] = {} super().__init__(modules) def deduplicate(self) -> "ModuleCollection": @@ -402,20 +402,19 @@ class ModuleCollection(interfaces.context.ModuleContainer): new_modules = [] seen: Set[str] = set() for mod in self._modules: - if mod.hash not in seen or mod.size == 0: + if self._modules[mod].hash not in seen or self._modules[mod].size == 0: new_modules.append(mod) - seen.add(mod.hash) # type: ignore # FIXME: mypy #5107 + seen.add(self._modules[mod].hash) return ModuleCollection(new_modules) def free_module_name(self, prefix: str = "module") -> str: """Returns an unused module name""" - if prefix not in self._prefix_count: - self._prefix_count[prefix] = 1 + existing_names = [name for name in self if re.match(rf"^{prefix}[0-9]*$", name)] + if not existing_names: return prefix - count = self._prefix_count[prefix] + count = len(existing_names) while prefix + str(count) in self: count += 1 - self._prefix_count[prefix] = count return prefix + str(count) @property diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py new file mode 100644 index 000000000..581647922 --- /dev/null +++ b/volatility3/framework/deprecation.py @@ -0,0 +1,147 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# This file contains the Deprecation class used to deprecate methods in an orderly manner + +import warnings +import functools +import inspect + +from typing import Callable, Tuple + +from volatility3.framework import interfaces, exceptions, versionutils + + +def method_being_removed(message: str, removal_date: str): + """A decorator for marking functions as being removed in the future and without a replacement. + Callers to this function should explicitly list the API paths that should be used instead. + + Args: + message: A message added to the standard deprecation warning. Should include the replacement API paths + removal_date: A YYYY-MM-DD formatted date of when the function will be removed from the framework + """ + + def decorator(deprecated_func): + @functools.wraps(deprecated_func) + def wrapper(*args, **kwargs): + warnings.warn( + f"This API ({deprecated_func.__module__}.{deprecated_func.__qualname__}) will be removed in the first release after {removal_date}. {message}", + FutureWarning, + ) + return deprecated_func(*args, **kwargs) + + return wrapper + + return decorator + + +def deprecated_method( + replacement: Callable, + removal_date: str, + replacement_version: Tuple[int, int, int] = None, + additional_information: str = "", +): + """A decorator for marking functions as deprecated. + + Args: + replacement: The replacement function overriding the deprecated API, in the form of a Callable (typically a method) + removal_date: A YYYY-MM-DD formatted date of when the function will be removed from the framework + replacement_version: The "replacement" base class version that the deprecated method expects before proxying to it. This implies that "replacement" is a method from a class that inherits from VersionableInterface. + additional_information: Information appended at the end of the deprecation message + """ + + def decorator(deprecated_func): + @functools.wraps(deprecated_func) + def wrapper(*args, **kwargs): + nonlocal replacement, replacement_version, additional_information + # Prevent version mismatches between deprecated (proxy) methods and the ones they proxy + if ( + replacement_version is not None + and callable(replacement) + and hasattr(replacement, "__self__") + ): + replacement_base_class = replacement.__self__ + + # Verify that the base class inherits from VersionableInterface + if inspect.isclass(replacement_base_class) and issubclass( + replacement_base_class, + interfaces.configuration.VersionableInterface, + ): + # SemVer check + if not versionutils.matches_required( + replacement_version, replacement_base_class.version + ): + raise exceptions.VersionMismatchException( + deprecated_func, + replacement_base_class, + replacement_version, + "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}' + warnings.warn(deprecation_msg, FutureWarning) + # Return the wrapped function with its original arguments + return deprecated_func(*args, **kwargs) + + return wrapper + + return decorator + + +def renamed_class(deprecated_class_name: str, message: str, removal_date: str): + """A decorator for marking classes as being renamed and removed in the future. + Callers to this function should explicitly update to use the other plugins instead. + + Args: + deprecated_class_name: The name of the class being deprecated + message: A message added to the standard deprecation warning. Should include the replacement API paths + removal_date: A YYYY-MM-DD formatted date of when the function will be removed from the framework + """ + + def decorator(replacement_func): + @functools.wraps(replacement_func) + def wrapper(*args, **kwargs): + warnings.warn( + f"This plugin ({deprecated_class_name}) has been renamed and will be removed in the first release after {removal_date}. {message}", + FutureWarning, + ) + return replacement_func(*args, **kwargs) + + return wrapper + + return decorator + + +class PluginRenameClass: + """Class to move all classmethod invocations (for when a plugin has been moved)""" + + def __init_subclass__(cls, replacement_class, removal_date, **kwargs): + deprecated_class_name = f"{cls.__module__}.{cls.__qualname__}" + super().__init_subclass__(**kwargs) + for attr, value in replacement_class.__dict__.items(): + if isinstance(value, classmethod) and attr != "get_requirements": + setattr( + cls, + attr, + classmethod( + renamed_class( + deprecated_class_name=deprecated_class_name, + removal_date=removal_date, + message=f"Please ensure all method calls to this plugin are replaced with calls to {replacement_class.__module__}.{replacement_class.__qualname__}", + )(value.__func__) + ), + ) + else: + if attr == "run": + setattr( + cls, + attr, + method_being_removed( + removal_date=removal_date, + message=f"This plugin has been renamed, please call {replacement_class.__module__}.{replacement_class.__qualname__} rather than {deprecated_class_name}.", + )(value), + ) + elif not attr.startswith("__"): + setattr(cls, attr, value) + return super(PluginRenameClass).__init_subclass__(**kwargs) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index c44fb4f2e..3b70c5c29 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -8,7 +8,8 @@ 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 Dict, Optional + +from typing import Callable, Dict, Optional, Tuple from volatility3.framework import interfaces @@ -130,3 +131,35 @@ class OfflineException(VolatilityException): class RenderException(VolatilityException): """Thrown if there is an error during rendering""" + + +class LinuxPageCacheException(VolatilityException): + """Thrown if there is an error during Linux Page Cache processing""" + + +class VersionMismatchException(VolatilityException): + """Thrown if a version mismatch has been encountered between two components.""" + + def __init__( + self, + source_component: Callable, + target_component: interfaces.configuration.VersionableInterface, + target_version: Tuple[int, int, int], + failure_reason: str = None, + *args, + ): + """ + Args: + source_component: The component that required the target component + target_component: The component that is required. Must inherit from interfaces.configuration.VersionableInterface + target_version: The version of the target component that was required, and ultimately was not satisfied + failure_reason: A detailed failure reason to enhance debugging and bug tracking + """ + super().__init__(*args) + self.source_component = source_component + self.target_component = target_component + self.target_version = target_version + 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." diff --git a/volatility3/framework/interfaces/__init__.py b/volatility3/framework/interfaces/__init__.py index 51d81d63a..fd6b1e062 100644 --- a/volatility3/framework/interfaces/__init__.py +++ b/volatility3/framework/interfaces/__init__.py @@ -13,12 +13,12 @@ components of volatility to write plugins. # This will also avoid namespace issues, because people can use interfaces.layers to # avoid clashing with the layers package from volatility3.framework.interfaces import ( - renderers, - configuration, - context, - layers, - objects, - plugins, - symbols, - automagic, + renderers as renderers, + configuration as configuration, + context as context, + layers as layers, + objects as objects, + plugins as plugins, + symbols as symbols, + automagic as automagic, ) diff --git a/volatility3/framework/interfaces/automagic.py b/volatility3/framework/interfaces/automagic.py index 0867b1608..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 @@ -42,7 +43,7 @@ class AutomagicInterface( priority = 10 """An ordering to indicate how soon this automagic should be run""" - exclusion_list = [] + exclusion_list: List[str] = [] """A list of plugin categories (typically operating systems) which the plugin will not operate on""" def __init__( diff --git a/volatility3/framework/interfaces/configuration.py b/volatility3/framework/interfaces/configuration.py index da0a4556c..b6f4f889c 100644 --- a/volatility3/framework/interfaces/configuration.py +++ b/volatility3/framework/interfaces/configuration.py @@ -53,7 +53,7 @@ ConfigSimpleType = Optional[Union[SimpleTypes, List[SimpleTypes]]] def path_join(*args) -> str: """Joins configuration paths together.""" # If a path element (particularly the first) is empty, then remove it from the list - args = tuple([arg for arg in args if arg]) + args = tuple(arg for arg in args if arg) return CONFIG_SEPARATOR.join(args) @@ -82,7 +82,7 @@ class HierarchicalDict(collections.abc.Mapping): def __init__( self, - initial_dict: Dict[str, "SimpleTypeRequirement"] = None, + initial_dict: Optional[Dict[str, "SimpleTypeRequirement"]] = None, separator: str = CONFIG_SEPARATOR, ) -> None: """ @@ -94,7 +94,7 @@ class HierarchicalDict(collections.abc.Mapping): raise TypeError(f"Separator must be a one character string: {separator}") self._separator = separator self._data: Dict[str, ConfigSimpleType] = {} - self._subdict: Dict[str, "HierarchicalDict"] = {} + self._subdict: Dict[str, HierarchicalDict] = {} if isinstance(initial_dict, str): initial_dict = json.loads(initial_dict) if isinstance(initial_dict, dict): @@ -182,9 +182,7 @@ class HierarchicalDict(collections.abc.Mapping): else: if not isinstance(value, HierarchicalDict): raise TypeError( - "HierarchicalDicts can only store HierarchicalDicts within their structure: {}".format( - type(value) - ) + f"HierarchicalDicts can only store HierarchicalDicts within their structure: {type(value)}" ) self._subdict[key] = value @@ -330,7 +328,7 @@ class RequirementInterface(metaclass=ABCMeta): def __init__( self, name: str, - description: str = None, + description: Optional[str] = None, default: ConfigSimpleType = None, optional: bool = False, ) -> None: @@ -498,9 +496,7 @@ class SimpleTypeRequirement(RequirementInterface): if not isinstance(value, self.instance_type): vollog.log( constants.LOGLEVEL_V, - "TypeError - {} requirements only accept {} type: {}".format( - self.name, self.instance_type.__name__, repr(value) - ), + f"TypeError - {self.name} requirements only accept {self.instance_type.__name__} type: {repr(value)}", ) return {config_path: self} return {} @@ -622,7 +618,7 @@ class ConstructableRequirementInterface(RequirementInterface): self, context: "interfaces.context.ContextInterface", config_path: str, - requirement_dict: Dict[str, object] = None, + requirement_dict: Optional[Dict[str, object]] = None, ) -> Optional["interfaces.objects.ObjectInterface"]: """Constructs the class, handing args and the subrequirements as parameters to __init__""" @@ -656,6 +652,7 @@ class ConstructableRequirementInterface(RequirementInterface): class ConfigurableRequirementInterface(RequirementInterface): """Simple Abstract class to provide build_required_config.""" + @abstractmethod def build_configuration( self, context: "interfaces.context.ContextInterface", @@ -775,17 +772,16 @@ class ConfigurableInterface(metaclass=ABCMeta): str: The newly generated full configuration path """ random_config_dict = "".join( - random.SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(8) + random.SystemRandom().choices(string.ascii_uppercase + string.digits, k=8) ) new_config_path = path_join(base_config_path, random_config_dict) # TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in # This should check that each k corresponds to a requirement and each v is of the appropriate type # This would require knowledge of the new configurable itself to verify, and they should do validation in the - # constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type + # constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a basic type for k, v in kwargs.items(): - if not isinstance(v, (int, str, bool, float, bytes)): + if not isinstance(v, BasicTypes): raise TypeError( "Config values passed to make_subconfig can only be simple types" ) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 8b5e816e8..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 @@ -85,7 +86,7 @@ class ContextInterface(metaclass=ABCMeta): object_type: Union[str, "interfaces.objects.Template"], layer_name: str, offset: int, - native_layer_name: str = None, + native_layer_name: Optional[str] = None, **arguments, ) -> "interfaces.objects.ObjectInterface": """Object factory, takes a context, symbol, offset and optional @@ -114,6 +115,7 @@ class ContextInterface(metaclass=ABCMeta): """ return copy.deepcopy(self) + @abstractmethod def module( self, module_name: str, @@ -232,7 +234,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): def object( self, object_type: str, - offset: int = None, + offset: Optional[int] = None, native_layer_name: Optional[str] = None, absolute: bool = False, **kwargs, @@ -266,7 +268,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): symbol_name: The name of a symbol (that must be present in the module's symbol table). The symbol's associated type will be used to construct an object at the symbol's offset. native_layer_name: The native layer for objects that reference a different layer (if not the default provided during module construction) absolute: A boolean specifying whether the offset is absolute within the layer, or relative to the start of the module - object_type: Override for the type from the symobl to use (or if the symbol type is missing) + object_type: Override for the type from the symbol to use (or if the symbol type is missing) Returns: The constructed object @@ -277,27 +279,37 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): symbol = self.get_symbol(name) return self.offset + symbol.address + @abstractmethod def get_type(self, name: str) -> "interfaces.objects.Template": """Returns a type from the module's symbol table.""" + @abstractmethod def get_symbol(self, name: str) -> "interfaces.symbols.SymbolInterface": """Returns a symbol object from the module's symbol table.""" + @abstractmethod def get_enumeration(self, name: str) -> "interfaces.objects.Template": """Returns an enumeration from the module's symbol table.""" + @abstractmethod def has_type(self, name: str) -> bool: """Determines whether a type is present in the module's symbol table.""" + @abstractmethod def has_symbol(self, name: str) -> bool: """Determines whether a symbol is present in the module's symbol table.""" + @abstractmethod def has_enumeration(self, name: str) -> bool: """Determines whether an enumeration is present in the module's symbol table.""" - def symbols(self) -> List: - """Lists the symbols contained in the symbol table for this module""" + @property + @abstractmethod + def symbols(self) -> Iterable[str]: + """Returns an iterable of the symbols contained in the symbol table for this module""" + raise NotImplementedError("Symbols property has not been implemented.") + @abstractmethod def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]: """Returns the symbols within table_name (or this module if not specified) that live at the specified absolute offset provided.""" @@ -343,6 +355,7 @@ class ModuleContainer(collections.abc.Mapping): def __iter__(self): return iter(self._modules) + @abstractmethod def free_module_name(self, prefix: str = "module") -> str: """Returns an unused table name to ensure no collision occurs when inserting a symbol table.""" diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index e2a68780a..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 @@ -136,7 +137,7 @@ class DataLayerInterface( def minimum_address(self) -> int: """Returns the minimum valid address of the space.""" - @property + @functools.cached_property def address_mask(self) -> int: """Returns a mask which encapsulates all the active bits of an address for this layer.""" @@ -188,7 +189,6 @@ class DataLayerInterface( the object unreadable (exceptions will be thrown using a DataLayer after destruction) """ - pass @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -211,7 +211,7 @@ class DataLayerInterface( context: interfaces.context.ContextInterface, scanner: ScannerInterface, progress_callback: constants.ProgressCallback = None, - sections: Iterable[Tuple[int, int]] = None, + sections: Optional[Iterable[Tuple[int, int]]] = None, ) -> Iterable[Any]: """Scans a Translation layer by chunk. @@ -361,9 +361,7 @@ class DataLayerInterface( data += self.context.layers[layer_name].read(address, chunk_size) except exceptions.InvalidAddressException: vollog.debug( - "Invalid address in layer {} found scanning {} at address {:x}".format( - layer_name, self.name, address - ) + f"Invalid address in layer {layer_name} found scanning {self.name} at address {address:x}" ) if len(data) > scanner.chunk_size + scanner.overlap: @@ -681,7 +679,7 @@ class LayerContainer(collections.abc.Mapping): if name in self._layers[layer].dependencies: raise exceptions.LayerException( self._layers[layer].name, - f"Layer {self._layers[layer].name} is depended upon by {layer}", + f"Layer {name} is depended upon by {layer}", ) # Otherwise, wipe out the layer self._layers[name].destroy() @@ -721,7 +719,7 @@ class LayerContainer(collections.abc.Mapping): raise NotImplementedError("Cycle checking has not yet been implemented") -class DummyProgress(object): +class DummyProgress: """A class to emulate Multiprocessing/threading Value objects.""" def __init__(self) -> None: diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 51d25510d..1228c4d0a 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -3,10 +3,12 @@ # """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, Optional @@ -52,7 +54,8 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(ReadOnlyMapping): +@dataclasses.dataclass +class ObjectInformation: """Contains common information useful/pertinent only to an individual object (like an instance) @@ -63,35 +66,20 @@ class ObjectInformation(ReadOnlyMapping): in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification. """ - def __init__( - self, - layer_name: str, - offset: int, - member_name: Optional[str] = None, - parent: Optional["ObjectInterface"] = None, - native_layer_name: Optional[str] = None, - size: Optional[int] = None, - ): - """Constructs a container for basic information about an object. + layer_name: str + offset: int + native_layer_name: str + member_name: Optional[str] = None + parent: Optional["ObjectInterface"] = None + size: Optional[int] = None - Args: - layer_name: Layer from which the data for the object will be read - offset: Offset within the layer at which the data for the object will be read - member_name: If the object was accessed as a member of a parent object, this was the name used to access it - parent: If the object was accessed as a member of a parent object, this is the parent object - native_layer_name: If this object references other objects (such as a pointer), what layer those objects live in - size: The size that the whole structure consumes in bytes - """ - super().__init__( - { - "layer_name": layer_name, - "offset": offset, - "member_name": member_name, - "parent": parent, - "native_layer_name": native_layer_name or layer_name, - "size": size, - } - ) + def __getitem__(self, key): + if key in self: + return getattr(self, key) + raise KeyError(f"No {key} present in ObjectInformation") + + def __contains__(self, key): + return key in [field.name for field in dataclasses.fields(self)] class ObjectInterface(metaclass=abc.ABCMeta): @@ -133,7 +121,7 @@ class ObjectInterface(metaclass=abc.ABCMeta): def __getattr__(self, attr: str) -> Any: """Method for ensuring volatility members can be returned.""" - raise AttributeError + raise AttributeError(f"Unable to find {attr} for type {type(self)}") @property def vol(self) -> ReadOnlyMapping: @@ -183,7 +171,7 @@ class ObjectInterface(metaclass=abc.ABCMeta): offset=self.vol.offset, member_name=self.vol.member_name, parent=self.vol.parent, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=object_template.size, ) return object_template(context=self._context, object_info=object_info) @@ -374,6 +362,7 @@ class Template: f"{self.__class__.__name__} object has no attribute {attr}" ) + @abc.abstractmethod def __call__( self, context: "interfaces.context.ContextInterface", diff --git a/volatility3/framework/interfaces/plugins.py b/volatility3/framework/interfaces/plugins.py index 697e4cdc3..f763815a6 100644 --- a/volatility3/framework/interfaces/plugins.py +++ b/volatility3/framework/interfaces/plugins.py @@ -46,7 +46,7 @@ class FileHandlerInterface(io.RawIOBase): def preferred_filename(self, filename: str): """Sets the preferred filename""" if self.closed: - raise IOError("FileHandler name cannot be changed once closed") + raise OSError("FileHandler name cannot be changed once closed") if not isinstance(filename, str): raise TypeError("FileHandler preferred filenames must be strings") if os.path.sep in filename: @@ -59,14 +59,14 @@ class FileHandlerInterface(io.RawIOBase): @staticmethod def sanitize_filename(filename: str) -> str: - """Sanititizes the filename to ensure only a specific whitelist of characters is allowed through""" - allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]{}!$%^:#~?<>,|" + """Sanititizes the filename to ensure only a specific allow list of characters is allowed through""" + allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]{}!$%^#~," result = "" for char in filename: if char in allowed: result += char else: - result += "?" + result += "_" # change unwanted chars to an underscore return result def __enter__(self): diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index b13de1834..3e9afaf21 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -10,7 +10,8 @@ suitable output. """ import datetime -from abc import abstractmethod, ABCMeta +import warnings +from abc import ABCMeta, abstractmethod from collections import abc from typing import ( Any, @@ -20,21 +21,68 @@ from typing import ( List, NamedTuple, Optional, - TypeVar, - Type, Tuple, + Type, + TypeVar, Union, ) +from typing import Dict + +from volatility3.framework import interfaces + + +class BasicType: + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return str(self) + + +class BaseAbsentValue: + """Class that represents values which are not present for some reason.""" + + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + + +class Column(NamedTuple): + name: str + type: Any -Column = NamedTuple("Column", [("name", str), ("type", Any)]) RenderOption = Any +T = TypeVar("T") + + +class TypeRendererInterface: + type = T + + def __init__( + self, func: Optional[Callable] = None, options: Optional[Dict[str, Any]] = None + ): + self._options = options or {} + setattr(self, "render", func) + + @property + def options(self): + return self._options + + def render(self, data: Union[T, BaseAbsentValue]) -> Any: + """Renders a specific datatype""" + return "" + + def __call__(self, data: Union[T, BaseAbsentValue]) -> Any: + """Shortcut for render""" + return self.render(data) + class Renderer(metaclass=ABCMeta): """Class that defines the interface that all output renderers must support.""" + _type_renderers: Dict[Union[Type, str], Callable] + def __init__(self, options: Optional[List[RenderOption]] = None) -> None: """Accepts an options object to configure the renderers.""" # FIXME: Once the config option objects are in place, put the _type_check in place @@ -98,11 +146,7 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta): """ -class BaseAbsentValue(object): - """Class that represents values which are not present for some reason.""" - - -class Disassembly(object): +class Disassembly(BasicType): """A class to indicate that the bytes provided should be disassembled (based on the architecture)""" @@ -111,6 +155,10 @@ class Disassembly(object): def __init__( self, data: bytes, offset: int = 0, architecture: str = "intel64" ) -> None: + warnings.warn( + "interfaces.renderers.Disassembly is now renderers.Disassembly", + FutureWarning, + ) self.data = data self.architecture = None if architecture in self.possible_architectures: @@ -119,6 +167,10 @@ class Disassembly(object): raise TypeError("Offset must be an integer type") self.offset = offset + def __str__(self) -> str: + """Fallback method of rendering""" + return str(self.data) + # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) @@ -131,13 +183,13 @@ BaseTypes = Union[ Type[bytes], Type[datetime.datetime], Type[BaseAbsentValue], - Type[Disassembly], + Type[BasicType], ] ColumnsType = List[Tuple[str, BaseTypes]] VisitorSignature = Callable[[TreeNode, _Type], _Type] -class TreeGrid(object, metaclass=ABCMeta): +class TreeGrid(metaclass=ABCMeta): """Class providing the interface for a TreeGrid (which contains TreeNodes) The structure of a TreeGrid is designed to maintain the structure of the tree in a single object. @@ -150,16 +202,15 @@ class TreeGrid(object, metaclass=ABCMeta): and to create cycles. """ - base_types: ClassVar[Tuple] = ( - int, - str, - float, - bytes, - datetime.datetime, - Disassembly, - ) + # TODO: Figure out why this isn't just BaseTypes (which includes AbsentValues' + base_types: ClassVar[Tuple] = (int, str, float, bytes, datetime.datetime, BasicType) - def __init__(self, columns: ColumnsType, generator: Generator) -> None: + def __init__( + self, + columns: ColumnsType, + generator: Generator, + context: Optional["interfaces.context.ContextInterface"] = None, + ) -> None: """Constructs a TreeGrid object using a specific set of columns. The TreeGrid itself is a root element, that can have children but no values. @@ -170,6 +221,15 @@ class TreeGrid(object, metaclass=ABCMeta): columns: A list of column tuples made up of (name, type). generator: An iterable containing row for a tree grid, each row contains a indent level followed by the values for each column in order. """ + self._context = context + + @property + def context(self) -> Optional["interfaces.context.ContextInterface"]: + """Returns the context value for the tree grid (to retrieve data items) + + This is a property to ensure the renderers don't try changing the context for any reason + """ + return self._context @staticmethod @abstractmethod @@ -179,7 +239,7 @@ class TreeGrid(object, metaclass=ABCMeta): @abstractmethod def populate( self, - function: VisitorSignature = None, + function: Optional[VisitorSignature] = None, initial_accumulator: Any = None, fail_on_errors: bool = True, ) -> Optional[Exception]: @@ -231,7 +291,7 @@ class TreeGrid(object, metaclass=ABCMeta): node: Optional[TreeNode], function: VisitorSignature, initial_accumulator: _Type, - sort_key: ColumnSortKey = None, + sort_key: Optional[ColumnSortKey] = None, ) -> None: """Visits all the nodes in a tree, calling function on each one. diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index b645f5cd1..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 @@ -10,7 +11,6 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Type from volatility3.framework import constants, exceptions, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import configuration, objects -from volatility3.framework.interfaces.configuration import RequirementInterface class SymbolInterface: @@ -122,7 +122,7 @@ class BaseSymbolTableInterface: @property def symbols(self) -> Iterable[str]: - """Returns an iterator of the Symbol names.""" + """Returns an iterable of the available symbol names.""" raise NotImplementedError( "Abstract property symbols not implemented by subclass." ) @@ -131,7 +131,7 @@ class BaseSymbolTableInterface: @property def types(self) -> Iterable[str]: - """Returns an iterator of the Symbol type names.""" + """Returns an iterable of the available symbol type names.""" raise NotImplementedError( "Abstract property types not implemented by subclass." ) @@ -149,7 +149,7 @@ class BaseSymbolTableInterface: @property def enumerations(self) -> Iterable[Any]: - """Returns an iterator of the Enumeration names.""" + """Returns an iterable of the available enumerations.""" raise NotImplementedError( "Abstract property enumerations not implemented by subclass." ) @@ -250,13 +250,13 @@ class BaseSymbolTableInterface: def clear_symbol_cache(self) -> None: """Clears the symbol cache of this symbol table.""" - pass class SymbolSpaceInterface(collections.abc.Mapping): """An interface for the container that holds all the symbol-containing tables for use within a context.""" + @abstractmethod def free_table_name(self, prefix: str = "layer") -> str: """Returns an unused table name to ensure no collision occurs when inserting a symbol table.""" @@ -347,7 +347,7 @@ class SymbolTableInterface( return config @classmethod - def get_requirements(cls) -> List[RequirementInterface]: + def get_requirements(cls) -> List[configuration.RequirementInterface]: return super().get_requirements() + [ requirements.IntRequirement( name="symbol_mask", @@ -366,6 +366,7 @@ class NativeTableInterface(BaseSymbolTableInterface): @property def symbols(self) -> Iterable[str]: + """Returns an iterable of the available symbol names.""" return [] def get_enumeration(self, name: str) -> objects.Template: @@ -374,11 +375,17 @@ class NativeTableInterface(BaseSymbolTableInterface): ) @property - def enumerations(self) -> Iterable[str]: + def enumerations(self) -> Iterable[Any]: + """Returns an iterable of the available enumerations.""" + return [] + + @property + def types(self) -> Iterable[str]: + """Returns an iterable of the available symbol type names.""" return [] -class MetadataInterface(object): +class MetadataInterface: """Interface for accessing metadata stored within a symbol table.""" def __init__(self, json_data: Dict) -> None: 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/crash.py b/volatility3/framework/layers/crash.py index 3cfc0a25b..a5b25d178 100644 --- a/volatility3/framework/layers/crash.py +++ b/volatility3/framework/layers/crash.py @@ -1,7 +1,6 @@ # This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import contextlib import logging import struct from typing import Tuple, Optional @@ -138,7 +137,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer): ulong_bitmap_array = summary_header.get_buffer_long() # outer_index points to a 32 bits array inside a list of arrays, # each bit indicating a page mapping state - for outer_index in range(0, ulong_bitmap_array.vol.count): + for outer_index in range(ulong_bitmap_array.vol.count): ulong_bitmap = ulong_bitmap_array[outer_index] # All pages in this 32 bits array are mapped (speedup iteration process) if ulong_bitmap == 0xFFFFFFFF: @@ -166,7 +165,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer): seg_first_bit = None # Some pages in this 32 bits array are mapped and some aren't else: - for inner_bit_position in range(0, 32): + for inner_bit_position in range(32): current_bit = outer_index * 32 + inner_bit_position page_mapped = ulong_bitmap & (1 << inner_bit_position) if page_mapped: @@ -220,9 +219,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer): for idx, (start_position, mapped_offset, length, _) in enumerate(segments): vollog.log( constants.LOGLEVEL_VVVV, - "Segment {}: Position {:#x} Offset {:#x} Length {:#x}".format( - idx, start_position, mapped_offset, length - ), + f"Segment {idx}: Position {start_position:#x} Offset {mapped_offset:#x} Length {length:#x}", ) self._segments = segments diff --git a/volatility3/framework/layers/elf.py b/volatility3/framework/layers/elf.py index 81f3c3634..5777981cb 100644 --- a/volatility3/framework/layers/elf.py +++ b/volatility3/framework/layers/elf.py @@ -6,7 +6,7 @@ import struct from typing import Optional from volatility3.framework import exceptions, interfaces, constants -from volatility3.framework.constants.linux import ELF_CLASS +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.layers import segmented from volatility3.framework.symbols import intermed @@ -23,7 +23,7 @@ class Elf64Layer(segmented.SegmentedLayer): _header_struct = struct.Struct(" int: """Page shift for the intel memory layers.""" return cls._page_size_in_bits @classproperty - @functools.lru_cache() + @functools.lru_cache def page_size(cls) -> int: """Page size for the intel memory layers. @@ -83,25 +91,25 @@ class Intel(linear.LinearlyMappedLayer): return 1 << cls._page_size_in_bits @classproperty - @functools.lru_cache() + @functools.lru_cache def page_mask(cls) -> int: """Page mask for the intel memory layers.""" return ~(cls.page_size - 1) @classproperty - @functools.lru_cache() + @functools.lru_cache def bits_per_register(cls) -> int: """Returns the bits_per_register to determine the range of an IntelTranslationLayer.""" return cls._bits_per_register @classproperty - @functools.lru_cache() + @functools.lru_cache def minimum_address(cls) -> int: return 0 @classproperty - @functools.lru_cache() + @functools.lru_cache def maximum_address(cls) -> int: return (1 << cls._maxvirtaddr) - 1 @@ -115,7 +123,6 @@ class Intel(linear.LinearlyMappedLayer): high_mask = (1 << (high_bit + 1)) - 1 low_mask = (1 << low_bit) - 1 mask = high_mask ^ low_mask - # print(high_bit, low_bit, bin(mask), bin(value)) return value & mask @staticmethod @@ -129,7 +136,7 @@ class Intel(linear.LinearlyMappedLayer): return bool(entry & (1 << 6)) def canonicalize(self, addr: int) -> int: - """Canonicalizes an address by performing an appropiate sign extension on the higher addresses""" + """Canonicalizes an address by performing an appropriate sign extension on the higher addresses""" if self._bits_per_register <= self._maxvirtaddr: return addr & self.address_mask elif addr < (1 << self._maxvirtaddr - 1): @@ -137,7 +144,7 @@ class Intel(linear.LinearlyMappedLayer): return self._mask(addr, self._maxvirtaddr, 0) + self._canonical_prefix def decanonicalize(self, addr: int) -> int: - """Removes canonicalization to ensure an adress fits within the correct range if it has been canonicalized + """Removes canonicalization to ensure an address fits within the correct range if it has been canonicalized This will produce an address outside the range if the canonicalization is incorrect """ @@ -152,7 +159,7 @@ class Intel(linear.LinearlyMappedLayer): translated address lives in and the layer_name that the address lives in """ - entry, position = self._translate_entry(offset) + entry, position = self._translate_entry(offset & self.page_mask) # Now we're done if not self._page_is_valid(entry): @@ -163,16 +170,26 @@ class Intel(linear.LinearlyMappedLayer): entry, f"Page Fault at entry {hex(entry)} in page entry", ) - page = self._mask(entry, self._maxphyaddr - 1, position + 1) | self._mask( - offset, position, 0 - ) + + pfn = self._pte_pfn(entry) + page_offset = self._mask(offset, position, 0) + page = pfn << self.page_shift | page_offset return page, 1 << (position + 1), self._base_layer - def _translate_entry(self, offset: int) -> Tuple[int, int]: - """Translates a specific offset based on paging tables. + def _pte_pfn(self, entry: int) -> int: + """Extracts the page frame number (PFN) from the page table entry (PTE) entry""" + return self._mask(entry, self._maxphyaddr - 1, 0) >> self.page_shift - Returns the translated entry value + @functools.lru_cache(maxsize=1024) + def _translate_entry(self, page_address: int) -> int: + """Translates a page address based on paging tables. + + Args: + page_address: The page base address + + Returns: + the translated entry value """ # Setup the entry and how far we are through the offset # Position maintains the number of bits left to process @@ -181,11 +198,13 @@ class Intel(linear.LinearlyMappedLayer): entry = self._initial_entry if not ( - self.minimum_address <= (offset & self.address_mask) <= self.maximum_address + self.minimum_address + <= (page_address & self.address_mask) + <= self.maximum_address ): raise exceptions.PagedInvalidAddressException( self.name, - offset, + page_address, position + 1, entry, "Entry outside virtual address range: " + hex(entry), @@ -197,23 +216,11 @@ class Intel(linear.LinearlyMappedLayer): if not self._page_is_valid(entry): raise exceptions.PagedInvalidAddressException( self.name, - offset, + page_address, position + 1, entry, "Page Fault at entry " + hex(entry) + " in table " + name, ) - # Check if we're a large page - if large_page and (entry & (1 << 7)): - # Mask off the PAT bit - if entry & (1 << 12): - entry -= 1 << 12 - # We're a large page, the rest is finished below - # If we want to implement PSE-36, it would need to be done here - break - # Figure out how much of the offset we should be using - start = position - position -= size - index = self._mask(offset, start, position + 1) >> (position + 1) # Grab the base address of the table we'll be getting the next entry from base_address = self._mask( @@ -224,42 +231,76 @@ class Intel(linear.LinearlyMappedLayer): if table is None: raise exceptions.PagedInvalidAddressException( self.name, - offset, + page_address, position + 1, entry, "Page Fault at entry " + hex(entry) + " in table " + name, ) + # Figure out how much of the offset we should be using + start = position + position -= size + index = self._mask(page_address, start, position + 1) >> (position + 1) + # Read the data for the next entry - entry_data = table[ - (index << self._index_shift) : (index << self._index_shift) - + self._entry_size - ] + entry_data_start = index << self._index_shift + entry_data = table[entry_data_start : entry_data_start + self._entry_size] if INTEL_TRANSLATION_DEBUGGING: vollog.log( constants.LOGLEVEL_VVVV, - "Entry {} at index {} gives data {} as {}".format( - hex(entry), - hex(index), - hex(struct.unpack(self._entry_format, entry_data)[0]), - name, - ), + f"Entry {hex(entry)} at index {hex(index)} gives data {hex(struct.unpack(self._entry_format, entry_data)[0])} as {name}", ) # Read out the new entry from memory (entry,) = struct.unpack(self._entry_format, entry_data) + # Check if we're a large page + if large_page and (entry & self._PAGE_PSE): + # Mask off the PAT bit + if entry & self._PAGE_PAT_LARGE: + entry -= self._PAGE_PAT_LARGE + # We're a large page, the rest is finished below + # If we want to implement PSE-36, it would need to be done here + break + return entry, position - @functools.lru_cache(1025) + @functools.lru_cache(maxsize=1025) def _get_valid_table(self, base_address: int) -> Optional[bytes]: """Extracts the table, validates it and returns it if it's valid.""" - table = self._context.layers.read( - self._base_layer, base_address, self.page_size - ) + try: + table = self._context.layers.read( + self._base_layer, base_address, self.page_size + ) + except exceptions.InvalidAddressException: + return None + #### # If the table is entirely duplicates, then mark the whole table as bad + # This is because Windows 10 onwards has a tendency to map unused pages as present + # This had the following consequences: + # - Used very litle physical memory + # - Exploded virtual memory + # - Causes *scan plugins to take multiple hours to complete even on small images + + # Previous versions of volatility would ignore a page during a scan when it matched + # the one directly preceding it in physical memory. + # This could trip if only two pages were identical and still required enumerating all + # the invalid pages (which itself was quite time consuming) + + # For this reason, volatility 3 shifted to looking at entire page tables (1,024 pages) + # and if all the pages mapped to the same place the table wouuld be skipped + # This could also be applied to the Directory level as well as the Table level, allowing + # Volatility to skip huge sections of virtual memory very efficiently, without missing + # any pages that were distinct within a particular page table (or directory). + + # In order to work at this level, the logic was moved out of the scanning component and + # directly into the layer logic itself. This does have the side effect of preventing + # entirely duplicated page tables from reporting as present, however, the trade off between + # Windows 10+ reduced scanning times (common amongst scan plugins) versus incorrectly reporting + # entire page tables of identically mapped repeating *valid* data (rare) was accepted in favour + # of the more common occurance. if table == table[: self._entry_size] * self._entry_number: return None return table @@ -278,7 +319,7 @@ class Intel(linear.LinearlyMappedLayer): def is_dirty(self, offset: int) -> bool: """Returns whether the page at offset is marked dirty""" - return self._page_is_dirty(self._translate_entry(offset)[0]) + return self._page_is_dirty(self._translate_entry(offset & self.page_mask)[0]) def mapping( self, offset: int, length: int, ignore_errors: bool = False @@ -303,7 +344,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 @@ -322,7 +369,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 @@ -347,12 +400,18 @@ class Intel(linear.LinearlyMappedLayer): yield offset, length, mapped_offset, length, layer_name return None while length > 0: + skip_mask = None try: chunk_offset, page_size, layer_name = self._translate(offset) - chunk_size = min(page_size - (chunk_offset % page_size), length) + # Page align the chunk size value + chunk_size = min(page_size - (offset % page_size), length) if not self._context.layers[layer_name].is_valid( chunk_offset, chunk_size ): + # Virtual -> physical is contiguous in the chunk_size range. + # If we fail, we can jump directly to the end as we know all bytes in between + # aren't mapped (virtually and) physically anyway. + skip_mask = chunk_size - 1 raise exceptions.InvalidAddressException( layer_name=layer_name, invalid_address=chunk_offset ) @@ -362,12 +421,13 @@ class Intel(linear.LinearlyMappedLayer): ) as excp: if not ignore_errors: raise - # We can jump more if we know where the page fault failed - if isinstance(excp, exceptions.PagedInvalidAddressException): - mask = (1 << excp.invalid_bits) - 1 - else: - mask = (1 << self._page_size_in_bits) - 1 - length_diff = mask + 1 - (offset & mask) + if skip_mask is None: + # We can jump more if we know where the page fault occured + if isinstance(excp, exceptions.PagedInvalidAddressException): + skip_mask = (1 << excp.invalid_bits) - 1 + else: + skip_mask = (1 << self._page_size_in_bits) - 1 + length_diff = skip_mask + 1 - (offset & skip_mask) length -= length_diff offset += length_diff else: @@ -405,7 +465,7 @@ class IntelPAE(Intel): _structure = [ ("page directory pointer", 2, False), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] _direct_metadata = collections.ChainMap({"pae": True}, Intel._direct_metadata) @@ -425,7 +485,7 @@ class Intel32e(Intel): ("page map layer 4", 9, False), ("page directory pointer", 9, True), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] @@ -501,3 +561,85 @@ class WindowsIntel32e(WindowsMixin, Intel32e): def _translate(self, offset: int) -> Tuple[int, int, str]: return self._translate_swap(self, offset, self._bits_per_register // 2) + + +class LinuxMixin(Intel): + @functools.cached_property + def _register_mask(self) -> int: + return (1 << self._bits_per_register) - 1 + + @functools.cached_property + def _physical_mask(self) -> int: + # From kernels 4.18 the physical mask is dynamic: See AMD SME, Intel Multi-Key Total + # Memory Encryption and CONFIG_DYNAMIC_PHYSICAL_MASK: 94d49eb30e854c84d1319095b5dd0405a7da9362 + physical_mask = (1 << self._maxphyaddr) - 1 + # TODO: Come back once SME support is available in the framework + return physical_mask + + @functools.cached_property + def page_mask(self) -> int: + # Note that within the Intel class it's a class method. However, since it uses + # complement operations and we are working in Python, it would be more careful to + # limit it to the architecture's pointer size. + return ~(self.page_size - 1) & self._register_mask + + @functools.cached_property + def _physical_page_mask(self) -> int: + return self.page_mask & self._physical_mask + + @functools.cached_property + def _pte_pfn_mask(self) -> int: + return self._physical_page_mask + + @functools.cached_property + def _pte_flags_mask(self) -> int: + return ~self._pte_pfn_mask & self._register_mask + + def _pte_flags(self, pte) -> int: + return pte & self._pte_flags_mask + + def _is_pte_present(self, entry: int) -> bool: + return ( + self._pte_flags(entry) & (self._PAGE_PRESENT | self._PAGE_PROTNONE) + ) != 0 + + def _page_is_valid(self, entry: int) -> bool: + # Overrides the Intel static method with the Linux-specific implementation + return self._is_pte_present(entry) + + def _pte_needs_invert(self, entry) -> bool: + # Entries that were set to PROT_NONE (PAGE_PRESENT) are inverted + # A clear PTE shouldn't be inverted. See f19f5c4 + return entry and not (entry & self._PAGE_PRESENT) + + def _protnone_mask(self, entry: int) -> int: + """Gets a mask to XOR with the page table entry to get the correct PFN""" + return self._register_mask if self._pte_needs_invert(entry) else 0 + + def _pte_pfn(self, entry: int) -> int: + """Extracts the page frame number from the page table entry""" + pfn = entry ^ self._protnone_mask(entry) + return (pfn & self._pte_pfn_mask) >> self.page_shift + + +class LinuxIntel(LinuxMixin, Intel): + pass + + +class LinuxIntelPAE(LinuxMixin, IntelPAE): + pass + + +class LinuxIntel32e(LinuxMixin, Intel32e): + # In the Linux kernel, the __PHYSICAL_MASK_SHIFT is a mask used to extract the + # physical address from a PTE. In Volatility3, this is referred to as _maxphyaddr. + # + # Until kernel version 4.17, Linux x86-64 used a 46-bit mask. With commit + # b83ce5ee91471d19c403ff91227204fb37c95fb2, this was extended to 52 bits, + # applying to both 4 and 5-level page tables. + # + # We initially used 52 bits for all Intel 64-bit systems, but this produced incorrect + # results for PROT_NONE pages. Since the mask value is defined by a preprocessor macro, + # it's difficult to detect the exact bit shift used in the current kernel. + # Using 46 bits has proven reliable for our use case, as seen in tools like crashtool. + _maxphyaddr = 46 diff --git a/volatility3/framework/layers/leechcore.py b/volatility3/framework/layers/leechcore.py index 542fd6ca2..06c359203 100644 --- a/volatility3/framework/layers/leechcore.py +++ b/volatility3/framework/layers/leechcore.py @@ -48,7 +48,7 @@ if HAS_LEECHCORE: try: self._handle = leechcorepyc.LeechCore(self._device) except TypeError: - raise IOError(f"Unable to open LeechCore device {self._device}") + raise OSError(f"Unable to open LeechCore device {self._device}") return self._handle def fileno(self): @@ -129,6 +129,8 @@ if HAS_LEECHCORE: def readline(self, __size: Optional[int] = ...) -> bytes: data = b"" + if not __size: + __size = 0 while __size > self._chunk_size or __size < 0: data += self.read(self._chunk_size) index = data.find(b"\n") diff --git a/volatility3/framework/layers/msf.py b/volatility3/framework/layers/msf.py index 8d84a774b..a7bf6466e 100644 --- a/volatility3/framework/layers/msf.py +++ b/volatility3/framework/layers/msf.py @@ -194,7 +194,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer): ) -> None: super().__init__(context, config_path, name, metadata) self._base_layer = self.config["base_layer"] - self._pages = self.config.get("pages", None) + self._pages = self.config.get("pages", []) self._pages_len = len(self._pages) if not self._pages: raise PDBFormatException(name, "Invalid/no pages specified") @@ -225,7 +225,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer): returned = 0 page_size = self._pdb_layer.page_size while length > 0: - page = math.floor((offset + returned) / page_size) + page = (offset + returned) // page_size page_position = (offset + returned) % page_size chunk_size = min(page_size - page_position, length) if page >= self._pages_len: @@ -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/qemu.py b/volatility3/framework/layers/qemu.py index 3942b25a5..5190027c4 100644 --- a/volatility3/framework/layers/qemu.py +++ b/volatility3/framework/layers/qemu.py @@ -9,6 +9,7 @@ import struct from typing import Any, Dict, List, Optional, Set, Tuple from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners, segmented from volatility3.framework.symbols import intermed @@ -99,6 +100,16 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer): context=context, config_path=config_path, name=name, metadata=metadata ) + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return super().get_requirements() + [ + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + ] + @classmethod def _check_header( cls, base_layer: interfaces.layers.DataLayerInterface, name: str = "" @@ -236,7 +247,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer): if self._architecture is None: vollog.log( constants.LOGLEVEL_VV, - f"QEVM architecture could not be determined", + "QEVM architecture could not be determined", ) # Once all segments have been read, determine the PCI hole if any diff --git a/volatility3/framework/layers/registry.py b/volatility3/framework/layers/registry.py index 609832886..e8b1246d3 100644 --- a/volatility3/framework/layers/registry.py +++ b/volatility3/framework/layers/registry.py @@ -7,23 +7,22 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.configuration import requirements -from volatility3.framework.configuration.requirements import ( - IntRequirement, - TranslationLayerRequirement, -) -from volatility3.framework.exceptions import InvalidAddressException from volatility3.framework.layers import linear from volatility3.framework.symbols import intermed -from volatility3.plugins.windows import pslist +from volatility3.framework.symbols.windows import extensions vollog = logging.getLogger(__name__) -class RegistryFormatException(exceptions.LayerException): +class RegistryException(exceptions.LayerException): + """Base Registry Exception class for catching Registry layer errors.""" + + +class RegistryFormatException(RegistryException): """Thrown when an error occurs with the underlying Registry file format.""" -class RegistryInvalidIndex(exceptions.LayerException): +class RegistryInvalidIndex(RegistryException): """Thrown when an index that doesn't exist or can't be found occurs.""" @@ -65,16 +64,15 @@ class RegistryHive(linear.LinearlyMappedLayer): # Win10 17063 introduced the Registry process to map most hives. Check # if it exists and update RegistryHive._base_layer - for proc in pslist.PsList.list_processes( - self.context, self.config["base_layer"], self.config["nt_symbols"] - ): - proc_name = proc.ImageFileName.cast( - "string", max_length=proc.ImageFileName.vol.count, errors="replace" + try: + registry_proc = self._find_registry_process() + if registry_proc: + self._base_layer = registry_proc.add_process_layer() + except ValueError: + vollog.log( + constants.LOGLEVEL_VVVV, + "Error walking process list, results may not be valid.", ) - if proc_name == "Registry" and proc.InheritedFromUniqueProcessId == 4: - proc_layer_name = proc.add_process_layer() - self._base_layer = proc_layer_name - break self._base_block = self.hive.BaseBlock.dereference() @@ -96,6 +94,41 @@ class RegistryHive(linear.LinearlyMappedLayer): f"Exception when setting hive {self.name} max address, using {hex(self._maxaddr)}", ) + def _find_registry_process(self) -> Optional[extensions.EPROCESS]: + """Walk the active process list and return the Registry process if it exists. Duplicates + PsList.list_processes() since pulling in the plugin causes problems. + + Returns: + The Registry EPROCESS object if it exists, or None + """ + + kernel = self.context.modules.get(self.config["kernel_module_name"]) + + if not kernel or not kernel.offset: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) + + ps_aph_offset = kernel.get_symbol("PsActiveProcessHead").address + list_entry = kernel.object(object_type="_LIST_ENTRY", offset=ps_aph_offset) + reloff = kernel.get_type("_EPROCESS").relative_child_offset( + "ActiveProcessLinks" + ) + eproc = kernel.object( + object_type="_EPROCESS", + offset=list_entry.vol.offset - reloff, + absolute=True, + ) + + for proc in eproc.ActiveProcessLinks: + proc_name = proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ) + if proc_name == "Registry" and proc.InheritedFromUniqueProcessId == 4: + return proc + + return None + def _get_hive_maxaddr(self, volatile): return ( self._hive_maxaddr_volatile if volatile else self._hive_maxaddr_non_volatile @@ -116,7 +149,7 @@ class RegistryHive(linear.LinearlyMappedLayer): @property def root_cell_offset(self) -> int: """Returns the offset for the root cell in this hive.""" - with contextlib.suppress(InvalidAddressException): + with contextlib.suppress(exceptions.InvalidAddressException): if ( self._base_block.Signature.cast( "string", max_length=4, encoding="latin-1" @@ -140,7 +173,13 @@ class RegistryHive(linear.LinearlyMappedLayer): """Returns the appropriate Node, interpreted from the Cell based on its Signature.""" cell = self.get_cell(cell_offset) - signature = cell.cast("string", max_length=2, encoding="latin-1") + try: + signature = cell.cast("string", max_length=2, encoding="latin-1") + except (RegistryException, exceptions.InvalidAddressException): + vollog.debug( + f"Failed to get cell signature for cell (0x{cell.vol.offset:x})" + ) + return cell if signature == "nk": return cell.u.KeyNode elif signature == "sk": @@ -156,9 +195,7 @@ class RegistryHive(linear.LinearlyMappedLayer): else: # It doesn't matter that we use KeyNode, we're just after the first two bytes vollog.debug( - "Unknown Signature {} (0x{:x}) at offset {}".format( - signature, cell.u.KeyNode.Signature, cell_offset - ) + f"Unknown Signature {signature} (0x{cell.u.KeyNode.Signature:x}) at offset {cell_offset}" ) return cell @@ -178,9 +215,7 @@ class RegistryHive(linear.LinearlyMappedLayer): if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"): raise RegistryFormatException( self.name, - "Encountered {} instead of _CM_KEY_NODE".format( - root_node.vol.type_name - ), + f"Encountered {root_node.vol.type_name} instead of _CM_KEY_NODE", ) node_key = [root_node] if key.endswith("\\"): @@ -190,9 +225,9 @@ class RegistryHive(linear.LinearlyMappedLayer): while key_array and node_key: subkeys = node_key[-1].get_subkeys() for subkey in subkeys: - # registry keys are not case sensitive so compare lowercase - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724946(v=vs.85).aspx - if subkey.get_name().lower() == key_array[0].lower(): + # registry keys are not case sensitive so compare likewise + # https://learn.microsoft.com/en-us/windows/win32/sysinfo/structure-of-the-registry + if subkey.get_name().casefold() == key_array[0].casefold(): node_key = node_key + [subkey] found_key, key_array = found_key + [key_array[0]], key_array[1:] break @@ -231,7 +266,7 @@ class RegistryHive(linear.LinearlyMappedLayer): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - IntRequirement( + requirements.IntRequirement( name="hive_offset", description="Offset within the base layer at which the hive lives", default=0, @@ -240,7 +275,7 @@ class RegistryHive(linear.LinearlyMappedLayer): requirements.SymbolTableRequirement( name="nt_symbols", description="Windows kernel symbols" ), - TranslationLayerRequirement( + requirements.TranslationLayerRequirement( name="base_layer", description="Layer in which the registry hive lives", optional=False, @@ -260,7 +295,7 @@ class RegistryHive(linear.LinearlyMappedLayer): self.name, hex(offset & 0x7FFFFFFF), hex(self._get_hive_maxaddr(volatile)), - "volative" if volatile else "non-volatile", + "volatile" if volatile else "non-volatile", self.get_name(), ), ) diff --git a/volatility3/framework/layers/resources.py b/volatility3/framework/layers/resources.py index 2dba7caa8..66fb617af 100644 --- a/volatility3/framework/layers/resources.py +++ b/volatility3/framework/layers/resources.py @@ -29,7 +29,7 @@ except ImportError: try: # Import so that the handler is found by the framework.class_subclasses callc - import smb.SMBHandler # lgtm [py/unused-import] + from smb import SMBHandler as SMBHandler # lgtm [py/unused-import] except ImportError: # If we fail to import this, it means that SMB handling won't be available pass @@ -57,7 +57,7 @@ def cascadeCloseFile(new_fp: IO[bytes], original_fp: IO[bytes]) -> IO[bytes]: return new_fp -class ResourceAccessor(object): +class ResourceAccessor: """Object for opening URLs as files (downloading locally first if necessary)""" @@ -177,33 +177,47 @@ class ResourceAccessor(object): + ".cache", ) + try: + content_length = int(fp.info().get("Content-Length", -1)) + except (AttributeError, ValueError): + # If our fp doesn't have an info member, carry on gracefully + content_length = -1 + if not os.path.exists(temp_filename): vollog.debug(f"Caching file at: {temp_filename}") + cache_file_size = -1 try: - content_length = fp.info().get("Content-Length", -1) - except AttributeError: - # If our fp doesn't have an info member, carry on gracefully - content_length = -1 - with open(temp_filename, "wb") as cache_file: - count = 0 - block = fp.read(block_size) - while block: - count += len(block) - if self._progress_callback: - self._progress_callback( - count * 100 / max(count, int(content_length)), - f"Reading file {url}", - ) - cache_file.write(block) + with open(temp_filename, "wb") as cache_file: + count = 0 block = fp.read(block_size) + while block: + count += len(block) + if self._progress_callback: + self._progress_callback( + count * 100 / max(count, int(content_length)), + f"Reading file {url}", + ) + cache_file.write(block) + block = fp.read(block_size) + cache_file.seek(0, os.SEEK_END) + cache_file_size = cache_file.tell() + finally: + if cache_file_size < content_length: + os.remove(temp_filename) + raise ValueError("Cached file did not download completely") else: - vollog.debug(f"Using already cached file at: {temp_filename}") + vollog.debug( + f"Trying to use already cached file at: {temp_filename}" + ) + # Re-open the cache with a different mode # Since we don't want people thinking they're able to save to the cache file, # open it in read mode only and allow breakages to happen if they wanted to write curfile = open(temp_filename, mode="rb") + # Validate the hash or delete the temp_filename and report an error + # Determine whether the file is a particular type of file, and if so, open it as such IMPORTED_MAGIC = False if HAS_MAGIC: @@ -291,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/scanners/__init__.py b/volatility3/framework/layers/scanners/__init__.py index dd8dc46be..f07849f42 100644 --- a/volatility3/framework/layers/scanners/__init__.py +++ b/volatility3/framework/layers/scanners/__init__.py @@ -5,12 +5,14 @@ import re from typing import Generator, List, Tuple, Dict, Optional from volatility3.framework.interfaces import layers -from volatility3.framework.layers.scanners import multiregexp +from volatility3.framework.layers.scanners import multiregexp as multiregexp class BytesScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) def __init__(self, needle: bytes) -> None: @@ -38,6 +40,8 @@ class RegExScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) def __init__(self, pattern: bytes, flags: int = re.DOTALL) -> None: @@ -57,6 +61,7 @@ class RegExScanner(layers.ScannerInterface): class MultiStringScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) _required_framework_version = (2, 0, 0) def __init__(self, patterns: List[bytes]) -> None: @@ -72,7 +77,7 @@ class MultiStringScanner(layers.ScannerInterface): return None for char in value: - trie[char] = trie.get(char, {}) + trie.setdefault(char, {}) trie = trie[char] # Mark the end of a string diff --git a/volatility3/framework/layers/scanners/multiregexp.py b/volatility3/framework/layers/scanners/multiregexp.py index be3581f05..9831a9d8e 100644 --- a/volatility3/framework/layers/scanners/multiregexp.py +++ b/volatility3/framework/layers/scanners/multiregexp.py @@ -6,7 +6,7 @@ import re from typing import Generator, List, Tuple -class MultiRegexp(object): +class MultiRegexp: """Algorithm for multi-string matching.""" def __init__(self) -> None: 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 7fd19700b..5dc00f344 100644 --- a/volatility3/framework/layers/vmware.py +++ b/volatility3/framework/layers/vmware.py @@ -57,6 +57,10 @@ class VmwareLayer(segmented.SegmentedLayer): ) meta_layer = self.context.layers.get(self._meta_layer, None) + if meta_layer is None: + raise exceptions.LayerException( + self._meta_layer, "VMware: Meta layer not found" + ) header_size = struct.calcsize(self.header_structure) data = meta_layer.read(0, header_size) magic, unknown, groupCount = struct.unpack(self.header_structure, data) diff --git a/volatility3/framework/layers/xen.py b/volatility3/framework/layers/xen.py index e7aa0ccec..7f42eb662 100644 --- a/volatility3/framework/layers/xen.py +++ b/volatility3/framework/layers/xen.py @@ -5,7 +5,7 @@ from typing import Optional from volatility3.framework import constants, interfaces, exceptions from volatility3.framework.layers import elf from volatility3.framework.symbols import intermed -from volatility3.framework.constants.linux import ELF_CLASS +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -15,7 +15,7 @@ class XenCoreDumpLayer(elf.Elf64Layer): _header_struct = struct.Struct(" TUnion[int, float, bytes, str, bool]: """Converts a series of bytes to a particular type of value.""" - if struct_type == int: + if struct_type is int: return int.from_bytes( data, byteorder=data_format.byteorder, signed=data_format.signed ) - if struct_type == bool: + if struct_type is bool: struct_format = "?" - elif struct_type == float: + elif struct_type is float: float_vals = "zzezfzzzd" if ( data_format.length > len(float_vals) @@ -70,7 +70,7 @@ def convert_value_to_data( f"Written value is not of the correct type for {struct_type.__name__}" ) - if struct_type == int and isinstance(value, int): + if struct_type is int and isinstance(value, int): # Doubling up on the isinstance is for mypy return int.to_bytes( value, @@ -78,9 +78,9 @@ def convert_value_to_data( byteorder=data_format.byteorder, signed=data_format.signed, ) - if struct_type == bool: + if struct_type is bool: struct_format = "?" - elif struct_type == float: + elif struct_type is float: float_vals = "zzezfzzzd" if ( data_format.length > len(float_vals) @@ -152,7 +152,7 @@ class PrimitiveObject(interfaces.objects.ObjectInterface): type_name: str, object_info: interfaces.objects.ObjectInformation, data_format: DataFormatInfo, - new_value: TUnion[int, float, bool, bytes, str] = None, + new_value: Optional[TUnion[int, float, bool, bytes, str]] = None, **kwargs, ) -> "PrimitiveObject": """Creates the appropriate class and returns it so that the native type @@ -356,8 +356,9 @@ class String(PrimitiveObject, str): ), **params, ) - if value.find("\x00") >= 0: - value = value[: value.find("\x00")] + index = value.find("\x00") + if index >= 0: + value = value[:index] return value class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): @@ -401,13 +402,35 @@ class Pointer(Integer): pointer should be recast. The "pointer" must always live within the space (even if the data provided is invalid). """ + mask = context.layers[object_info.native_layer_name].address_mask + new = ( + cls._get_raw_value( + context, data_format, object_info.layer_name, object_info.offset + ) + & mask + ) + return new + + @classmethod + def _get_raw_value( + cls, + context: interfaces.context.ContextInterface, + data_format: DataFormatInfo, + layer_name: str, + offset: int, + ) -> int: length, endian, signed = data_format if signed: raise ValueError("Pointers cannot have signed values") - mask = context.layers[object_info.native_layer_name].address_mask - data = context.layers.read(object_info.layer_name, object_info.offset, length) + data = context.layers.read(layer_name, offset, length) value = int.from_bytes(data, byteorder=endian, signed=signed) - return value & mask + return value + + def get_raw_value(self) -> int: + raw = self._get_raw_value( + self._context, self.vol.data_format, self.vol.layer_name, self.vol.offset + ) + return raw def dereference( self, layer_name: Optional[str] = None @@ -435,6 +458,7 @@ class Pointer(Integer): offset=offset, parent=self, size=self.vol.subtype.size, + native_layer_name=layer_name, ), ) return self._cache[layer_name] @@ -601,7 +625,7 @@ class Enumeration(interfaces.objects.ObjectInterface, int): inverse_choices[v] = k return inverse_choices - def lookup(self, value: int = None) -> str: + def lookup(self, value: Optional[int] = None) -> str: """Looks up an individual value and returns the associated name. If multiple identifiers map to the same value, the first matching identifier will be returned @@ -690,7 +714,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): type_name: str, object_info: interfaces.objects.ObjectInformation, count: int = 0, - subtype: templates.ObjectTemplate = None, + subtype: Optional[templates.ObjectTemplate] = None, ) -> None: super().__init__(context=context, type_name=type_name, object_info=object_info) self._vol["count"] = count @@ -788,7 +812,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): layer_name=self.vol.layer_name, offset=mask & (self.vol.offset + (self.vol.subtype.size * index)), parent=self, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=self.vol.subtype.size, ) result += [self.vol.subtype(context=self._context, object_info=object_info)] @@ -955,7 +979,7 @@ class AggregateType(interfaces.objects.ObjectInterface): offset=mask & (self.vol.offset + relative_offset), member_name=attr, parent=self, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=template.size, ) member = template(context=self._context, object_info=object_info) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 8aa527cdb..799639a67 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -2,9 +2,13 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import re +import logging from typing import Optional, Union -from volatility3.framework import interfaces, objects, constants +from volatility3.framework import interfaces, objects, constants, exceptions + +vollog = logging.getLogger(__name__) def rol(value: int, count: int, max_bits: int = 64) -> int: @@ -22,33 +26,210 @@ def bswap_32(value: int) -> int: def bswap_64(value: int) -> int: - low = bswap_32((value >> 32)) - high = bswap_32((value & 0xFFFFFFFF)) + low = bswap_32(value >> 32) + high = bswap_32(value & 0xFFFFFFFF) return ((high << 32) | low) & 0xFFFFFFFFFFFFFFFF def array_to_string( - array: "objects.Array", count: Optional[int] = None, errors: str = "replace" -) -> interfaces.objects.ObjectInterface: - """Takes a volatility Array of characters and returns a string.""" + array: "objects.Array", + count: Optional[int] = None, + errors: str = "replace", + block_size=32, + encoding="utf-8", +) -> str: + """Takes a Volatility 'Array' of characters and returns a Python string. + + Args: + array: The Volatility `Array` object containing character elements. + count: Optional maximum number of characters to convert. If None, the function + processes the entire array. + errors: Specifies error handling behavior for decoding, defaulting to "replace". + block_size: Reading block size. Defaults to 32 + + Returns: + A decoded string representation of the character array. + """ # TODO: Consider checking the Array's target is a native char - if count is None: - count = array.vol.count if not isinstance(array, objects.Array): raise TypeError("Array_to_string takes an Array of char") - return array.cast("string", max_length=count, errors=errors) + if count is None: + count = array.vol.count + + return address_to_string( + context=array._context, + layer_name=array.vol.layer_name, + address=array.vol.offset, + count=count, + errors=errors, + block_size=block_size, + encoding=encoding, + ) -def pointer_to_string(pointer: "objects.Pointer", count: int, errors: str = "replace"): - """Takes a volatility Pointer to characters and returns a string.""" +def pointer_to_string( + pointer: "objects.Pointer", + count: int, + errors: str = "replace", + block_size=32, + encoding="utf-8", +) -> str: + """Takes a Volatility 'Pointer' to characters and returns a Python string. + + Args: + pointer: A `Pointer` object containing character elements. + count: Optional maximum number of characters to convert. If None, the function + processes the entire array. + errors: Specifies error handling behavior for decoding, defaulting to "replace". + block_size: Reading block size. Defaults to 32 + + Returns: + A decoded string representation of the data referenced by the pointer. + """ if not isinstance(pointer, objects.Pointer): raise TypeError("pointer_to_string takes a Pointer") + if count < 1: raise ValueError("pointer_to_string requires a positive count") - char = pointer.dereference() - return char.cast("string", max_length=count, errors=errors) + + return address_to_string( + context=pointer._context, + layer_name=pointer.vol.layer_name, + address=pointer, + count=count, + errors=errors, + block_size=block_size, + encoding=encoding, + ) + + +def gather_contiguous_bytes_from_address( + context, data_layer, starting_address: int, count: int +) -> bytes: + """ + This method reconstructs a string from memory while also carefully examining each page + + It goes page-by-page reading the bytes. This is done by calculating page boundaries + and then only reading one page at a time. + + If a page is missing, the code initially catches the exception. + If data is non-empty (meaning at least one read succeeded), then we return what was read + If the first page fails, then we re-raise the exception + """ + + data = b"" + + if isinstance(data_layer, interfaces.layers.TranslationLayerInterface): + last_address = starting_address + + for address, length, _, _, _ in data_layer.mapping( + offset=starting_address, length=count, ignore_errors=True + ): + # we hit a swapped out page + if last_address != address: + break + + data += data_layer.read(address, length) + + last_address = address + length + + elif starting_address + count < data_layer.maximum_address: + data = data_layer.read(starting_address, count) + + # if we were able to read from the first page, we want to try and construct the string + # if the first page fails -> throw exception + if data: + return data + else: + raise exceptions.InvalidAddressException( + layer_name=data_layer, invalid_address=starting_address + ) + + +def bytes_to_decoded_string( + data: bytes, encoding: str, errors: str, return_truncated: bool = True +) -> str: + """ + Args: + data: The `bytes` buffer containing the string of a string at offset 0 + encoding: An encoding value for the encoding parameter of `bytes.decode` + errors: An errors value for the errors parameter of `bytes.decode` + return_truncated: Dictates whether truncated strings should be returned or + if a ValueError should be thrown if a truncated (broken) string was decoded + Returns: + bytes: The decoded string starting at offset of data + + This function takes a bytes buffer that contains at a string of unknown + length starting at the first byte, and returns the properly decoded string + + It starts by using Python's `bytes.decode` to attempt to decode the entire string + It then finds the termination character (\ufffd or \x00) and splices the string + Finally, it returns this spliced string after its been decoded with the + caller-specified encoding + """ + # this is the standard byte used to replace bad unicode characters + unicode_replacement_char = "\ufffd" + + # used to find the terminating byte + termination_re = re.compile(f"{unicode_replacement_char}|\x00") + + # run over the entire string, letting Python replace invalid characters + full_decoded_string = data.decode(encoding=encoding, errors="replace") + + # stop at the first terminating character or get the whole string if not found + try: + idx = termination_re.search(full_decoded_string).start() + except AttributeError: + if return_truncated: + idx = len(full_decoded_string) + else: + raise ValueError( + "return_truncated set to False and truncated string decoded." + ) + + # cut at terminating byte, if found + data = bytes(full_decoded_string[:idx], encoding=encoding) + + # return with caller-specified encoding and errors + return data.decode(encoding=encoding, errors=errors) + + +def address_to_string( + context: interfaces.context.ContextInterface, + layer_name: str, + address: int, + count: int, + errors: str = "replace", + block_size=32, + encoding="utf-8", +) -> str: + """Reads a null-terminated string from a given specified memory address, processing + it in blocks for efficiency. + + Args: + context: The context used to retrieve memory layers and symbol tables + layer_name: The name of the memory layer to read from + address: The address where the string is located in memory + count: The number of bytes to read + errors: The error handling scheme to use for encoding errors. Defaults to "replace" + block_size: Reading block size. Defaults to 32 + + Returns: + The decoded string extracted from memory. + """ + if not isinstance(address, int): + raise TypeError("Address must be a valid integer") + + if count < 1: + raise ValueError("Count must be greater than 0") + + layer = context.layers[layer_name] + + data = gather_contiguous_bytes_from_address(context, layer, address, count) + + return bytes_to_decoded_string(data=data, errors=errors, encoding=encoding) def array_of_pointers( @@ -71,3 +252,63 @@ def array_of_pointers( ).clone() subtype_pointer.update_vol(subtype=subtype) return array.cast("array", count=count, subtype=subtype_pointer) + + +def dynamically_sized_array_of_pointers( + context: interfaces.context.ContextInterface, + array: interfaces.objects.ObjectInterface, + subtype: Union[str, interfaces.objects.Template], + iterator_guard_value: int, + stop_value: int = 0, + stop_on_invalid_pointers: bool = True, +) -> interfaces.objects.ObjectInterface: + """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). + Array iteration should always be performed with an arbitrary guard value as maximum size, + to prevent running forever in case something unexpected happens. + + Args: + context: The context on which to operate. + array: The object to cast to an array. + iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. + subtype: The subtype of the array's pointers. + stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays + that are known to have smeared entries before the end. + + Returns: + An array of pointer objects + """ + new_count = 0 + sym_table_name = array.get_symbol_table_name() + sym_table = context.symbol_space[sym_table_name] + ptr_size = sym_table.get_type("pointer").size + layer_name = array.vol.layer_name + + offset = array.vol.offset + entry = None + while entry != stop_value and new_count < iterator_guard_value: + try: + entry = context.object( + sym_table_name + constants.BANG + "pointer", + offset=offset, + layer_name=layer_name, + ) + except exceptions.InvalidAddressException: + break + + if not entry.is_readable() and stop_on_invalid_pointers: + break + + offset += ptr_size + new_count += 1 + else: + vollog.log( + constants.LOGLEVEL_V, + f"""Iterator guard value {iterator_guard_value} reached while iterating over array at offset {array.vol.offset:#x}.\ + This means that there is a bug (e.g. smearing) with this array, or that it may contain valid entries past the iterator guard value.""", + ) + + # Leverage the "Array" object instead of returning a Python list + return array_of_pointers( + array=array, count=new_count, subtype=subtype, context=context + ) diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index b3c2fd3a5..eea39206d 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -22,7 +22,12 @@ class Banners(interfaces.plugins.PluginInterface): return [ requirements.TranslationLayerRequirement( name="primary", description="Memory layer to scan" - ) + ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), ] def _generator(self): @@ -55,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/configwriter.py b/volatility3/framework/plugins/configwriter.py index eca01a84a..a567a6acd 100644 --- a/volatility3/framework/plugins/configwriter.py +++ b/volatility3/framework/plugins/configwriter.py @@ -14,8 +14,8 @@ vollog = logging.getLogger(__name__) class ConfigWriter(plugins.PluginInterface): - """Runs the automagics and both prints and outputs configuration in the - output directory.""" + """Runs the automagics and both prints and outputs configuration in the \ +output directory.""" _required_framework_version = (2, 0, 0) diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 4f07bd5a8..10f391321 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -7,6 +7,7 @@ import os import pathlib import zipfile from typing import Generator, List +from importlib.util import find_spec from volatility3 import schemas, symbols from volatility3.framework import constants, interfaces, renderers @@ -71,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: @@ -96,16 +100,12 @@ class IsfInfo(plugins.PluginInterface): if filter_item in isf_file: filtered_list.append(isf_file) - try: - import jsonschema - - if not self.config["validate"]: - raise ImportError # Act as if we couldn't import if validation is turned off + if find_spec("jsonschema") and self.config["validate"]: def check_valid(data): return "True" if schemas.validate(data, True) else "False" - except ImportError: + else: def check_valid(data): return "Unknown" @@ -135,6 +135,7 @@ class IsfInfo(plugins.PluginInterface): valid = check_valid(data) except (UnicodeDecodeError, json.decoder.JSONDecodeError): vollog.warning(f"Invalid ISF: {entry}") + continue yield ( 0, ( diff --git a/volatility3/framework/plugins/layerwriter.py b/volatility3/framework/plugins/layerwriter.py index 24149a390..bcc999aba 100644 --- a/volatility3/framework/plugins/layerwriter.py +++ b/volatility3/framework/plugins/layerwriter.py @@ -62,7 +62,7 @@ class LayerWriter(plugins.PluginInterface): Args: context: the context from which to read the memory layer layer_name: the name of the layer to write out - preferred_name: a string with the preferred filename for hte file + preferred_name: a string with the preferred filename for the file chunk_size: an optional size for the chunks that should be written (defaults to 0x500000) open_method: class for creating FileHandler context managers progress_callback: an optional function that takes a percentage and a string that displays output @@ -119,7 +119,7 @@ class LayerWriter(plugins.PluginInterface): # Update the filename, which may have changed if a file # with the same name already existed. output_name = file_handle.preferred_filename - except IOError as excp: + except OSError as excp: yield 0, (f"Layer cannot be written to {output_name}: {excp}",) yield 0, (f"Layer has been written to {output_name}",) diff --git a/volatility3/framework/plugins/linux/bash.py b/volatility3/framework/plugins/linux/bash.py index ce4567ca6..18d9f3bfc 100644 --- a/volatility3/framework/plugins/linux/bash.py +++ b/volatility3/framework/plugins/linux/bash.py @@ -1,8 +1,8 @@ # 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 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" +"""A module containing a plugin that recovers bash command history +from bash process memory.""" import datetime import struct @@ -13,7 +13,7 @@ from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.layers import scanners from volatility3.framework.objects import utility -from volatility3.framework.symbols.linux.bash import BashIntermedSymbols +from volatility3.framework.symbols.linux import bash from volatility3.plugins import timeliner from volatility3.plugins.linux import pslist @@ -22,6 +22,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): """Recovers bash command history from memory.""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -31,8 +32,23 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), requirements.ListRequirement( name="pid", @@ -45,7 +61,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): def _generator(self, tasks): vmlinux = self.context.modules[self.config["kernel"]] is_32bit = not symbols.symbol_table_is_64bit( - self.context, vmlinux.symbol_table_name + context=self.context, symbol_table_name=vmlinux.symbol_table_name ) if is_32bit: pack_format = "I" @@ -54,7 +70,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): pack_format = "Q" bash_json_file = "bash64" - bash_table_name = BashIntermedSymbols.create( + bash_table_name = bash.BashIntermedSymbols.create( self.context, self.config_path, "linux", bash_json_file ) diff --git a/volatility3/framework/plugins/linux/boottime.py b/volatility3/framework/plugins/linux/boottime.py index 8f63ee7f8..0b9abb856 100644 --- a/volatility3/framework/plugins/linux/boottime.py +++ b/volatility3/framework/plugins/linux/boottime.py @@ -15,8 +15,7 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) """Shows the time the system was started""" _required_framework_version = (2, 11, 0) - - _version = (1, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -26,8 +25,13 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 3, 0) + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), ] diff --git a/volatility3/framework/plugins/linux/capabilities.py b/volatility3/framework/plugins/linux/capabilities.py index bfdb69aba..364047893 100644 --- a/volatility3/framework/plugins/linux/capabilities.py +++ b/volatility3/framework/plugins/linux/capabilities.py @@ -29,7 +29,7 @@ class TaskData: @dataclass class CapabilitiesData: - """Stores each set of capabilties for a task""" + """Stores each set of capabilities for a task""" cap_inheritable: interfaces.objects.ObjectInterface cap_permitted: interfaces.objects.ObjectInterface @@ -49,9 +49,8 @@ class CapabilitiesData: class Capabilities(plugins.PluginInterface): """Lists process capabilities""" - _required_framework_version = (2, 0, 0) - - _version = (1, 0, 0) + _required_framework_version = (2, 13, 0) + _version = (1, 1, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -61,8 +60,8 @@ class Capabilities(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pids", @@ -87,7 +86,7 @@ class Capabilities(plugins.PluginInterface): try: kernel_cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap") except exceptions.SymbolError: - # It should be a kernel < 3.2 + # It should be a kernel < 3.2 See 73efc0394e148d0e15583e13712637831f926720 return None vol2_last_cap = extensions.kernel_cap_struct.get_last_cap_value() @@ -137,7 +136,7 @@ class Capabilities(plugins.PluginInterface): comm=utility.array_to_string(task.comm), pid=int(task.pid), tgid=int(task.tgid), - ppid=int(task.parent.pid), + ppid=int(task.get_parent_pid()), euid=int(task.cred.euid), ) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 201a443f7..3f9e14161 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -1,141 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" import logging -from typing import List - -from volatility3.framework import exceptions, interfaces -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import format_hints +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_afinfo vollog = logging.getLogger(__name__) -class Check_afinfo(plugins.PluginInterface): - """Verifies the operation function pointers of network protocols.""" +class Check_afinfo( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_afinfo.Check_afinfo, + removal_date="2026-06-07", +): + """Verifies the operation function pointers of network protocols (deprecated).""" + _version = (1, 0, 0) _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - ] - - # returns whether the symbol is found within the kernel (system.map) or not - def _is_known_address(self, handler_addr): - symbols = list(self.context.symbol_space.get_symbols_by_location(handler_addr)) - - return len(symbols) > 0 - - def _check_members(self, var_ops, var_name, members): - for check in members: - # redhat-specific garbage - if check.startswith("__UNIQUE_ID_rh_kabi_hide"): - continue - - if check == "write": - addr = var_ops.member(attr="write") - else: - addr = getattr(var_ops, check) - - if addr and addr != 0 and not self._is_known_address(addr): - yield check, addr - - def _check_afinfo(self, var_name, var, op_members, seq_members): - # check if object has a least one of the members used for analysis by this function - required_members = ["seq_fops", "seq_ops", "seq_show"] - has_required_member = any(var.has_member(member) for member in required_members) - if not has_required_member: - vollog.debug( - f"{var_name} object at {hex(var.vol.offset)} had none of the required members: {', '.join([member for member in required_members])}" - ) - raise exceptions.PluginRequirementException - - if var.has_member("seq_fops"): - for hooked_member, hook_address in self._check_members( - var.seq_fops, var_name, op_members - ): - yield var_name, hooked_member, hook_address - - # newer kernels - if var.has_member("seq_ops"): - for hooked_member, hook_address in self._check_members( - var.seq_ops, var_name, seq_members - ): - yield var_name, hooked_member, hook_address - - # this is the most commonly hooked member by rootkits, so a force a check on it - else: - if var.has_member("seq_show"): - if not self._is_known_address(var.seq_show): - yield var_name, "show", var.seq_show - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - op_members = vmlinux.get_type("file_operations").members - seq_members = vmlinux.get_type("seq_operations").members - - tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) - udp = ( - "udp_seq_afinfo", - [ - "udplite6_seq_afinfo", - "udp6_seq_afinfo", - "udplite4_seq_afinfo", - "udp4_seq_afinfo", - ], - ) - protocols = [tcp, udp] - - # used to track the calls to _check_afinfo and the - # number of errors produced due to missing members - symbols_checked = set() - symbols_with_errors = set() - - # loop through all symbols - for struct_type, global_vars in protocols: - for global_var_name in global_vars: - # this will lookup fail for the IPv6 protocols on kernels without IPv6 support - try: - global_var = vmlinux.get_symbol(global_var_name) - except exceptions.SymbolError: - continue - - global_var = vmlinux.object( - object_type=struct_type, offset=global_var.address - ) - - symbols_checked.add(global_var_name) - try: - for name, member, address in self._check_afinfo( - global_var_name, global_var, op_members, seq_members - ): - yield 0, (name, member, format_hints.Hex(address)) - except exceptions.PluginRequirementException: - symbols_with_errors.add(global_var_name) - - # if every call to _check_afinfo failed show a warning - if symbols_checked == symbols_with_errors: - vollog.warning( - "This plugin was not able to check for hooks. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Symbol Name", str), - ("Member", str), - ("Handler Address", format_hints.Hex), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index b7f73c3eb..6c2c6f3d5 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -1,72 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_creds -from volatility3.framework import interfaces, renderers -from volatility3.framework.renderers import format_hints -from volatility3.framework.configuration import requirements -from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) -class Check_creds(interfaces.plugins.PluginInterface): - """Checks if any processes are sharing credential structures""" +class Check_creds( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_creds.Check_creds, + removal_date="2026-06-07", +): + """Checks if any processes are sharing credential structures (deprecated).""" _required_framework_version = (2, 0, 0) - - _version = (2, 0, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - type_task = vmlinux.get_type("task_struct") - - if not type_task.has_member("cred"): - raise TypeError( - "This plugin requires the task_struct structure to have a cred member. " - "This member is not present in the supplied symbol table. " - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - creds = {} - - tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) - - for task in tasks: - task_cred_ptr = task.cred - if not (task_cred_ptr and task_cred_ptr.is_readable()): - continue - - cred_addr = task_cred_ptr.dereference().vol.offset - - creds.setdefault(cred_addr, []) - creds[cred_addr].append(task.pid) - - for cred_addr, pids in creds.items(): - if len(pids) > 1: - pid_str = ", ".join([str(pid) for pid in pids]) - - fields = [ - format_hints.Hex(cred_addr), - pid_str, - ] - yield (0, fields) - - def run(self): - headers = [ - ("CredVAddr", format_hints.Hex), - ("PIDs", str), - ] - return renderers.TreeGrid(headers, self._generator()) + _version = (2, 0, 2) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index cc3a08933..449f85e1e 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -1,125 +1,20 @@ # This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List - -from volatility3.framework import interfaces, renderers, symbols -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_idt vollog = logging.getLogger(__name__) -class Check_idt(interfaces.plugins.PluginInterface): - """Checks if the IDT has been altered""" +class Check_idt( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_idt.Check_idt, + removal_date="2026-06-07", +): + """Checks if the IDT has been altered (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - - is_32bit = not symbols.symbol_table_is_64bit( - self.context, vmlinux.symbol_table_name - ) - - idt_table_size = 256 - - address_mask = self.context.layers[vmlinux.layer_name].address_mask - - # hw handlers + system call - check_idxs = list(range(0, 20)) + [128] - - if is_32bit: - if vmlinux.has_type("gate_struct"): - idt_type = "gate_struct" - else: - idt_type = "desc_struct" - else: - if vmlinux.has_type("gate_struct64"): - idt_type = "gate_struct64" - elif vmlinux.has_type("gate_struct"): - idt_type = "gate_struct" - else: - idt_type = "idt_desc" - - addrs = vmlinux.object_from_symbol("idt_table") - - table = vmlinux.object( - object_type="array", - offset=addrs.vol.offset, - subtype=vmlinux.get_type(idt_type), - count=idt_table_size, - absolute=True, - ) - - for i in check_idxs: - ent = table[i] - - if not ent: - continue - - if hasattr(ent, "Address"): - idt_addr = ent.Address - else: - low = ent.offset_low - middle = ent.offset_middle - - if hasattr(ent, "offset_high"): - high = ent.offset_high - else: - high = 0 - - idt_addr = (high << 32) | (middle << 16) | low - - idt_addr = idt_addr & address_mask - - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( - vmlinux, handlers, idt_addr - ) - - yield ( - 0, - [ - format_hints.Hex(i), - format_hints.Hex(idt_addr), - module_name, - symbol_name, - ], - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Index", format_hints.Hex), - ("Address", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ], - self._generator(), - ) + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 9b3594c5e..d8b3ddcf1 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -1,88 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List - -from volatility3.framework import interfaces, renderers, exceptions, constants -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_modules vollog = logging.getLogger(__name__) -class Check_modules(plugins.PluginInterface): - """Compares module list to sysfs info, if available""" +class Check_modules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_modules.Check_modules, + removal_date="2026-06-07", +): + """Compares module list to sysfs info, if available (deprecated).""" + _version = (3, 0, 1) _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), - ] - - @classmethod - def get_kset_modules( - cls, context: interfaces.context.ContextInterface, vmlinux_name: str - ): - vmlinux = context.modules[vmlinux_name] - - try: - module_kset = vmlinux.object_from_symbol("module_kset") - except exceptions.SymbolError: - module_kset = None - - if not module_kset: - raise TypeError( - "This plugin requires the module_kset structure. This structure is not present in the supplied symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - ret = {} - - kobj_off = vmlinux.get_type("module_kobject").relative_child_offset("kobj") - - for kobj in module_kset.list.to_list( - vmlinux.symbol_table_name + constants.BANG + "kobject", "entry" - ): - mod_kobj = vmlinux.object( - object_type="module_kobject", - offset=kobj.vol.offset - kobj_off, - absolute=True, - ) - - mod = mod_kobj.mod - - name = utility.pointer_to_string(kobj.name, 32) - if kobj.name and kobj.reference_count() > 2: - ret[name] = mod - - return ret - - def _generator(self): - kset_modules = self.get_kset_modules(self.context, self.config["kernel"]) - - lsmod_modules = set( - str(utility.array_to_string(modules.name)) - for modules in lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - ) - - for mod_name in set(kset_modules.keys()).difference(lsmod_modules): - yield (0, (format_hints.Hex(kset_modules[mod_name]), str(mod_name))) - - def run(self): - return renderers.TreeGrid( - [("Module Address", format_hints.Hex), ("Module Name", str)], - self._generator(), - ) diff --git a/volatility3/framework/plugins/linux/check_syscall.py b/volatility3/framework/plugins/linux/check_syscall.py index b6634d612..5e3e40cbb 100644 --- a/volatility3/framework/plugins/linux/check_syscall.py +++ b/volatility3/framework/plugins/linux/check_syscall.py @@ -1,208 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" -import contextlib import logging -from typing import List - -from volatility3.framework import constants, exceptions, interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import format_hints +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_syscall vollog = logging.getLogger(__name__) -try: - import capstone - has_capstone = True -except ImportError: - has_capstone = False - - -class Check_syscall(plugins.PluginInterface): - """Check system call table for hooks.""" +class Check_syscall( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_syscall.Check_syscall, + removal_date="2026-06-07", +): + """Check system call table for hooks (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - ] - - def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux): - """Returns the size of the table based on the next symbol.""" - ret = 0 - - symbol_list = [] - for sn in vmlinux.symbols: - with contextlib.suppress(exceptions.SymbolError): - # When requesting the symbol from the module, a full resolve is performed - symbol_list.append((vmlinux.get_symbol(sn).address, sn)) - sorted_symbols = sorted(symbol_list) - - sym_address = 0 - - for tmp_sym_address, sym_name in sorted_symbols: - if tmp_sym_address > table_addr: - sym_address = tmp_sym_address - break - - if sym_address > 0: - ret = int((sym_address - table_addr) / ptr_sz) - - return ret - - def _get_table_size_meta(self, vmlinux): - """returns the number of symbols that start with __syscall_meta__ this - is a fast way to determine the number of system calls, but not the most - accurate.""" - - return len( - [ - sym - for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols - if sym.startswith("__syscall_meta__") - ] - ) - - def _get_table_info_other(self, table_addr, ptr_sz, vmlinux): - table_size_meta = self._get_table_size_meta(vmlinux) - table_size_syms = self._get_table_size_next_symbol(table_addr, ptr_sz, vmlinux) - - sizes = [size for size in [table_size_meta, table_size_syms] if size > 0] - - table_size = min(sizes) - - return table_size - - def _get_table_info_disassembly(self, ptr_sz, vmlinux): - """Find the size of the system call table by disassembling functions - that immediately reference it in their first instruction This is in the - form 'cmp reg,NR_syscalls'.""" - table_size = 0 - - if not has_capstone: - return table_size - - if ptr_sz == 4: - syscall_entry_func = "sysenter_do_call" - mode = capstone.CS_MODE_32 - else: - syscall_entry_func = "system_call_fastpath" - mode = capstone.CS_MODE_64 - - md = capstone.Cs(capstone.CS_ARCH_X86, mode) - - try: - func_addr = vmlinux.get_symbol(syscall_entry_func).address - except exceptions.SymbolError as e: - # if we can't find the disassemble function then bail and rely on a different method - return 0 - - vmlinux = self.context.modules[self.config["kernel"]] - data = self.context.layers.read(vmlinux.layer_name, func_addr, 6) - - for address, size, mnemonic, op_str in md.disasm_lite(data, func_addr): - if mnemonic == "CMP": - table_size = int(op_str.split(",")[1].strip()) & 0xFFFF - break - - return table_size - - def _get_table_info(self, vmlinux, table_name, ptr_sz): - table_sym = vmlinux.get_symbol(table_name) - - table_size = self._get_table_info_disassembly(ptr_sz, vmlinux) - - if table_size == 0: - table_size = self._get_table_info_other(table_sym.address, ptr_sz, vmlinux) - - if table_size == 0: - vollog.error("Unable to get system call table size") - return 0, 0 - - return table_sym.address, table_size - - # TODO - add finding and parsing unistd.h once cached file enumeration is added - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - ptr_sz = vmlinux.get_type("pointer").size - if ptr_sz == 4: - table_name = "32bit" - else: - table_name = "64bit" - - try: - table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz) - except exceptions.SymbolError: - vollog.error("Unable to find the system call table. Exiting.") - return None - - tables = [(table_name, table_info)] - - # this table is only present on 64 bit systems with 32 bit emulation - # enabled in order to support 32 bit programs and libraries - # if the symbol isn't there then the support isn't in the kernel and so we skip it - try: - ia32_symbol = vmlinux.get_symbol("ia32_sys_call_table") - except exceptions.SymbolError: - ia32_symbol = None - - if ia32_symbol is not None: - ia32_info = self._get_table_info(vmlinux, "ia32_sys_call_table", ptr_sz) - tables.append(("32bit", ia32_info)) - - for table_name, (tableaddr, tblsz) in tables: - table = vmlinux.object( - object_type="array", - subtype=vmlinux.get_type("pointer"), - offset=tableaddr, - count=tblsz, - ) - - for i, call_addr in enumerate(table): - if not call_addr: - continue - - symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr)) - - if len(symbols) > 0: - sym_name = ( - str(symbols[0].split(constants.BANG)[1]) - if constants.BANG in symbols[0] - else str(symbols[0]) - ) - else: - sym_name = "UNKNOWN" - - yield ( - 0, - ( - format_hints.Hex(tableaddr), - table_name, - i, - format_hints.Hex(call_addr), - sym_name, - ), - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Table Address", format_hints.Hex), - ("Table Name", str), - ("Index", int), - ("Handler Address", format_hints.Hex), - ("Handler Symbol", str), - ], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/elfs.py b/volatility3/framework/plugins/linux/elfs.py index 22e39d127..34a8d0e7e 100644 --- a/volatility3/framework/plugins/linux/elfs.py +++ b/volatility3/framework/plugins/linux/elfs.py @@ -1,8 +1,8 @@ # 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 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" +"""A module containing a plugin for enumerating memory-mapped +ELF files across all processes.""" import logging from typing import List, Optional, Type @@ -14,7 +14,7 @@ from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux.extensions import elf -from volatility3.framework.constants.linux import ELF_MAX_EXTRACTION_SIZE +from volatility3.framework.constants import linux as linux_constants from volatility3.plugins.linux import pslist @@ -25,7 +25,7 @@ class Elfs(plugins.PluginInterface): """Lists all memory mapped ELF files for all processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 1) + _version = (2, 0, 3) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -35,8 +35,8 @@ class Elfs(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pid", @@ -116,7 +116,7 @@ class Elfs(plugins.PluginInterface): real_size = end - start # Check if ELF has a legitimate size - if real_size < 0 or real_size > ELF_MAX_EXTRACTION_SIZE: + if real_size < 0 or real_size > linux_constants.ELF_MAX_EXTRACTION_SIZE: raise ValueError(f"The claimed size of the ELF is invalid: {real_size}") sections[start] = real_size @@ -177,7 +177,7 @@ class Elfs(plugins.PluginInterface): name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), - path, + path or renderers.NotAvailableValue(), file_output, ), ) diff --git a/volatility3/framework/plugins/linux/envars.py b/volatility3/framework/plugins/linux/envars.py index 5cbf0f502..f4859cb49 100644 --- a/volatility3/framework/plugins/linux/envars.py +++ b/volatility3/framework/plugins/linux/envars.py @@ -3,8 +3,9 @@ # import logging +from typing import Iterable, Tuple -from volatility3.framework import exceptions, renderers +from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -16,7 +17,8 @@ vollog = logging.getLogger(__name__) class Envars(plugins.PluginInterface): """Lists processes with their environment variables""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 13, 0) + _version = (2, 0, 1) @classmethod def get_requirements(cls): @@ -27,8 +29,8 @@ class Envars(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pid", @@ -38,84 +40,101 @@ class Envars(plugins.PluginInterface): ), ] + @classmethod + def get_task_env_variables( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + env_area_max_size: int = 8192, + ) -> Iterable[Tuple[str, str]]: + """Yields environment variables for a given task. + + Args: + context: The plugin's operational context. + task: The task object from which to extract environment variables. + env_area_max_size: Maximum allowable size for the environment variables area. + Tasks exceeding this size will be skipped. Default is 8192. + + Yields: + Tuples of (key, value) representing each environment variable. + """ + + task_name = utility.array_to_string(task.comm) + task_pid = task.pid + env_start = task.mm.env_start + env_end = task.mm.env_end + env_area_size = env_end - env_start + if not (0 < env_area_size <= env_area_max_size): + vollog.debug( + f"Task {task_pid} {task_name} appears to have environment variables of size " + f"{env_area_size} bytes which fails the sanity checking, will not extract " + "any envars." + ) + return None + + # Get process layer to read envars from + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + proc_layer = context.layers[proc_layer_name] + + # Ensure the entire buffer is readable to prevent relying on exception handling + if not proc_layer.is_valid(env_start, env_area_size): + # Not mapped / swapped out + vollog.debug( + f"Unable to read environment variables for {task_pid} {task_name} starting at " + f" virtual address 0x{env_start:x} for {env_area_size} bytes, will not " + "extract any envars." + ) + return None + + # Read the full task environment variable buffer. + envar_data = proc_layer.read(env_start, env_area_size) + + # Parse envar data, envars are null terminated, keys and values are separated by '=' + envar_data = envar_data.rstrip(b"\x00") + for envar_pair in envar_data.split(b"\x00"): + try: + env_key, env_value = envar_pair.decode( + encoding="utf8", errors="replace" + ).split("=", 1) + except ValueError: + # Some legitimate programs, like 'avahi-daemon', avoid reallocating the args + # and instead exploit the fact that the environment variables area is contiguous + # to the args. This allows them to include a longer process name in the listing, + # causing overwrites and incorrect results. In such cases, it's better to abort + # the current task rather than displaying misleading or incorrect output. + break + + yield env_key, env_value + def _generator(self, tasks): """Generates a listing of processes along with environment variables""" # walk the process list and return the envars for task in tasks: - pid = task.pid - - # get process name as string - name = utility.array_to_string(task.comm) - - # try and get task parent - try: - ppid = task.parent.pid - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to read parent pid for task {pid} {name}, setting ppid to 0." - ) - ppid = 0 - - # kernel threads never have an mm as they do not have userland mappings - try: - mm = task.mm - except exceptions.InvalidAddressException: - # no mm so cannot get envars - vollog.debug( - f"Unable to access mm for task {pid} {name} it is likely a kernel thread, will not extract any envars." - ) - mm = None + if task.is_kernel_thread: continue - # if mm exists attempt to get envars - if mm: - # get process layer to read envars from - proc_layer_name = task.add_process_layer() - if proc_layer_name is None: - vollog.debug( - f"Unable to construct process layer for task {pid} {name}, will not extract any envars." - ) - continue - proc_layer = self.context.layers[proc_layer_name] + task_pid = task.pid + task_name = utility.array_to_string(task.comm) + task_ppid = task.get_parent_pid() - # get the size of the envars with sanity checking - envars_size = task.mm.env_end - task.mm.env_start - if not (0 < envars_size <= 8192): - vollog.debug( - f"Task {pid} {name} appears to have envars of size {envars_size} bytes which fails the sanity checking, will not extract any envars." - ) - continue - - # attempt to read all envars data - try: - envar_data = proc_layer.read(task.mm.env_start, envars_size) - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to read full envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)} for {envars_size} bytes, will not extract any envars." - ) - continue - - # parse envar data, envars are null terminated, keys and values are separated by '=' - envar_data = envar_data.rstrip(b"\x00") - for envar_pair in envar_data.split(b"\x00"): - try: - key, value = envar_pair.decode().split("=", 1) - except ValueError: - vollog.debug( - f"Unable to extract envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)}, they don't appear to be '=' separated" - ) - continue - yield (0, (pid, ppid, name, key, value)) + for env_key, env_value in self.get_task_env_variables(self.context, task): + yield (0, (task_pid, task_ppid, task_name, env_key, env_value)) def run(self): filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [("PID", int), ("PPID", int), ("COMM", str), ("KEY", str), ("VALUE", str)], - self._generator( - pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=filter_func - ) - ), + tasks = pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func ) + + headers = [ + ("PID", int), + ("PPID", int), + ("COMM", str), + ("KEY", str), + ("VALUE", str), + ] + + return renderers.TreeGrid(headers, self._generator(tasks)) diff --git a/volatility3/framework/plugins/linux/graphics/__init__.py b/volatility3/framework/plugins/linux/graphics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/volatility3/framework/plugins/linux/graphics/fbdev.py b/volatility3/framework/plugins/linux/graphics/fbdev.py new file mode 100644 index 000000000..ebdbd1706 --- /dev/null +++ b/volatility3/framework/plugins/linux/graphics/fbdev.py @@ -0,0 +1,334 @@ +# 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 +# +import logging +import io + +from dataclasses import dataclass +from typing import Type, List, Dict, Tuple +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.objects import utility +from volatility3.framework.constants import architectures +from volatility3.framework.symbols import linux + +# Image manipulation functions are kept in the plugin, +# to prevent a general exit on missing PIL (pillow) dependency. +try: + from PIL import Image + + has_pil = True +except ImportError: + has_pil = False + +vollog = logging.getLogger(__name__) + + +@dataclass +class Framebuffer: + """Framebuffer object internal representation. This is useful to unify a framebuffer with precalculated + properties and pass it through functions conveniently.""" + + id: str + xres_virtual: int + yres_virtual: int + line_length: int + bpp: int + """Bits Per Pixel""" + size: int + color_fields: Dict[str, Tuple[int, int, int]] + fb_info: interfaces.objects.ObjectInterface + + +class Fbdev(interfaces.plugins.PluginInterface): + """Extract framebuffers from the fbdev graphics subsystem""" + + _version = (1, 0, 0) + _required_framework_version = (2, 11, 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="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Dump framebuffers", + default=False, + optional=True, + ), + ] + + @classmethod + def parse_fb_pixel_bitfields( + cls, fb_var_screeninfo: interfaces.objects.ObjectInterface + ) -> Dict[str, Tuple[int, int, int]]: + """Organize a framebuffer pixel format into a dictionary. + This is needed to know the position and bitlength of a color inside + a pixel. + + Args: + fb_var_screeninfo: a fb_var_screeninfo kernel object instance + + Returns: + The color fields mappings + + Documentation: + include/uapi/linux/fb.h: + struct fb_bitfield { + __u32 offset; /* beginning of bitfield */ + __u32 length; /* length of bitfield */ + __u32 msb_right; /* != 0 : Most significant bit is right */ + }; + """ + # Naturally order by RGBA + color_mappings = [ + ("R", fb_var_screeninfo.red), + ("G", fb_var_screeninfo.green), + ("B", fb_var_screeninfo.blue), + ("A", fb_var_screeninfo.transp), + ] + color_fields = {} + for color_code, fb_bitfield in color_mappings: + color_fields[color_code] = ( + int(fb_bitfield.offset), + int(fb_bitfield.length), + int(fb_bitfield.msb_right), + ) + return color_fields + + @classmethod + def convert_fb_raw_buffer_to_image( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + fb: Framebuffer, + ): + """Convert raw framebuffer pixels to an image. + + Args: + fb: the relevant Framebuffer object + + Returns: + A PIL Image object + + Documentation: + include/uapi/linux/fb.h: + /* Interpretation of offset for color fields: All offsets are from the right, + * inside a "pixel" value, which is exactly 'bits_per_pixel' wide (means: you + * can use the offset as right argument to <<). A pixel afterwards is a bit + * stream and is written to video memory as that unmodified. + """ + kernel = context.modules[kernel_name] + kernel_layer = context.layers[kernel.layer_name] + + raw_pixels = io.BytesIO(kernel_layer.read(fb.fb_info.screen_base, fb.size)) + bytes_per_pixel = fb.bpp // 8 + image = Image.new("RGBA", (fb.xres_virtual, fb.yres_virtual)) + + # This is not designed to be extremely fast (numpy isn't available), + # but convenient and dynamic for any color field layout. + for y in range(fb.yres_virtual): + for x in range(fb.xres_virtual): + raw_pixel = int.from_bytes(raw_pixels.read(bytes_per_pixel), "little") + pixel = [0, 0, 0, 255] + # The framebuffer is expected to have been correctly constructed, + # especially by parse_fb_pixel_bitfields, to get the needed RGBA mappings. + for i, color_code in enumerate(["R", "G", "B", "A"]): + offset, length, msb_right = fb.color_fields[color_code] + if length == 0: + continue + color_value = (raw_pixel >> offset) & (2**length - 1) + if msb_right: + # Reverse bit order + color_value = int( + "{:0{length}b}".format(color_value, length=length)[::-1], 2 + ) + pixel[i] = color_value + image.putpixel((x, y), tuple(pixel)) + + return image + + @classmethod + def dump_fb( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], + fb: Framebuffer, + convert_to_png_image: bool, + ) -> str: + """Dump a Framebuffer buffer to disk. + + Args: + fb: the relevant Framebuffer object + convert_to_image: a boolean specifying if the buffer should be converted to an image + + Returns: + The filename of the dumped buffer. + """ + kernel = context.modules[kernel_name] + kernel_layer = context.layers[kernel.layer_name] + id = "N-A" if isinstance(fb.id, renderers.NotAvailableValue) else fb.id + base_filename = f"{id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp" + if convert_to_png_image: + image_object = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb) + raw_io_output = io.BytesIO() + image_object.save(raw_io_output, "PNG") + final_fb_buffer = raw_io_output.getvalue() + filename = f"{base_filename}.png" + else: + final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size) + filename = f"{base_filename}.raw" + + with open_method(filename) as fp: + fp.write(final_fb_buffer) + return fp.preferred_filename + + @classmethod + def parse_fb_info( + cls, + fb_info: interfaces.objects.ObjectInterface, + ) -> Framebuffer: + """Parse an fb_info struct + Args: + fb_info: an fb_info kernel object live instance + + Returns: + A Framebuffer object + + Documentation: + https://docs.kernel.org/fb/api.html: + - struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format. + Those information can't be directly modified by applications, but can be changed by the driver when an application modifies the format. + - struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode, + as well as other miscellaneous parameters. + """ + id = utility.array_to_string(fb_info.fix.id) or renderers.NotAvailableValue() + color_fields = None + + # 0 = color, 1 = grayscale, >1 = FOURCC + if fb_info.var.grayscale in [0, 1]: + color_fields = cls.parse_fb_pixel_bitfields(fb_info.var) + + # There a lot of tricky pixel formats used by drivers and vendors in include/uapi/linux/videodev2.h. + # As Volatility3 is not a video format converter, it is best to play it safe and let the user parse + # the raw data manually (with ffmpeg for example). + elif fb_info.var.grayscale > 1: + fourcc = linux.LinuxUtilities.convert_fourcc_code(fb_info.var.grayscale) + warn_msg = f"""Framebuffer "{id}" uses a FOURCC pixel format "{fourcc}" that isn't natively supported. +You can try using ffmpeg to decode the raw buffer. Example usage: +"ffmpeg -pix_fmts" to list supported formats, then +"ffmpeg -f rawvideo -video_size {fb_info.var.xres_virtual}x{fb_info.var.yres_virtual} -i .raw -pix_fmt output.png".""" + vollog.warning(warn_msg) + + # Prefer using the virtual resolution, instead of the visible one. + # This prevents missing non-visible data stored in the framebuffer. + fb = Framebuffer( + id, + xres_virtual=fb_info.var.xres_virtual, + yres_virtual=fb_info.var.yres_virtual, + line_length=fb_info.fix.line_length, + bpp=fb_info.var.bits_per_pixel, + size=fb_info.var.yres_virtual * fb_info.fix.line_length, + color_fields=color_fields, + fb_info=fb_info, + ) + + 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." + ) + return + + kernel_name = self.config["kernel"] + kernel = self.context.modules[kernel_name] + + if not kernel.has_symbol("num_registered_fb"): + vollog.error( + '"num_registered_fb" symbol does not exist in the symbol table. This means you are either analyzing an unsupported kernel version, your symbol table is corrupt, or the fbdev driver is compiled as a kernel module.' + ) + return + + try: + num_registered_fb = kernel.object_from_symbol("num_registered_fb") + except exceptions.SymbolError: + vollog.error( + 'Creating an object from "num_registered_fb" caused a symbol error. This is a sign that the symbol table is outdated. Please re-generate your symbol table using the latest dwarf2json' + ) + return + + if num_registered_fb < 1: + vollog.info("No registered framebuffer in the fbdev API.") + return + + registered_fb = kernel.object_from_symbol("registered_fb") + fb_info_list = utility.array_of_pointers( + registered_fb, + num_registered_fb, + kernel.symbol_table_name + constants.BANG + "fb_info", + self.context, + ) + + for fb_info in fb_info_list: + fb = self.parse_fb_info(fb_info) + file_output = "Disabled" + if self.config["dump"]: + try: + file_output = self.dump_fb( + self.context, kernel_name, self.open, fb, bool(fb.color_fields) + ) + file_output = str(file_output) + except exceptions.InvalidAddressException as excp: + vollog.error( + f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".' + ) + file_output = renderers.UnreadableValue() + + try: + fb_device_name = utility.pointer_to_string( + fb.fb_info.dev.kobj.name, 256 + ) + except exceptions.InvalidAddressException: + fb_device_name = renderers.NotAvailableValue() + + yield ( + 0, + ( + format_hints.Hex(fb.fb_info.screen_base), + fb_device_name, + fb.id, + fb.size, + f"{fb.xres_virtual}x{fb.yres_virtual}", + fb.bpp, + "RUNNING" if fb.fb_info.state == 0 else "SUSPENDED", + file_output, + ), + ) + + def run(self): + columns = [ + ("Address", format_hints.Hex), + ("Device", str), + ("ID", str), + ("Size", int), + ("Virtual resolution", str), + ("BPP", int), + ("State", str), + ("Filename", str), + ] + + return renderers.TreeGrid( + columns, + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index fd4b28943..f7bdd6b0b 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -1,246 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Set, Tuple, Iterable -from volatility3.framework import renderers, interfaces, exceptions, objects -from volatility3.framework.constants import architectures -from volatility3.framework.renderers import format_hints -from volatility3.framework.configuration import requirements -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import hidden_modules vollog = logging.getLogger(__name__) -class Hidden_modules(interfaces.plugins.PluginInterface): - """Carves memory to find hidden kernel modules""" +class Hidden_modules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=hidden_modules.Hidden_modules, + removal_date="2026-06-07", +): + """Carves memory to find hidden kernel modules (deprecated).""" - _required_framework_version = (2, 10, 0) - - _version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=architectures.LINUX_ARCHS, - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), - ] - - @staticmethod - def get_modules_memory_boundaries( - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> Tuple[int]: - """Determine the boundaries of the module allocation area - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - A tuple containing the minimum and maximum addresses for the module allocation area. - """ - vmlinux = context.modules[vmlinux_module_name] - if vmlinux.has_symbol("mod_tree"): - # Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca - mod_tree = vmlinux.object_from_symbol("mod_tree") - modules_addr_min = mod_tree.addr_min - modules_addr_max = mod_tree.addr_max - elif vmlinux.has_symbol("module_addr_min"): - # 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db - modules_addr_min = vmlinux.object_from_symbol("module_addr_min") - modules_addr_max = vmlinux.object_from_symbol("module_addr_max") - - if isinstance(modules_addr_min, objects.Void): - raise exceptions.VolatilityException( - "Your ISF symbols lack type information. You may need to update the" - "ISF using the latest version of dwarf2json" - ) - else: - raise exceptions.VolatilityException( - "Cannot find the module memory allocation area. Unsupported kernel" - ) - - return modules_addr_min, modules_addr_max - - @classmethod - def _get_module_address_alignment( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> int: - """Obtain the module memory address alignment. - - struct module is aligned to the L1 cache line, which is typically 64 bytes for most - common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this - will still work. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - The struct module alignment - """ - # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata - # Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be - # essential for retrieving type metadata in the future. - return 64 - - @staticmethod - def _validate_alignment_patterns( - addresses: Iterable[int], - address_alignment: int, - ) -> bool: - """Check if the memory addresses meet our alignments patterns - - Args: - addresses: Iterable with the address values - address_alignment: Number of bytes for alignment validation - - Returns: - True if all the addresses meet the alignment - """ - return all(addr % address_alignment == 0 for addr in addresses) - - @classmethod - def get_hidden_modules( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - known_module_addresses: Set[int], - modules_memory_boundaries: Tuple, - ) -> Iterable[interfaces.objects.ObjectInterface]: - """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 - in Volatility2, but it doesn't work with older kernels. - - From kernels 4.2 struct module allocation are aligned to the L1 cache line size. - In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in - the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can - also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json - doesn't support this feature yet. - In kernels < 4.2, alignment attributes are absent in the struct module, meaning - alignment cannot be guaranteed. Therefore, for older kernels, it's better to use - the traditional scan technique. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - known_module_addresses: Set with known module addresses - modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - Yields: - module objects - """ - vmlinux = context.modules[vmlinux_module_name] - vmlinux_layer = context.layers[vmlinux.layer_name] - - module_addr_min, module_addr_max = modules_memory_boundaries - module_address_alignment = cls._get_module_address_alignment( - context, vmlinux_module_name - ) - if not cls._validate_alignment_patterns( - known_module_addresses, module_address_alignment - ): - vollog.warning( - f"Module addresses aren't aligned to {module_address_alignment} bytes. " - "Switching to 1 byte aligment scan method." - ) - module_address_alignment = 1 - - mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") - mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") - offset_to_mkobj_mod = mkobj_offset + mod_offset - mod_member_template = vmlinux.get_type("module_kobject").child_template("mod") - mod_size = mod_member_template.size - mod_member_data_format = mod_member_template.data_format - - for module_addr in range( - module_addr_min, module_addr_max, module_address_alignment - ): - if module_addr in known_module_addresses: - continue - - try: - # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() - self_referential_bytes = vmlinux_layer.read( - module_addr + offset_to_mkobj_mod, mod_size - ) - self_referential = objects.convert_data_to_value( - self_referential_bytes, int, mod_member_data_format - ) - if self_referential != module_addr: - continue - except ( - exceptions.PagedInvalidAddressException, - exceptions.InvalidAddressException, - ): - continue - - module = vmlinux.object("module", offset=module_addr, absolute=True) - if module and module.is_valid(): - yield module - - @classmethod - def get_lsmod_module_addresses( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> Set[int]: - """Obtain a set the known module addresses from linux.lsmod plugin - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - A set containing known kernel module addresses - """ - vmlinux = context.modules[vmlinux_module_name] - vmlinux_layer = context.layers[vmlinux.layer_name] - - known_module_addresses = { - vmlinux_layer.canonicalize(module.vol.offset) - for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) - } - return known_module_addresses - - def _generator(self): - vmlinux_module_name = self.config["kernel"] - known_module_addresses = self.get_lsmod_module_addresses( - self.context, vmlinux_module_name - ) - modules_memory_boundaries = self.get_modules_memory_boundaries( - self.context, vmlinux_module_name - ) - for module in self.get_hidden_modules( - self.context, - vmlinux_module_name, - known_module_addresses, - modules_memory_boundaries, - ): - module_addr = module.vol.offset - module_name = module.get_name() or renderers.NotAvailableValue() - fields = (format_hints.Hex(module_addr), module_name) - yield (0, fields) - - def run(self): - if self.context.symbol_space.verify_table_versions( - "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) - ): - raise exceptions.SymbolSpaceError( - "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" - ) - - headers = [ - ("Address", format_hints.Hex), - ("Name", str), - ] - return renderers.TreeGrid(headers, self._generator()) + _required_framework_version = (2, 25, 0) + _version = (3, 0, 2) diff --git a/volatility3/framework/plugins/linux/iomem.py b/volatility3/framework/plugins/linux/iomem.py index 6732084db..5be6627fc 100644 --- a/volatility3/framework/plugins/linux/iomem.py +++ b/volatility3/framework/plugins/linux/iomem.py @@ -59,7 +59,7 @@ class IOMem(interfaces.plugins.PluginInterface): f"Unable to create resource object at {resource_offset:#x}. This resource, " "its sibling, and any of it's children and will be missing from the output." ) - return None + return # get name with protection against smear as following a pointer try: @@ -71,6 +71,15 @@ class IOMem(interfaces.plugins.PluginInterface): ) name = renderers.UnreadableValue() + try: + start = resource.start + end = resource.end + except exceptions.InvalidAddressException: + vollog.warning( + f"Unable to follow pointer to start and end for resource object at {resource_offset:#x}. Skipping entry." + ) + return + # mark this resource as seen in the seen set. Normally this should not be needed but will protect # against possible infinite loops. Warn the user if an infinite loop would have happened. if resource_offset in seen: @@ -79,12 +88,12 @@ class IOMem(interfaces.plugins.PluginInterface): "this should not normally occur. No further results from related resources will be " "displayed to protect against infinite loops." ) - return None + return else: seen.add(resource_offset) # yield information on this resource - yield depth, (name, resource.start, resource.end) + yield depth, (name, start, end) # process child resource if this exists if resource.child != 0: diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py new file mode 100644 index 000000000..164fe62dd --- /dev/null +++ b/volatility3/framework/plugins/linux/ip.py @@ -0,0 +1,214 @@ +# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import List +from volatility3.framework import interfaces, renderers, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols.linux import network +from volatility3.framework.symbols.linux.extensions import network as net_extensions + + +class Addr(plugins.PluginInterface): + """Lists network interface information for all devices""" + + _required_framework_version = (2, 22, 0) + + _version = (1, 0, 2) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="Net", component=network.NetSymbols, version=(1, 0, 0) + ), + ] + + def _gather_net_dev_info(self, net_dev: net_extensions.net_device): + mac_addr = net_dev.get_mac_address() + promisc = net_dev.promisc + operational_state = net_dev.get_operational_state() + iface_name = net_dev.get_device_name() + iface_ifindex = net_dev.ifindex + try: + net_ns_id = net_dev.get_net_namespace_id() + except AttributeError: + net_ns_id = None + + # Interface IPv4 Addresses + in_device = net_dev.ip_ptr.dereference().cast("in_device") + for in_ifaddr in in_device.get_addresses(): + 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, + ) + + # Interface IPv6 Addresses + inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev") + for inet6_ifaddr in inet6_dev.get_addresses(): + 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, + ) + + def _enumerate_net_namespace_list(self): + vmlinux = self.context.modules[self.config["kernel"]] + + net_type_symname = vmlinux.symbol_table_name + constants.BANG + "net" + net_device_symname = vmlinux.symbol_table_name + constants.BANG + "net_device" + network.NetSymbols.apply(self.context.symbol_space[vmlinux.symbol_table_name]) + + # 'net_namespace_list' exists from kernels >= 2.6.24 + net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") + for net_ns in net_namespace_list.to_list(net_type_symname, "list"): + yield from net_ns.dev_base_head.to_list(net_device_symname, "dev_list") + + def _generator(self): + for net_dev in self._enumerate_net_namespace_list(): + for ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + 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, + ), + ) + + def run(self): + headers = [ + ("NetNS", int), + ("Index", int), + ("Interface", str), + ("MAC", str), + ("Promiscuous", bool), + ("IP", str), + ("Prefix", int), + ("Scope Type", str), + ("State", str), + ] + + return renderers.TreeGrid(headers, self._generator()) + + +class Link(plugins.PluginInterface): + """Lists information about network interfaces similar to `ip link show`""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="Net", component=network.NetSymbols, version=(1, 0, 0) + ), + ] + + def _gather_net_dev_link_info(self, net_device): + mac_addr = net_device.get_mac_address() + operational_state = net_device.get_operational_state() + iface_name = net_device.get_device_name() + mtu = net_device.mtu + qdisc_name = net_device.get_qdisc_name() + qlen = net_device.get_queue_length() + try: + net_ns_id = net_device.get_net_namespace_id() + except AttributeError: + net_ns_id = renderers.NotAvailableValue() + + # Format flags to string. Drop IFF_ to match iproute2 'ip link' output. + # Also, note that iproute2 removes IFF_RUNNING, see print_link_flags() + flags_list = [ + flag.replace("IFF_", "") + for flag in net_device.get_flag_names() + if flag != "IFF_RUNNING" + ] + 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, + ) + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + network.NetSymbols.apply(self.context.symbol_space[vmlinux.symbol_table_name]) + + net_type_symname = vmlinux.symbol_table_name + constants.BANG + "net" + net_device_symname = vmlinux.symbol_table_name + constants.BANG + "net_device" + + # 'net_namespace_list' exists from kernels >= 2.6.24 + net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") + for net_ns in net_namespace_list.to_list(net_type_symname, "list"): + for net_dev in net_ns.dev_base_head.to_list(net_device_symname, "dev_list"): + for fields in self._gather_net_dev_link_info(net_dev): + yield 0, fields + + def run(self): + headers = [ + ("NS", int), + ("Interface", str), + ("MAC", str), + ("State", str), + ("MTU", int), + ("Qdisc", str), + ("Qlen", int), + ("Flags", str), + ] + + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/kallsyms.py b/volatility3/framework/plugins/linux/kallsyms.py new file mode 100644 index 000000000..c8bca03f7 --- /dev/null +++ b/volatility3/framework/plugins/linux/kallsyms.py @@ -0,0 +1,150 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Union + +from volatility3.framework import interfaces, renderers +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.constants import architectures +from volatility3.framework.symbols.linux import kallsyms + + +vollog = logging.getLogger(__name__) + + +class Kallsyms(plugins.PluginInterface): + """Kallsyms symbols enumeration plugin. + + If no arguments are provided, all symbols are included: core, modules, ftrace, and BPF. + Alternatively, you can use any combination of --core, --modules, --ftrace, and --bpf + to customize the output. + """ + + _required_framework_version = (2, 19, 0) + + _version = (1, 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="Kallsyms", component=kallsyms.Kallsyms, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="core", + description="Include core symbols", + default=False, + optional=True, + ), + requirements.BooleanRequirement( + name="modules", + description="Include module symbols", + default=False, + optional=True, + ), + requirements.BooleanRequirement( + name="ftrace", + description="Include ftrace symbols", + default=False, + optional=True, + ), + requirements.BooleanRequirement( + name="bpf", + description="Include BPF symbols", + default=False, + optional=True, + ), + ] + + def _get_symbol_size( + self, kassymbol: kallsyms.KASSymbol + ) -> Union[int, interfaces.renderers.BaseAbsentValue]: + # Symbol sizes are calculated using the address of the next non-aliased + # symbol or the end of the kernel text area _end/_etext. However, some kernel + # symbols live beyond that area. For these symbols, the size will be negative, + # resulting in incorrect values. Unfortunately, there isn't much that can be done + # in such cases. + # See comments on .init.scratch in arch/x86/kernel/vmlinux.lds.S for details + if not kassymbol or not kassymbol.size: + return renderers.NotAvailableValue() + + return kassymbol.size if kassymbol.size >= 0 else renderers.NotAvailableValue() + + def _generator(self): + module_name = self.config["kernel"] + vmlinux = self.context.modules[module_name] + + kas = kallsyms.Kallsyms( + context=self.context, + layer_name=vmlinux.layer_name, + module_name=module_name, + ) + + include_core = self.config.get("core", False) + include_modules = self.config.get("modules", False) + include_ftrace = self.config.get("ftrace", False) + include_bpf = self.config.get("bpf", False) + + symbols_flags = (include_core, include_modules, include_ftrace, include_bpf) + if not any(symbols_flags): + include_core = include_modules = include_ftrace = include_bpf = True + + symbol_generators = [] + + if include_core: + symbol_generators.append(kas.get_core_symbols()) + if include_modules: + symbol_generators.append(kas.get_modules_symbols()) + if include_ftrace: + symbol_generators.append(kas.get_ftrace_symbols()) + if include_bpf: + symbol_generators.append(kas.get_bpf_symbols()) + + for symbols_generator in symbol_generators: + for kassymbol in symbols_generator: + if not kassymbol: + continue + # Symbol sizes are calculated using the address of the next non-aliased + # symbol or the end of the kernel text area _end/_etext. However, some kernel + # symbols are located beyond that area, which causes this method to fail for + # the last symbol, resulting in a negative size. + # See comments on .init.scratch in arch/x86/kernel/vmlinux.lds.S for details + symbol_size = self._get_symbol_size(kassymbol) + + if kassymbol.exported is None: + exported = renderers.NotAvailableValue() + else: + exported = kassymbol.exported + + fields = ( + format_hints.Hex(kassymbol.address), + kassymbol.type or renderers.NotAvailableValue(), + symbol_size, + exported, + kassymbol.subsystem, + kassymbol.module_name, + kassymbol.name, + kassymbol.type_description or renderers.NotAvailableValue(), + ) + yield 0, fields + + def run(self): + headers = [ + ("Addr", format_hints.Hex), + ("Type", str), + ("Size", int), + ("Exported", bool), + ("SubSystem", str), + ("ModuleName", str), + ("SymbolName", str), + ("Description", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 72273a77b..72dcc7bad 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -1,79 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging - -from volatility3.framework import interfaces, renderers, exceptions -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import keyboard_notifiers vollog = logging.getLogger(__name__) -class Keyboard_notifiers(interfaces.plugins.PluginInterface): - """Parses the keyboard notifier call chain""" +class Keyboard_notifiers( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=keyboard_notifiers.Keyboard_notifiers, + removal_date="2026-06-07", +): + """Parses the keyboard notifier call chain (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - - try: - knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") - except exceptions.SymbolError: - knl_addr = None - - if not knl_addr: - raise TypeError( - "This plugin requires the keyboard_notifier_list structure. " - "This structure is not present in the supplied symbol table. " - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - knl = vmlinux.object( - object_type="atomic_notifier_head", - offset=knl_addr.vol.offset, - absolute=True, - ) - - for call_back in linux.LinuxUtilities.walk_internal_list( - vmlinux, "notifier_block", "next", knl.head - ): - call_addr = call_back.notifier_call - - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( - vmlinux, handlers, call_addr - ) - - yield (0, [format_hints.Hex(call_addr), module_name, symbol_name]) - - def run(self): - return renderers.TreeGrid( - [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/kmsg.py b/volatility3/framework/plugins/linux/kmsg.py index e26d69543..ba02763d0 100644 --- a/volatility3/framework/plugins/linux/kmsg.py +++ b/volatility3/framework/plugins/linux/kmsg.py @@ -5,7 +5,7 @@ import re import logging from abc import ABC, abstractmethod from enum import Enum -from typing import Generator, Iterator, List, Tuple +from typing import Generator, Iterator, List, Tuple, Optional from volatility3.framework import ( class_subclasses, @@ -73,7 +73,7 @@ class ABCKmsg(ABC): cls, context: interfaces.context.ContextInterface, config: interfaces.configuration.HierarchicalDict, - ) -> Iterator[Tuple[str, str, str, str, str]]: + ) -> Iterator[Tuple[str, str, str, Optional[str], str]]: """It calls each subclass symtab_checks() to test the required conditions to that specific kernel implementation. @@ -108,10 +108,12 @@ class ABCKmsg(ABC): break if kmsg_inst is None: - vollog.error("Unsupported kernel ring buffer implementation") + vollog.error( + "Unsupported kernel ring buffer implementation. Please file a bug on our issue tracker with your specific kernel version." + ) @abstractmethod - def run(self) -> Iterator[Tuple[str, str, str, str, str]]: + def run(self) -> Iterator[Tuple[str, str, str, Optional[str], str]]: """Walks through the specific kernel implementation. Returns: @@ -135,8 +137,14 @@ class ABCKmsg(ABC): bool: True if the kernel being analyzed fulfill the class requirements. """ - def get_string(self, addr: int, length: int) -> str: - txt = self._context.layers[self.layer_name].read(addr, length) # type: ignore + def get_string(self, addr: int, length: int) -> Optional[str]: + layer = self._context.layers[self.layer_name] + if not layer.is_valid(addr, length): + vollog.warning("Failed to read log record at address 0x%x", addr) + return None + + txt = layer.read(addr, length) + return txt.decode(encoding="utf8", errors="replace") def nsec_to_sec_str(self, nsec: int) -> str: @@ -149,27 +157,27 @@ class ABCKmsg(ABC): # This might seem insignificant but it could cause some issues # when compared with userland tool results or when used in # timelines. - return "%lu.%06lu" % (nsec / 1000000000, (nsec % 1000000000) / 1000) + return f"{nsec // 1000000000}.{(nsec % 1000000000) // 1000:06}" def get_timestamp_in_sec_str(self, obj) -> str: # obj could be log, printk_log or printk_info return self.nsec_to_sec_str(obj.ts_nsec) - def get_caller(self, obj): + def get_caller(self, obj) -> Optional[str]: # In some kernel versions, it's only available if CONFIG_PRINTK_CALLER is defined. # caller_id is a member of printk_log struct from 5.1 to the latest 5.9 # From kernels 5.10 on, it's a member of printk_info struct if obj.has_member("caller_id"): return self.get_caller_text(obj.caller_id) - else: - return renderers.NotAvailableValue() - def get_caller_text(self, caller_id): + return None + + def get_caller_text(self, caller_id) -> str: caller_name = "CPU" if caller_id & 0x80000000 else "Task" - caller = "%s(%u)" % (caller_name, caller_id & ~0x80000000) + caller = f"{caller_name}({caller_id & ~0x80000000})" return caller - def get_prefix(self, obj) -> Tuple[int, int, str, str]: + def get_prefix(self, obj) -> Tuple[int, int, str, Optional[str]]: # obj could be log, printk_log or printk_info return ( obj.facility, @@ -207,6 +215,7 @@ class Kmsg_pre_3_5(ABCKmsg): def symtab_checks(cls, vmlinux) -> bool: return ( vmlinux.has_symbol("log_end") + and vmlinux.has_symbol("log_buf_len") and not vmlinux.has_symbol("log_first_idx") and not ( vmlinux.has_type("log") @@ -214,7 +223,7 @@ class Kmsg_pre_3_5(ABCKmsg): ) ) - def run(self) -> Iterator[Tuple[str, str, str, str, str]]: + def run(self) -> Iterator[Tuple[str, str, str, Optional[str], str]]: log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name="log_buf") log_buf_len = self.vmlinux.object_from_symbol(symbol_name="log_buf_len") log_buf = utility.pointer_to_string(log_buf_ptr, count=log_buf_len) @@ -243,7 +252,7 @@ class Kmsg_pre_3_5(ABCKmsg): facility = level_facility >> 3 level_txt = self.get_level_text(level) facility_txt = self.get_facility_text(facility) - caller = renderers.NotAvailableValue() + caller = None yield facility_txt, level_txt, timestamp_str, caller, line @@ -260,10 +269,10 @@ class Kmsg_3_5_to_3_11(ABCKmsg): and vmlinux.has_symbol("log_first_idx") ) - def _get_log_struct_name(self): + def _get_log_struct_name(self) -> str: return "log" - def get_text_from_log(self, msg) -> str: + def get_text_from_log(self, msg) -> Optional[str]: log_struct_name = self._get_log_struct_name() log_struct_size = self.vmlinux.get_type(log_struct_name).size msg_offset = msg.vol.offset + log_struct_size @@ -272,22 +281,27 @@ class Kmsg_3_5_to_3_11(ABCKmsg): def get_log_lines(self, msg) -> Generator[str, None, None]: if msg.text_len > 0: text = self.get_text_from_log(msg) - yield from text.splitlines() + if text: + yield from text.splitlines() def get_dict_lines(self, msg) -> Generator[str, None, None]: if msg.dict_len == 0: - return None + return log_struct_name = self._get_log_struct_name() log_struct_size = self.vmlinux.get_type(log_struct_name).size dict_offset = msg.vol.offset + log_struct_size + msg.text_len - dict_data = self._context.layers[self.layer_name].read( - dict_offset, msg.dict_len - ) - for chunk in dict_data.split(b"\x00"): - yield " " + chunk.decode() + layer = self._context.layers[self.layer_name] + try: + dict_data = layer.read(dict_offset, msg.dict_len) + except exceptions.InvalidAddressException: + vollog.debug("Unable to read kmsg dict from 0x%x", dict_offset) + return - def run(self) -> Iterator[Tuple[str, str, str, str, str]]: + for chunk in dict_data.split(b"\x00"): + yield " " + chunk.decode(encoding="utf8", errors="replace") + + def run(self) -> Iterator[Tuple[str, str, str, Optional[str], str]]: # First, the ring buffer size is determined in the kernel configuration # by CONFIG_LOG_BUF_SHIFT. This static buffer is held in the '__log_buf' # global variable, with 'log_buf' serving as a pointer to it. @@ -300,7 +314,13 @@ class Kmsg_3_5_to_3_11(ABCKmsg): # remains unused. Therefore, it is crucial to read from 'log_buf' rather # than '__log_buf'. - log_buf_ptr = self.vmlinux.object_from_symbol("log_buf") + # This can happen on kernels where log_buf is declared twice + try: + log_buf_ptr = self.vmlinux.object_from_symbol("log_buf") + except exceptions.InvalidAddressException: + vollog.debug("Unable to access `log_buf`. Bailing.") + return + log_buf_len = self.vmlinux.object_from_symbol("log_buf_len") log_first_idx = int(self.vmlinux.object_from_symbol("log_first_idx")) @@ -316,24 +336,31 @@ class Kmsg_3_5_to_3_11(ABCKmsg): while cur_idx < end_idx: msg_offset = log_buf_ptr + cur_idx # type: ignore - msg = self.vmlinux.object(object_type=log_struct_name, offset=msg_offset) - if msg.len == 0: - # As per kernel/printk.c: - # A length == 0 for the next message indicates a wrap-around to - # the beginning of the buffer. - cur_idx = 0 - end_idx = log_next_idx - else: - facility, level, timestamp, caller = self.get_prefix(msg) - level_txt = self.get_level_text(level) - facility_txt = self.get_facility_text(facility) + msg = self.vmlinux.object( + object_type=log_struct_name, offset=msg_offset, absolute=True + ) - for line in self.get_log_lines(msg): - yield facility_txt, level_txt, timestamp, caller, line - for line in self.get_dict_lines(msg): - yield facility_txt, level_txt, timestamp, caller, line + try: + if msg.len == 0: + # As per kernel/printk.c: + # A length == 0 for the next message indicates a wrap-around to + # the beginning of the buffer. + cur_idx = 0 + end_idx = log_next_idx + else: + facility, level, timestamp, caller = self.get_prefix(msg) + level_txt = self.get_level_text(level) + facility_txt = self.get_facility_text(facility) - cur_idx += msg.len + for line in self.get_log_lines(msg): + yield facility_txt, level_txt, timestamp, caller, line + for line in self.get_dict_lines(msg): + yield facility_txt, level_txt, timestamp, caller, line + + cur_idx += msg.len + except exceptions.InvalidAddressException: + vollog.warning("Kmsg buffer msg length could not be read") + return class Kmsg_3_11_to_5_10(Kmsg_3_5_to_3_11): @@ -344,9 +371,14 @@ class Kmsg_3_11_to_5_10(Kmsg_3_5_to_3_11): @classmethod def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_type("printk_log") + return ( + not vmlinux.has_type("printk_ringbuffer") + and vmlinux.has_type("printk_log") + and vmlinux.get_type("printk_log").has_member("ts_nsec") + and vmlinux.has_symbol("log_first_idx") + ) - def _get_log_struct_name(self): + def _get_log_struct_name(self) -> str: return "printk_log" @@ -397,9 +429,9 @@ class Kmsg_5_10_to_(ABCKmsg): @classmethod def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_symbol("prb") + return vmlinux.has_symbol("prb") and vmlinux.has_type("printk_ringbuffer") - def get_text_from_data_ring(self, text_data_ring, desc, info) -> str: + def get_text_from_data_ring(self, text_data_ring, desc, info) -> Optional[str]: text_data_sz = text_data_ring.size_bits text_data_mask = 1 << text_data_sz @@ -408,7 +440,7 @@ class Kmsg_5_10_to_(ABCKmsg): # This record doesn't contain text if begin & 1: - return "" + return None # This means a wrap-around to the beginning of the buffer if begin > end: @@ -427,7 +459,8 @@ class Kmsg_5_10_to_(ABCKmsg): def get_log_lines(self, text_data_ring, desc, info) -> Generator[str, None, None]: text = self.get_text_from_data_ring(text_data_ring, desc, info) - yield from text.splitlines() + if text: + yield from text.splitlines() def get_dict_lines(self, info) -> Generator[str, None, None]: dict_text = utility.array_to_string(info.dev_info.subsystem) @@ -438,7 +471,7 @@ class Kmsg_5_10_to_(ABCKmsg): if dict_text: yield f" DEVICE={dict_text}" - def run(self) -> Iterator[Tuple[str, str, str, str, str]]: + def run(self) -> Iterator[Tuple[str, str, str, Optional[str], str]]: # static struct printk_ringbuffer *prb = &printk_rb_static; ringbuffers = self.vmlinux.object_from_symbol("prb").dereference() @@ -500,7 +533,7 @@ class Kmsg(interfaces.plugins.PluginInterface): _required_framework_version = (2, 6, 0) - _version = (1, 0, 2) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -512,17 +545,31 @@ class Kmsg(interfaces.plugins.PluginInterface): ), ] - def _generator(self) -> Iterator[Tuple[int, Tuple[str, str, str, str, str]]]: - for values in ABCKmsg.run_all(context=self.context, config=self.config): - yield (0, values) + def _generator( + self, + ) -> Iterator[Tuple[int, Tuple[str, str, str, Optional[str], str]]]: + 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, + ), + ) def run(self): if not self.context.symbol_space.verify_table_versions( "dwarf2json", lambda version, _: (not version) or version > (0, 4, 1) ): - raise exceptions.SymbolSpaceError( + vollog.info( "Invalid symbol table, please ensure the ISF table produced by dwarf2json was produced using a version > 0.4.1" ) + return return renderers.TreeGrid( [ diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 2e51b4688..4ed0e15b9 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -4,14 +4,15 @@ import logging from typing import List -from volatility3.framework import constants, exceptions, interfaces, renderers +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import linux from volatility3.framework.constants import architectures from volatility3.framework.objects import utility -from volatility3.plugins.linux import pslist, lsmod +from volatility3.plugins.linux import pslist vollog = logging.getLogger(__name__) @@ -20,8 +21,7 @@ class Kthreads(plugins.PluginInterface): """Enumerates kthread functions""" _required_framework_version = (2, 11, 0) - - _version = (1, 0, 0) + _version = (1, 0, 3) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -31,34 +31,40 @@ class Kthreads(plugins.PluginInterface): description="Linux kernel", architectures=architectures.LINUX_ARCHS, ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 3, 0) - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), ] def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - - kthread_type = vmlinux.get_type( - vmlinux.symbol_table_name + constants.BANG + "kthread" - ) + kthread_type = vmlinux.get_type("kthread") if not kthread_type.has_member("threadfn"): raise exceptions.VolatilityException( "Unsupported kthread implementation. This plugin only works with kernels >= 5.8" ) + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + for task in pslist.PsList.list_tasks( self.context, vmlinux.name, include_threads=True ): @@ -67,38 +73,50 @@ class Kthreads(plugins.PluginInterface): if task.has_member("worker_private"): # kernels >= 5.17 e32cf5dfbe227b355776948b2c9b5691b84d1cbd - ktread_base_pointer = task.worker_private + kthread_base_pointer = task.worker_private else: # 5.8 <= kernels < 5.17 in 52782c92ac85c4e393eb4a903a62e6c24afa633f threadfn # was added to struct kthread. task.set_child_tid is safe on those versions. - ktread_base_pointer = task.set_child_tid + kthread_base_pointer = task.set_child_tid - if not ktread_base_pointer.is_readable(): + if not kthread_base_pointer.is_readable(): continue - kthread = ktread_base_pointer.dereference().cast("kthread") + kthread = kthread_base_pointer.dereference().cast("kthread") threadfn = kthread.threadfn if not (threadfn and threadfn.is_readable()): continue - task_name = utility.array_to_string(task.comm) + thread_name = utility.array_to_string(task.comm) # kernels >= 5.17 in d6986ce24fc00b0638bd29efe8fb7ba7619ed2aa full_name was added to kthread - thread_name = ( - utility.pointer_to_string(kthread.full_name, count=255) - if kthread.has_member("full_name") - else task_name - ) - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( - vmlinux, handlers, threadfn + if kthread.has_member("full_name"): + try: + thread_name = utility.pointer_to_string( + kthread.full_name, count=255 + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"full_name pointer for thread at {kthread.vol.offset:#x} is paged out." + ) + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, threadfn + ) ) + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + fields = [ task.pid, thread_name, format_hints.Hex(threadfn), module_name, - symbol_name, + symbol_name or renderers.NotAvailableValue(), ] yield 0, fields diff --git a/volatility3/framework/plugins/linux/library_list.py b/volatility3/framework/plugins/linux/library_list.py index 062ed078e..dedd77ade 100644 --- a/volatility3/framework/plugins/linux/library_list.py +++ b/volatility3/framework/plugins/linux/library_list.py @@ -21,8 +21,7 @@ class LibraryList(interfaces.plugins.PluginInterface): """Enumerate libraries loaded into processes""" _required_framework_version = (2, 0, 0) - - _version = (1, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls): @@ -32,8 +31,8 @@ class LibraryList(interfaces.plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 2, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pids", diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index a65b0d00b..e0878647f 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -1,17 +1,15 @@ # 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 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" +"""A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import List, Iterable +from typing import Iterable, List -from volatility3.framework import exceptions, renderers, constants, interfaces +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -20,7 +18,9 @@ class Lsmod(plugins.PluginInterface): """Lists loaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (3, 0, 3) + + implementation = linux_utilities_modules.Modules.list_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -28,51 +28,50 @@ class Lsmod(plugins.PluginInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"], + 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=(2, 0, 0), + ), + 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="2026-03-25", + ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str ) -> Iterable[interfaces.objects.ObjectInterface]: - """Lists all the modules in the primary layer. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - vmlinux_symbols: The name of the table containing the kernel symbols - - Yields: - The modules present in the `layer_name` layer's modules list - - This function will throw a SymbolError exception if kernel module support is not enabled. - """ - vmlinux = context.modules[vmlinux_module_name] - - modules = vmlinux.object_from_symbol(symbol_name="modules").cast("list_head") - - table_name = modules.vol.type_name.split(constants.BANG)[0] - - for module in modules.to_list(table_name + constants.BANG + "module", "list"): - yield module - - def _generator(self): - try: - for module in self.list_modules(self.context, self.config["kernel"]): - mod_size = module.get_init_size() + module.get_core_size() - - mod_name = utility.array_to_string(module.name) - - yield 0, (format_hints.Hex(module.vol.offset), mod_name, mod_size) - - except exceptions.SymbolError: - vollog.debug( - "The required symbol 'module' is not present in symbol table. Please check that kernel modules are enabled for the system under analysis." - ) + return linux_utilities_modules.Modules.list_modules( + context, vmlinux_module_name + ) def run(self): return renderers.TreeGrid( - [("Offset", format_hints.Hex), ("Name", str), ("Size", int)], + 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 42b447dfb..75c2a1c2b 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -54,7 +54,7 @@ class FDInternal: """ task: interfaces.objects.ObjectInterface - fd_fields: Tuple[int, int, str] + fd_fields: Tuple[int, interfaces.objects.ObjectInterface, str] def to_user(self) -> FDUser: """Augment the FD information to be presented to the user @@ -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, 0) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -120,8 +120,13 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) @@ -132,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 @@ -140,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 @@ -162,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)) @@ -179,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), @@ -196,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): @@ -215,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/malfind.py b/volatility3/framework/plugins/linux/malfind.py index 18f3dcd56..647e1531a 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -1,114 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -from typing import List import logging -from volatility3.framework import constants, interfaces -from volatility3.framework import renderers, symbols -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.linux import pslist +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import malfind vollog = logging.getLogger(__name__) -class Malfind(interfaces.plugins.PluginInterface): - """Lists process memory ranges that potentially contain injected code.""" +class Malfind( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=malfind.Malfind, + removal_date="2026-06-07", +): + """Lists process memory ranges that potentially contain injected code (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) - ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, - ), - ] - - def _list_injections(self, task): - """Generate memory regions for a process that may contain injected - code.""" - - proc_layer_name = task.add_process_layer() - if not proc_layer_name: - return None - - proc_layer = self.context.layers[proc_layer_name] - - 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 ( - vma.is_suspicious(proc_layer) - and vma.get_name(self.context, task) != "[vdso]" - ): - data = proc_layer.read(vma.vm_start, 64, pad=True) - yield vma, data - - def _generator(self, tasks): - # determine if we're on a 32 or 64 bit kernel - vmlinux = self.context.modules[self.config["kernel"]] - is_32bit_arch = not symbols.symbol_table_is_64bit( - self.context, vmlinux.symbol_table_name - ) - - for task in tasks: - process_name = utility.array_to_string(task.comm) - - for vma, data in self._list_injections(task): - if is_32bit_arch: - architecture = "intel" - else: - architecture = "intel64" - - disasm = interfaces.renderers.Disassembly( - data, vma.vm_start, architecture - ) - - yield ( - 0, - ( - task.pid, - process_name, - format_hints.Hex(vma.vm_start), - format_hints.Hex(vma.vm_end), - vma.get_protection(), - format_hints.HexBytes(data), - disasm, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Start", format_hints.Hex), - ("End", format_hints.Hex), - ("Protection", str), - ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), - ], - self._generator( - pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=filter_func - ) - ), - ) + _version = (1, 0, 3) diff --git a/volatility3/framework/plugins/linux/malware/__init__.py b/volatility3/framework/plugins/linux/malware/__init__.py new file mode 100644 index 000000000..89458befc --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/__init__.py @@ -0,0 +1,8 @@ +# 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 +# +"""All core linux malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/linux/malware/check_afinfo.py b/volatility3/framework/plugins/linux/malware/check_afinfo.py new file mode 100644 index 000000000..4ecf5f788 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_afinfo.py @@ -0,0 +1,216 @@ +# 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 +# +"""A module containing a plugin that verifies the operation function +pointers of network protocols.""" + +import logging +from typing import List, Tuple, Generator + +from volatility3.framework import exceptions, interfaces +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class Check_afinfo(plugins.PluginInterface): + """Verifies the operation function pointers of network protocols.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + @classmethod + def _check_members( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_ops: interfaces.objects.ObjectInterface, + var_name: str, + members: List[str], + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Yields any members that are not pointing inside the kernel + """ + + vmlinux = context.modules[vmlinux_name] + + for check in members: + # redhat-specific garbage + if check.startswith("__UNIQUE_ID_rh_kabi_hide"): + continue + + # These structures have members like `write` and `next`, which are built in Python functions + addr = var_ops.member(attr=check) + + # Unimplemented handlers are set to 0 + if not addr: + continue + + if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0: + yield var_name, check, addr + + @classmethod + def _check_pre_4_18_ops( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_name: str, + var: interfaces.objects.ObjectInterface, + op_members: List[str], + seq_members: List[str], + ): + """ + Finds the correct way to reference `op_members` + """ + vmlinux = context.modules[vmlinux_name] + + if var.has_member("seq_fops"): + yield from cls._check_members( + context, vmlinux_name, var.seq_fops, var_name, op_members + ) + # newer kernels + if var.has_member("seq_ops"): + yield from cls._check_members( + context, vmlinux_name, var.seq_ops, var_name, seq_members + ) + + # this is the most commonly hooked member by rootkits, so a force a check on it + elif var.has_member("seq_show"): + if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0: + yield var_name, "show", var.seq_show + else: + raise exceptions.VolatilityException( + "_check_afinfo_pre_4_18: Unable to find sequence operations members for checking." + ) + + @classmethod + def _check_afinfo_pre_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of < 4.18 systems + """ + tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) + udp = ( + "udp_seq_afinfo", + [ + "udplite6_seq_afinfo", + "udp6_seq_afinfo", + "udplite4_seq_afinfo", + "udp4_seq_afinfo", + ], + ) + protocols = [tcp, udp] + + vmlinux = context.modules[vmlinux_name] + + op_members = vmlinux.get_type("file_operations").members + + # loop through all symbols + for struct_type, global_vars in protocols: + for global_var_name in global_vars: + # this will lookup fail for the IPv6 protocols on kernels without IPv6 support + try: + global_var = vmlinux.object_from_symbol(global_var_name) + except exceptions.SymbolError: + continue + + yield from cls._check_pre_4_18_ops( + context, + vmlinux_name, + global_var_name, + global_var, + op_members, + seq_members, + ) + + @classmethod + def _check_afinfo_post_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of >= 4.18 systems + """ + vmlinux = context.modules[vmlinux_name] + + ops_structs = [ + "raw_seq_ops", + "udp_seq_ops", + "arp_seq_ops", + "unix_seq_ops", + "udp6_seq_ops", + "raw6_seq_ops", + "tcp_seq_ops", + "tcp4_seq_ops", + "tcp6_seq_ops", + "packet_seq_ops", + ] + + for protocol_ops_var in ops_structs: + # These will fail if the particular kernel doesn't have support for a protocol like IPv6 + try: + protocol_ops = vmlinux.object_from_symbol(protocol_ops_var) + except exceptions.SymbolError: + continue + + yield from cls._check_members( + context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members + ) + + @classmethod + def check_afinfo( + cls, context: interfaces.context.ContextInterface, vmlinux_name + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Walks the network protocol operations structures for common network protocols. + Reports any initialized operations members that do not point inside the kernel. + """ + vmlinux = context.modules[vmlinux_name] + + type_check = vmlinux.get_type("tcp_seq_afinfo") + if type_check.has_member("seq_fops"): + checker = cls._check_afinfo_pre_4_18 + else: + checker = cls._check_afinfo_post_4_18 + + seq_members = vmlinux.get_type("seq_operations").members + + yield from checker(context, vmlinux_name, seq_members) + + def _generator(self): + """ + A simple wrapper around `check_afino` + """ + for name, member, address in self.check_afinfo( + self.context, self.config["kernel"] + ): + yield 0, (name, member, format_hints.Hex(address)) + + def run(self): + return renderers.TreeGrid( + [ + ("Symbol Name", str), + ("Member", str), + ("Handler Address", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/check_creds.py b/volatility3/framework/plugins/linux/malware/check_creds.py new file mode 100644 index 000000000..e2b84d679 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_creds.py @@ -0,0 +1,71 @@ +# 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 +# + +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import pslist + + +class Check_creds(interfaces.plugins.PluginInterface): + """Checks if any processes are sharing credential structures""" + + _required_framework_version = (2, 0, 0) + _version = (2, 0, 2) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + type_task = vmlinux.get_type("task_struct") + + if not type_task.has_member("cred"): + raise TypeError( + "This plugin requires the task_struct structure to have a cred member. " + "This member is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + creds = {} + + tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) + + for task in tasks: + task_cred_ptr = task.cred + if not (task_cred_ptr and task_cred_ptr.is_readable()): + continue + + cred_addr = task_cred_ptr.dereference().vol.offset + + creds.setdefault(cred_addr, []) + creds[cred_addr].append(task.pid) + + for cred_addr, pids in creds.items(): + if len(pids) > 1: + pid_str = ", ".join(str(pid) for pid in pids) + + fields = [ + format_hints.Hex(cred_addr), + pid_str, + ] + yield (0, fields) + + def run(self): + headers = [ + ("CredVAddr", format_hints.Hex), + ("PIDs", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/malware/check_idt.py b/volatility3/framework/plugins/linux/malware/check_idt.py new file mode 100644 index 000000000..e199d98d3 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_idt.py @@ -0,0 +1,168 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List, Optional + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Check_idt(interfaces.plugins.PluginInterface): + """Checks if the IDT has been altered""" + + _required_framework_version = (2, 0, 0) + + # 2.0.0 - Add versioning at all, add `get_idt_type` + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + @staticmethod + def get_idt_type(context, vmlinux_name) -> Optional[str]: + """ + Determines the IDT type for this symbol table or returns None + + The original version ended clauses with an `else` leading to bad fall through + of returning a type that did not exist in the symbol table. + + Future updates should not leave fall through cases to avoid this repeating. + """ + + vmlinux = context.modules[vmlinux_name] + + is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) + + # These are in a specific order. Only append to the lists going forward + # or ask Andrew to run tests before merging. + if is_32bit: + idt_types = ["gate_struct", "desc_struct", "gate_struct32"] + else: + idt_types = ["gate_struct64", "gate_struct", "idt_desc"] + + for idt_type in idt_types: + if vmlinux.has_type(idt_type): + return idt_type + + return None + + def _generator(self): + idt_type = self.get_idt_type(self.context, self.config["kernel"]) + if not idt_type: + vollog.error( + "Unable to determine the data structure type for IDT entries. Please file a bug on the GitHub tracker with your kernel version." + ) + return + + vmlinux = self.context.modules[self.config["kernel"]] + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + idt_table_size = 256 + + kernel_layer = self.context.layers[vmlinux.layer_name] + + address_mask = kernel_layer.address_mask + + # hw handlers + system call + check_idxs = list(range(20)) + [128] + + addrs = vmlinux.object_from_symbol("idt_table") + + table = vmlinux.object( + object_type="array", + offset=addrs.vol.offset, + subtype=vmlinux.get_type(idt_type), + count=idt_table_size, + absolute=True, + ) + + for i in check_idxs: + ent = table[i] + + if not ent or not kernel_layer.is_valid(ent.vol.offset): + continue + + if hasattr(ent, "a"): + idt_addr = (ent.b & 0xFFFF0000) | (ent.a & 0x0000FFFF) + else: + low = ent.offset_low + middle = ent.offset_middle + + # offset_high is for 64bit systems + if hasattr(ent, "offset_high"): + high = ent.offset_high + else: + high = 0 + + idt_addr = (high << 32) | (middle << 16) | low + + idt_addr = idt_addr & address_mask + + # 0 means unintialized/unused, not a rootkit + if idt_addr == 0: + module_name = renderers.NotAvailableValue() + symbol_name = renderers.NotAvailableValue() + else: + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, idt_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(i), + format_hints.Hex(idt_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Index", format_hints.Hex), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py new file mode 100644 index 000000000..1834a616e --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -0,0 +1,93 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import Dict, Generator, List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +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 + +vollog = logging.getLogger(__name__) + + +class Check_modules(interfaces.plugins.PluginInterface): + """Compares module list to sysfs info, if available""" + + _version = (3, 0, 1) + _required_framework_version = (2, 0, 0) + + @classmethod + def compare_kset_and_lsmod( + 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 + ) + + lsmod_modules = set( + str(utility.array_to_string(modules.name)) + for modules in linux_utilities_modules.Modules.list_modules( + context=context, vmlinux_module_name=vmlinux_name + ) + ) + + for mod_name in set(kset_modules.keys()).difference(lsmod_modules): + yield kset_modules[mod_name] + + 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, + version=(3, 0, 1), + ), + requirements.VersionRequirement( + name="linux_utilities_modules_module_display_plugin", + component=linux_utilities_modules.ModuleDisplayPlugin, + version=(2, 0, 0), + ), + 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="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 new file mode 100644 index 000000000..6476d6621 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_syscall.py @@ -0,0 +1,216 @@ +# 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 +# +"""A module containing a plugin that checks the system call table for hooks.""" + +import contextlib +import logging +from typing import List + +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + + +class Check_syscall(plugins.PluginInterface): + """Check system call table for hooks.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux): + """Returns the size of the table based on the next symbol.""" + ret = 0 + + symbol_list = [] + for sn in vmlinux.symbols: + with contextlib.suppress(exceptions.SymbolError): + # When requesting the symbol from the module, a full resolve is performed + symbol_list.append((vmlinux.get_symbol(sn).address, sn)) + sorted_symbols = sorted(symbol_list) + + sym_address = 0 + + for tmp_sym_address, sym_name in sorted_symbols: + if tmp_sym_address > table_addr: + sym_address = tmp_sym_address + break + + if sym_address > 0: + ret = int((sym_address - table_addr) / ptr_sz) + + return ret + + def _get_table_size_meta(self, vmlinux): + """returns the number of symbols that start with __syscall_meta__ this + is a fast way to determine the number of system calls, but not the most + accurate.""" + + return len( + [ + sym + for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols + if sym.startswith("__syscall_meta__") + ] + ) + + def _get_table_info_other(self, table_addr, ptr_sz, vmlinux): + table_size_meta = self._get_table_size_meta(vmlinux) + table_size_syms = self._get_table_size_next_symbol(table_addr, ptr_sz, vmlinux) + + sizes = [size for size in [table_size_meta, table_size_syms] if size > 0] + + table_size = min(sizes) + + return table_size + + def _get_table_info_disassembly(self, ptr_sz, vmlinux) -> int: + """Find the size of the system call table by disassembling functions + that immediately reference it in their first instruction This is in the + form 'cmp reg,NR_syscalls'.""" + table_size = 0 + + if not has_capstone: + return table_size + + if ptr_sz == 4: + syscall_entry_func = "sysenter_do_call" + mode = capstone.CS_MODE_32 + else: + syscall_entry_func = "system_call_fastpath" + mode = capstone.CS_MODE_64 + + md = capstone.Cs(capstone.CS_ARCH_X86, mode) + + try: + func_addr = vmlinux.get_symbol(syscall_entry_func).address + except exceptions.SymbolError: + # if we can't find the disassemble function then bail and rely on a different method + return 0 + + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + try: + data = vmlinux_layer.read(func_addr, 6) + except exceptions.InvalidAddressException: + return 0 + + for _address, _size, mnemonic, op_str in md.disasm_lite(data, func_addr): + if mnemonic == "CMP": + table_size = int(op_str.split(",")[1].strip()) & 0xFFFF + break + + return table_size + + def _get_table_info(self, vmlinux, table_name, ptr_sz): + table_sym = vmlinux.get_symbol(table_name) + + table_size = self._get_table_info_disassembly(ptr_sz, vmlinux) + + if table_size == 0: + table_size = self._get_table_info_other(table_sym.address, ptr_sz, vmlinux) + + if table_size == 0: + vollog.error("Unable to get system call table size") + return 0, 0 + + return table_sym.address, table_size + + # TODO - add finding and parsing unistd.h once cached file enumeration is added + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + ptr_sz = vmlinux.get_type("pointer").size + if ptr_sz == 4: + table_name = "32bit" + else: + table_name = "64bit" + + try: + table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz) + except exceptions.SymbolError: + vollog.error("Unable to find the system call table. Exiting.") + return None + + tables = [(table_name, table_info)] + + # this table is only present on 64 bit systems with 32 bit emulation + # enabled in order to support 32 bit programs and libraries + # if the symbol isn't there then the support isn't in the kernel and so we skip it + try: + ia32_symbol = vmlinux.get_symbol("ia32_sys_call_table") + except exceptions.SymbolError: + ia32_symbol = None + + if ia32_symbol is not None: + ia32_info = self._get_table_info(vmlinux, "ia32_sys_call_table", ptr_sz) + tables.append(("32bit", ia32_info)) + + for table_name, (tableaddr, tblsz) in tables: + table = vmlinux.object( + object_type="array", + subtype=vmlinux.get_type("pointer"), + offset=tableaddr, + count=tblsz, + ) + + for i in range(len(table)): + try: + call_addr = table[i] + except exceptions.InvalidAddressException: + vollog.debug(f"Failed to get system call table entry at index {i}") + continue + + symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr)) + + if len(symbols) > 0: + sym_name = ( + str(symbols[0].split(constants.BANG)[1]) + if constants.BANG in symbols[0] + else str(symbols[0]) + ) + else: + sym_name = "UNKNOWN" + + yield ( + 0, + ( + format_hints.Hex(tableaddr), + table_name, + i, + format_hints.Hex(call_addr), + sym_name, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Table Address", format_hints.Hex), + ("Table Name", str), + ("Index", int), + ("Handler Address", format_hints.Hex), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py new file mode 100644 index 000000000..a9f0b5b51 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -0,0 +1,228 @@ +# 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 +# +import logging +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, +) + +vollog = logging.getLogger(__name__) + + +class Hidden_modules(plugins.PluginInterface): + """Carves memory to find hidden kernel modules""" + + _required_framework_version = (2, 25, 0) + _version = (3, 0, 3) + + @classmethod + def find_hidden_modules( + cls, context, vmlinux_module_name: str + ) -> Generator[extensions.module, None, None]: + if context.symbol_space.verify_table_versions( + "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) + ): + raise exceptions.SymbolSpaceError( + "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" + ) + + known_module_addresses = cls.get_lsmod_module_addresses( + context, vmlinux_module_name + ) + modules_memory_boundaries = ( + linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_module_name + ) + ) + + yield from linux_utilities_modules.Modules.get_hidden_modules( + context, + vmlinux_module_name, + known_module_addresses, + modules_memory_boundaries, + ) + + @classmethod + def get_hidden_modules( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """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 + in Volatility2, but it doesn't work with older kernels. + + From kernels 4.2 struct module allocation are aligned to the L1 cache line size. + In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in + the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can + also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json + doesn't support this feature yet. + In kernels < 4.2, alignment attributes are absent in the struct module, meaning + alignment cannot be guaranteed. Therefore, for older kernels, it's better to use + the traditional scan technique. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: + module objects + """ + return linux_utilities_modules.get_hidden_modules( + vmlinux_module_name, known_module_addresses, modules_memory_boundaries + ) + + 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=(2, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 1), + ), + 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="2026-03-25", + replacement_version=(3, 0, 0), + ) + def get_modules_memory_boundaries( + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Tuple[int, int]: + return linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_module_name + ) + + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_module_address_alignment, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + @classmethod + def _get_module_address_alignment( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> int: + """Obtain the module memory address alignment. + + struct module is aligned to the L1 cache line, which is typically 64 bytes for most + common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this + will still work. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + The struct module alignment + """ + return linux_utilities_modules.get_module_address_alignment( + context, vmlinux_module_name + ) + + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_hidden_modules, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + @staticmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.validate_alignment_patterns, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + def _validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation + + Returns: + True if all the addresses meet the alignment + """ + return linux_utilities_modules.validate_alignment_patterns( + addresses, address_alignment + ) + + @classmethod + def get_lsmod_module_addresses( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Set[int]: + """Obtain a set the known module addresses from linux.lsmod plugin + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A set containing known kernel module addresses + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + known_module_addresses = { + vmlinux_layer.canonicalize(module.vol.offset) + for module in linux_utilities_modules.Modules.list_modules( + context, vmlinux_module_name + ) + } + 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/keyboard_notifiers.py b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py new file mode 100644 index 000000000..9e99809b2 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py @@ -0,0 +1,105 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Keyboard_notifiers(interfaces.plugins.PluginInterface): + """Parses the keyboard notifier call chain""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + try: + knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") + except exceptions.SymbolError: + knl_addr = None + + if not knl_addr: + raise TypeError( + "This plugin requires the keyboard_notifier_list structure. " + "This structure is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + if not self.context.layers[vmlinux.layer_name].is_valid(knl_addr.vol.offset): + vollog.error("The head of the keyboard notifier list is paged out.") + return + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + knl = vmlinux.object( + object_type="atomic_notifier_head", + offset=knl_addr.vol.offset, + absolute=True, + ) + + for call_back in linux.LinuxUtilities.walk_internal_list( + vmlinux, "notifier_block", "next", knl.head + ): + call_addr = call_back.notifier_call + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, call_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(call_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py new file mode 100644 index 000000000..cbd9f87c1 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -0,0 +1,150 @@ +# 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 +# + +from typing import List, Tuple, Optional +import logging +from volatility3.framework import interfaces +from volatility3.framework import renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class Malfind(interfaces.plugins.PluginInterface): + """Lists process memory ranges that potentially contain injected code.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 4) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + 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( + self, task + ) -> Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes]: + """Generate memory regions for a process that may contain injected + code.""" + + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + return None + + 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]": + 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 + vmlinux = self.context.modules[self.config["kernel"]] + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=vmlinux.symbol_table_name + ) + + for task in tasks: + process_name = utility.array_to_string(task.comm) + + 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 + offset, architecture + ) + + yield ( + 0, + ( + task.pid, + process_name, + format_hints.Hex(vma.vm_start), + format_hints.Hex(vma.vm_end), + vma_name or renderers.NotAvailableValue(), + vma.get_protection(), + format_hints.HexBytes(data), + disasm, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Path", str), + ("Protection", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/malware/modxview.py b/volatility3/framework/plugins/linux/malware/modxview.py new file mode 100644 index 000000000..63b265202 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/modxview.py @@ -0,0 +1,180 @@ +# 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 +# +import logging +from typing import Dict, Iterator, List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +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.symbols.linux.utilities import tainting + +vollog = logging.getLogger(__name__) + + +class Modxview(interfaces.plugins.PluginInterface): + """Centralize lsmod, check_modules and hidden_modules results to efficiently \ +spot modules presence and taints.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 17, 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=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_lsmod", + component=linux_utilities_modules.ModuleGathererLsmod, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_sysfs", + component=linux_utilities_modules.ModuleGathererSysFs, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_scanner", + component=linux_utilities_modules.ModuleGathererScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="plain_taints", + description="Display the plain taints string for each module.", + optional=True, + default=False, + ), + ] + + @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.flatten_run_modules_results, + replacement_version=(3, 0, 0), + removal_date="2026-03-25", + ) + def flatten_run_modules_results( + cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True + ) -> Iterator[extensions.module]: + """Flatten a dictionary mapping plugin names and modules list, to a single merged list. + This is useful to get a generic lookup list of all the detected modules. + + Args: + run_results: dictionary of plugin names mapping a list of detected modules + deduplicate: remove duplicate modules, based on their offsets + + Returns: + Iterator of modules objects + """ + return linux_utilities_modules.Modules.flatten_run_modules_results( + run_results, deduplicate + ) + + @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.run_modules_scanners, + replacement_version=(3, 0, 0), + removal_date="2026-03-25", + ) + def run_modules_scanners( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + run_hidden_modules: bool = True, + ) -> Dict[str, List[extensions.module]]: + """Run module scanning plugins and aggregate the results. It is designed + to not operate any inter-plugin results triage.""" + return linux_utilities_modules.Modules.run_modules_scanners( + context, kernel_name, run_hidden_modules + ) + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + wanted_gatherers = [ + linux_utilities_modules.ModuleGathererLsmod, + linux_utilities_modules.ModuleGathererSysFs, + linux_utilities_modules.ModuleGathererScanner, + ] + + run_results = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=wanted_gatherers, + flatten=False, + ) + + aggregated_modules = {} + # We want to be explicit on the plugins results we are interested in + for gatherer in wanted_gatherers: + # Iterate over each recovered module + for mod_info in run_results[gatherer.name]: + # Use offsets as unique keys, whether a module + # appears in many plugin runs or not + if aggregated_modules.get(mod_info.offset, None) is not None: + # Append the plugin to the list of originating plugins + aggregated_modules[mod_info.offset].append(gatherer.name) + else: + aggregated_modules[mod_info.offset] = [gatherer.name] + + for module_offset, gatherers in aggregated_modules.items(): + module = kernel.object("module", offset=module_offset, absolute=True) + + # Tainting parsing capabilities applied to the module + if self.config.get("plain_taints"): + taints = tainting.Tainting.get_taints_as_plain_string( + self.context, + self.config["kernel"], + module.taints, + True, + ) + else: + taints = ",".join( + tainting.Tainting.get_taints_parsed( + self.context, + self.config["kernel"], + module.taints, + True, + ) + ) + + yield ( + 0, + ( + module.get_name() or renderers.NotAvailableValue(), + format_hints.Hex(module_offset), + linux_utilities_modules.ModuleGathererLsmod.name in gatherers, + linux_utilities_modules.ModuleGathererSysFs.name in gatherers, + linux_utilities_modules.ModuleGathererScanner.name in gatherers, + taints or renderers.NotAvailableValue(), + ), + ) + + def run(self): + columns = [ + ("Name", str), + ("Address", format_hints.Hex), + ("In procfs", bool), + ("In sysfs", bool), + ("In scan", bool), + ("Taints", str), + ] + + return renderers.TreeGrid( + columns, + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py new file mode 100644 index 000000000..bd7f2b7cc --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -0,0 +1,815 @@ +# 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 +# +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 volatility3 import framework +from volatility3.framework import ( + constants, + deprecation, + exceptions, + interfaces, + renderers, +) +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.linux import network + +vollog = logging.getLogger(__name__) + + +@dataclass +class Proto: + name: str + hooks: Tuple[str] = field(default_factory=tuple) + + +PROTO_NOT_IMPLEMENTED = Proto(name="UNSPEC") + +NF_INET_HOOKS = ("PRE_ROUTING", "LOCAL_IN", "FORWARD", "LOCAL_OUT", "POST_ROUTING") +NF_DEC_HOOKS = ( + "PRE_ROUTING", + "LOCAL_IN", + "FORWARD", + "LOCAL_OUT", + "POST_ROUTING", + "HELLO", + "ROUTE", +) +NF_ARP_HOOKS = ("IN", "OUT", "FORWARD") +NF_NETDEV_HOOKS = ("INGRESS", "EGRESS") +LARGEST_HOOK_NUMBER = max( + len(NF_INET_HOOKS), len(NF_DEC_HOOKS), len(NF_ARP_HOOKS), len(NF_NETDEV_HOOKS) +) + + +class AbstractNetfilter(ABC): + """Netfilter Abstract Base Classes handling details across various + Netfilter implementations, including constants, helpers, and common + routines. + """ + + PROTO_HOOKS = ( + PROTO_NOT_IMPLEMENTED, # NFPROTO_UNSPEC + Proto(name="INET", hooks=NF_INET_HOOKS), # From kernels 3.14 + Proto(name="IPV4", hooks=NF_INET_HOOKS), + Proto(name="ARP", hooks=NF_ARP_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="NETDEV", hooks=NF_NETDEV_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="BRIDGE", hooks=NF_INET_HOOKS), + PROTO_NOT_IMPLEMENTED, + PROTO_NOT_IMPLEMENTED, + Proto(name="IPV6", hooks=NF_INET_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="DECNET", hooks=NF_DEC_HOOKS), # Removed in kernel 6.1 + ) + NF_MAX_HOOKS = LARGEST_HOOK_NUMBER + 1 + + def __init__( + self, context: interfaces.context.ContextInterface, kernel_module_name: str + ): + self._context = context + self.vmlinux = context.modules[kernel_module_name] + self.layer_name = self.vmlinux.layer_name + + # Set data sizes + self.ptr_size = self.vmlinux.get_type("pointer").size + self.list_head_size = self.vmlinux.get_type("list_head").size + + linuxutils_modulegatherers_required_version = ( + Netfilter._required_linuxutils_gatherers_version + ) + linuxutils_modulegatherers_current_version = ( + linux_utilities_modules.ModuleGatherers.version + ) + if not requirements.VersionRequirement.matches_required( + linuxutils_modulegatherers_required_version, + linuxutils_modulegatherers_current_version, + ): + raise exceptions.PluginRequirementException( + f"linux_utilities_modules.ModuleGatherer version not suitable: required {linuxutils_modulegatherers_required_version} found {linuxutils_modulegatherers_current_version}" + ) + + linux_net_required_version = Netfilter._required_linuxnet_version + linux_net_current_version = network.NetSymbols.version + if not requirements.VersionRequirement.matches_required( + linux_net_required_version, linux_net_current_version + ): + raise exceptions.PluginRequirementException( + f"symbols.linux.net.NetSymbols version not suitable: required {linux_net_required_version} found {linux_net_current_version}" + ) + + linux_utilities_modules_required_version = ( + Netfilter._required_linux_utilities_modules_version + ) + linux_utilities_modules_current_version = ( + linux_utilities_modules.Modules.version + ) + if not requirements.VersionRequirement.matches_required( + linux_utilities_modules_required_version, + linux_utilities_modules_current_version, + ): + raise exceptions.PluginRequirementException( + f"linux_utilities_modules.Modules version not suitable: required {linux_utilities_modules_required_version} found {linux_utilities_modules_current_version}" + ) + + symbol_table = context.symbol_space[self.vmlinux.symbol_table_name] + network.NetSymbols.apply(symbol_table) + + self.handlers = linux_utilities_modules.Modules.run_modules_scanners( + context=context, + kernel_module_name=kernel_module_name, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + @classmethod + def run_all( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: + """It calls each subclass symtab_checks() to test the required + conditions to that specific kernel implementation. + + Args: + context: The volatility3 context on which to operate + kernel_module_name: The name of the table containing the kernel symbols + + Yields: + The kmsg records. Same as _run() + """ + vmlinux = context.modules[kernel_module_name] + + implementation_inst = None # type: ignore + for subclass in framework.class_subclasses(cls): + if not subclass.symtab_checks(vmlinux=vmlinux): + vollog.log( + constants.LOGLEVEL_VVVV, + "Netfilter implementation '%s' doesn't match this memory dump", + subclass.__name__, + ) + continue + + vollog.log( + constants.LOGLEVEL_VVVV, + "Netfilter implementation '%s' matches!", + subclass.__name__, + ) + implementation_inst = subclass( + context=context, kernel_module_name=kernel_module_name + ) + # More than one class could be executed for an specific kernel version + # For instance: Netfilter Ingress hooks + yield from implementation_inst._run() + + if implementation_inst is None: + vollog.error("Unsupported Netfilter kernel implementation") + + def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: + """Iterates over namespaces and protocols, executing various callbacks that + allow customization of the code to the specific data structure used in a + particular kernel implementation + + get_hooks_container(net, proto_name, hook_name) + It returns the data structure used in a specific kernel implementation + to store the hooks for a respective namespace and protocol, basically: + For Ingress hooks: + network_namespace[] -> net_device[] -> nf_hooks_ingress[] + For egress hooks: + network_namespace[] -> net_device[] -> nf_hooks_egress[] + For all the other Netfilter hooks: + <= 4.2.8 + nf_hooks[] + >= 4.3 + network_namespace[] -> nf.hooks[] + + get_hook_ops(hook_container, proto_idx, hook_idx) + Give the 'hook_container' got in get_hooks_container(), it + returns an iterable of 'nf_hook_ops' elements for a respective protocol + and hook type. + + Returns: + netns [int]: Network namespace id + proto_name [str]: Protocol name + hook_name [str]: Hook name + priority [int]: Priority + hook_ops_hook [int]: Hook address + module_name [str]: Linux kernel module name + hooked [bool]: "True" if the network stack has been hijacked + """ + for netns, net in self.get_net_namespaces(): + for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop(): + hooks_container = self.get_hooks_container(net, proto_name, hook_name) + + for hook_container in hooks_container: + for hook_ops in self.get_hook_ops( + hook_container, proto_idx, hook_idx + ): + if not hook_ops: + continue + + priority = int(hook_ops.priority) + hook_ops_hook = hook_ops.hook + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self._context, + self.vmlinux.name, + self.handlers, + hook_ops_hook, + ) + ) + hooked = module_info is None + + yield ( + netns, + proto_name, + hook_name, + priority, + hook_ops_hook, + module_info, + symbol_name, + hooked, + ) + + @classmethod + @abstractmethod + def symtab_checks(cls, vmlinux: interfaces.context.ModuleInterface) -> bool: + """This method on each sublasss will be called to evaluate if the kernel + being analyzed fulfill the type & symbols requirements for the implementation. + The first class returning True will be instantiated and called via the + run() method. + + Returns: + bool: True if the kernel being analyzed fulfill the class requirements. + """ + + def _proto_hook_loop(self) -> Iterator[Tuple[int, str, int, str]]: + """Flattens the protocol families and hooks""" + for proto_idx, proto in enumerate(AbstractNetfilter.PROTO_HOOKS): + if proto == PROTO_NOT_IMPLEMENTED: + continue + if proto.name not in self.subscribed_protocols(): + # This protocol is not managed in this object + continue + for hook_idx, hook_name in enumerate(proto.hooks): + yield proto_idx, proto.name, hook_idx, hook_name + + def build_nf_hook_ops_array( + self, nf_hook_entries + ) -> Optional[interfaces.objects.ObjectInterface]: + """Function helper to build the nf_hook_ops array when it is not part of the + struct 'nf_hook_entries' definition. + + nf_hook_ops was stored adjacent in memory to the nf_hook_entry array, in the + new struct 'nf_hook_entries'. However, this 'nf_hooks_ops' array 'orig_ops' is + not part of the 'nf_hook_entries' struct. So, we need to calculate the offset. + + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; + } + """ + nf_hook_entry_size = self.vmlinux.get_type("nf_hook_entry").size + + try: + num_hook_entries = nf_hook_entries.num_hook_entries + except exceptions.InvalidAddressException: + return None + + orig_ops_addr = ( + nf_hook_entries.hooks.vol.offset + nf_hook_entry_size * num_hook_entries + ) + + if not self.vmlinux._context.layers[self.vmlinux.layer_name].is_valid( + orig_ops_addr + ): + return None + + orig_ops = self._context.object( + object_type=self.get_symbol_fullname("array"), + offset=orig_ops_addr, + subtype=self.vmlinux.get_type("pointer"), + layer_name=self.layer_name, + count=num_hook_entries, + ) + + return orig_ops + + def subscribed_protocols(self) -> Tuple[str]: + """Allows to select which PROTO_HOOKS protocols will be processed by the + Netfiler subclass. + """ + + # Most implementation handlers respond to these protocols, except for + # the ingress hook, which specifically handles the 'NETDEV' protocol. + # However, there is no corresponding Netfilter hook implementation for + # the INET protocol in the kernel. AFAIU, this is used as + # 'NFPROTO_INET = NFPROTO_IPV4 || NFPROTO_IPV6' + # in other parts of the kernel source code. + return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") + + @deprecation.method_being_removed( + 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: + """Helper to obtain the module and symbol name in the format needed for the + output of this plugin. + """ + module_name, symbol_name = ( + linux_utilities_modules.Modules.lookup_module_address( + self._context, self.vmlinux.name, self.handlers, addr + ) + ) + + if module_name == "UNKNOWN": + module_name = None + + if symbol_name != "N/A": + module_name = f"[{symbol_name}]" + + return module_name + + def get_net_namespaces(self): + """Common function to retrieve the different namespaces. + From 4.3 on, all the implementations use network namespaces. + """ + nethead = self.vmlinux.object_from_symbol("net_namespace_list") + symbol_net_name = self.get_symbol_fullname("net") + for net in nethead.to_list(symbol_net_name, "list"): + net_ns_id = net.ns.inum + yield net_ns_id, net + + def get_hooks_container(self, net, proto_name, hook_name): + """Returns the data structure used in a specific kernel implementation to store + the hooks for a respective namespace and protocol. + + Except for kernels < 4.3, all the implementations use network namespaces. + Also the data structure which contains the hooks, even though it changes its + implementation and/or data type, it is always in this location. + """ + yield net.nf.hooks + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + """Given the hook_container obtained from get_hooks_container(), it + returns an iterable of 'nf_hook_ops' elements for a corresponding protocol + and hook type. + + This is the most variable/unstable part of all Netfilter hook designs, it + changes almost in every single implementation. + """ + raise NotImplementedError("You must implement this method") + + def get_symbol_fullname(self, symbol_basename: str) -> str: + """Given a short symbol or type name, it returns its full name""" + return self.vmlinux.symbol_table_name + constants.BANG + symbol_basename + + @staticmethod + def get_member_type( + vol_type: interfaces.objects.Template, member_name: str + ) -> List[str]: + """Returns a list of types/subtypes belonging to the given type member. + + Args: + vol_type (interfaces.objects.Template): A vol3 type object + member_name (str): The member name + + Returns: + list: A list of types/subtypes + """ + _size, vol_obj = vol_type.vol.members[member_name] + type_name = vol_obj.type_name + type_basename = type_name.split(constants.BANG)[1] + member_type = [type_basename] + cur_type = vol_obj + while hasattr(cur_type, "subtype"): + subtype_name = cur_type.subtype.type_name + subtype_basename = subtype_name.split(constants.BANG)[1] + member_type.append(subtype_basename) + cur_type = cur_type.subtype + + return member_type + + +class NetfilterImp_to_4_3(AbstractNetfilter): + """At this point, Netfilter hooks were implemented as a linked list of struct + 'nf_hook_ops' type. One linked list per protocol per hook type. + It was like that until 4.2.8. + + struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return vmlinux.has_symbol("nf_hooks") + + def get_net_namespaces(self): + # In kernels <= 4.2.8 netfilter hooks are not implemented per namespaces + netns, net = renderers.NotAvailableValue(), renderers.NotAvailableValue() + yield netns, net + + def get_hooks_container(self, net, proto_name, hook_name): + nf_hooks = self.vmlinux.object_from_symbol("nf_hooks") + if not nf_hooks: + return + + yield nf_hooks + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + +class NetfilterImp_4_3_to_4_9(AbstractNetfilter): + """Netfilter hooks were added to network namespaces in 4.3. + It is still implemented as a linked list of 'struct nf_hook_ops' type but inside a + network namespace. One linked list per protocol per hook type. + + struct net { ... struct netns_nf nf; ... } + struct netns_nf { ... + struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") + == ["array", "array", "list_head"] + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + +class NetfilterImp_4_9_to_4_14(AbstractNetfilter): + """In this range of kernel versions, the doubly-linked lists of netfilter hooks were + replaced by an array of arrays of 'nf_hook_entry' pointers in a singly-linked lists. + struct net { ... struct netns_nf nf; ... } + struct netns_nf { .. + struct nf_hook_entry __rcu *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + + Also in v4.10 the struct nf_hook_entry changed, a hook function pointer was added to + it. However, for simplicity of this design, we will still take the hook address from + the 'nf_hook_ops'. As per v5.0-rc2, the hook address is duplicated in both sides. + - v4.9: + struct nf_hook_entry { + struct nf_hook_entry *next; + struct nf_hook_ops ops; + const struct nf_hook_ops *orig_ops; }; + - v4.10: + struct nf_hook_entry { + struct nf_hook_entry *next; + nf_hookfn *hook; + void *priv; + const struct nf_hook_ops *orig_ops; }; + (*) Even though the hook address is in the struct 'nf_hook_entry', we use the + original 'nf_hook_ops' hook address value, the one which was filled by the user, to + make it uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["array", "array", "pointer", "nf_hook_entry"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type + ) + + def _get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entry_list = hook_container[proto_idx][hook_idx] + while nf_hook_entry_list: + yield nf_hook_entry_list.orig_ops + nf_hook_entry_list = nf_hook_entry_list.next + + +class NetfilterImp_4_14_to_4_16(AbstractNetfilter): + """'nf_hook_ops' was removed from struct 'nf_hook_entry'. Instead, it was stored + adjacent in memory to the 'nf_hook_entry' array, in the new struct 'nf_hook_entries' + However, 'orig_ops' is not part of the 'nf_hook_entries' struct definition. So, we + have to craft it by hand. + + struct net { ... struct netns_nf nf; ... } + struct netns_nf { + struct nf_hook_entries *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + struct nf_hook_entry { + nf_hookfn *hook; + void *priv; } + + (*) Even though the hook address is in the struct 'nf_hook_entry', we use the + original 'nf_hook_ops' hook address value, the one which was filled by the user, to + make it uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["array", "array", "pointer", "nf_hook_entries"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type + ) + + def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): + """This allows to support different hook array implementations from this version + on. For instance, in kernels >= 4.16 this multi-dimensional array is split in + one-dimensional array of pointers to 'nf_hooks_entries' per each protocol.""" + return nf_hooks_addr[proto_idx][hook_idx] + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entries = self.get_nf_hook_entries(hook_container, proto_idx, hook_idx) + if not nf_hook_entries: + return + + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) + if not nf_hook_ops_ptr_arr: + return + + for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: + nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) + yield nf_hook_ops + + +class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16): + """The multidimensional array of nf_hook_entries was split in a one-dimensional + array per each protocol. + + struct net { + struct netns_nf nf; ... } + struct netns_nf { + struct nf_hook_entries * hooks_ipv4[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_ipv6[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_arp[NF_ARP_NUMHOOKS]; + struct nf_hook_entries * hooks_bridge[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_decnet[NF_DN_NUMHOOKS]; ... } + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + struct nf_hook_entry { + nf_hookfn *hook; + void *priv; } + + (*) Even though the hook address is in the struct nf_hook_entry, we use the original + nf_hook_ops hook address value, the one which was filled by the user, to make it + uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks_ipv4") + ) + + def get_hooks_container(self, net, proto_name, hook_name): + try: + if proto_name == "IPV4": + net_nf_hooks = net.nf.hooks_ipv4 + elif proto_name == "ARP": + net_nf_hooks = net.nf.hooks_arp + elif proto_name == "BRIDGE": + net_nf_hooks = net.nf.hooks_bridge + elif proto_name == "IPV6": + net_nf_hooks = net.nf.hooks_ipv6 + elif proto_name == "DECNET": + net_nf_hooks = net.nf.hooks_decnet + else: + return + + yield net_nf_hooks + + except AttributeError: + # Protocol family disabled at kernel compilation + # CONFIG_NETFILTER_FAMILY_ARP=n || + # CONFIG_NETFILTER_FAMILY_BRIDGE=n || + # CONFIG_DECNET=n + pass + + def _get_nf_hook_entries_ptr(self, nf_hooks_addr, proto_idx, hook_idx): + nf_hook_entries_ptr = nf_hooks_addr[hook_idx] + return nf_hook_entries_ptr + + def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): + return nf_hooks_addr[hook_idx] + + +class AbstractNetfilterNetDev(AbstractNetfilter): + """Base class to handle the Netfilter NetDev hooks. + It won't be executed. It has some common functions to all Netfilter NetDev hook + implementations. + + Netfilter NetDev hooks are set per network device which belongs to a network + namespace. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return False + + def subscribed_protocols(self): + return ("NETDEV",) + + def get_hooks_container(self, net, proto_name, hook_name): + net_device_type = self.vmlinux.get_type("net_device") + net_device_name = self.get_symbol_fullname("net_device") + for net_device in net.dev_base_head.to_list(net_device_name, "dev_list"): + if hook_name == "INGRESS": + if net_device_type.has_member("nf_hooks_ingress"): + # CONFIG_NETFILTER_INGRESS=y + yield net_device.nf_hooks_ingress + + elif hook_name == "EGRESS": + if net_device_type.has_member("nf_hooks_egress"): + # CONFIG_NETFILTER_EGRESS=y + yield net_device.nf_hooks_egress + + +class NetfilterNetDevImp_4_2_to_4_9(AbstractNetfilterNetDev): + """This is the first version of Netfilter Ingress hooks which was implemented using + a doubly-linked list of 'nf_hook_ops'. + struct list_head nf_hooks_ingress; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["list_head"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hooks_ingress = hook_container + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + return nf_hooks_ingress.to_list(nf_hook_ops_name, "list") + + +class NetfilterNetDevImp_4_9_to_4_14(AbstractNetfilterNetDev): + """In 4.9 it was changed to a simple singly-linked list. + struct nf_hook_entry * nf_hooks_ingress; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["pointer", "nf_hook_entry"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hooks_ingress_ptr = hook_container + if not nf_hooks_ingress_ptr: + return + + while nf_hooks_ingress_ptr: + nf_hook_entry = nf_hooks_ingress_ptr.dereference() + orig_ops = nf_hook_entry.orig_ops.dereference() + yield orig_ops + nf_hooks_ingress_ptr = nf_hooks_ingress_ptr.next + + +class NetfilterNetDevImp_4_14_to_latest(AbstractNetfilterNetDev): + """In 4.14 the hook list was converted to an array of pointers inside the struct + 'nf_hook_entries': + struct nf_hook_entries * nf_hooks_ingress; + struct nf_hook_entries { + u16 num_hook_entries; + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["pointer", "nf_hook_entries"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entries = hook_container + if not nf_hook_entries: + return + + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) + if not nf_hook_ops_ptr_arr: + return + + for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: + nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) + yield nf_hook_ops + + +class Netfilter(interfaces.plugins.PluginInterface): + """Lists Netfilter hooks.""" + + _required_framework_version = (2, 22, 0) + + _version = (2, 0, 0) + + _required_linux_utilities_modules_version = (3, 0, 0) + _required_linuxutils_gatherers_version = (1, 0, 0) + _required_linuxnet_version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=cls._required_linuxutils_gatherers_version, + ), + requirements.VersionRequirement( + name="linuxnet", + component=network.NetSymbols, + version=cls._required_linuxnet_version, + ), + ] + + def _format_fields(self, fields): + ( + netns, + proto_name, + hook_name, + priority, + hook_func, + module_info, + symbol_name, + hooked, + ) = fields + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + return ( + netns, + proto_name, + hook_name, + priority, + format_hints.Hex(hook_func), + module_name, + symbol_name or renderers.NotAvailableValue(), + str(hooked), + ) + + def _generator(self): + kernel_module_name = self.config["kernel"] + for fields in AbstractNetfilter.run_all( + context=self.context, kernel_module_name=kernel_module_name + ): + yield (0, self._format_fields(fields)) + + def run(self): + headers = [ + ("Net NS", int), + ("Proto", str), + ("Hook", str), + ("Priority", int), + ("Handler", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ("Is Hooked", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py new file mode 100644 index 000000000..493a1de9a --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -0,0 +1,300 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from pathlib import PurePosixPath +from typing import Optional, Tuple, Iterator + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.constants import linux as linux_constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class ProcessSpoofing(plugins.PluginInterface): + """Detects process spoofing by comparing executable path to cmdline & comm fields. + + Examples of such behavior can be found here: https://github.com/SolitudePy/linux-mal + """ + + _required_framework_version = (2, 27, 0) + _version = (1, 1, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + @classmethod + def get_executable_path( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Optional[str]: + """ + Extract the executable path from task_struct.mm.exe_file + + Args: + context: The context to operate on + task: task_struct object of the process + + Returns: + Returns executable path or None if not available + """ + + try: + mm = task.mm + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") + return None + + if not mm or not mm.is_readable(): + # Kernel threads don't have mm struct + return None + + try: + exe_file = mm.exe_file + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" + ) + return None + + if not exe_file or not exe_file.is_readable(): + return None + + try: + exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" + ) + return None + + return exe_path + + @classmethod + def get_cmdline_basename( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Optional[str]: + """ + Extract the command line arguments and return the basename of the first argument. + + Notes: + The read length is capped at ``MAX_ARG_STRLEN`` (32 * 4096) per the + kernel limit defined in ``include/uapi/linux/binfmts.h`` (see + linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940). + + Args: + context: The context to operate on + task: task_struct object of the process + + Returns: + Basename of the first command line argument or None if not available + """ + mm = task.mm + if not mm or not mm.is_readable(): + return None + + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start + + if size_to_read <= 0: + return None + + read_length = min(size_to_read, linux_constants.MAX_ARG_STRLEN) + + try: + cmdline = utility.address_to_string( + context=context, + layer_name=proc_layer_name, + address=start, + count=read_length, + errors="replace", + encoding="utf-8", + ) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" + ) + return None + + if not cmdline: + return None + + basename = PurePosixPath(cmdline).name + return basename if basename else None + + @classmethod + def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: + """ + Extract the comm field from task_struct + + Args: + task: task_struct object of the process + + Returns: + Process name from comm field or None if not available + """ + try: + return utility.array_to_string(task.comm) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") + return None + + @classmethod + def extract_process_names( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: + """ + Extract all process name sources for comparison + + Returns: + Tuple of (exe_path, exe_basename, cmdline_basename, comm) + """ + exe_path = cls.get_executable_path(context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None + if exe_basename and exe_basename.endswith(" (deleted)"): + exe_basename = exe_basename[: -len(" (deleted)")] + cmdline_basename = cls.get_cmdline_basename(context, task) + comm = cls.get_comm(task) + + return exe_path, exe_basename, cmdline_basename, comm + + def _detect_spoofing( + self, + exe_basename: Optional[str], + cmdline_basename: Optional[str], + comm: Optional[str], + ) -> Tuple[bool, bool]: + """ + Analyze the three name sources to detect potential spoofing + + Args: + exe_basename: Basename from exe_file path + cmdline_basename: Basename from command line + comm: Name from comm field + + Returns: + Tuple of (cmdline_spoofed, comm_spoofed) boolean flags + """ + # Skip kernel threads - need at least 2 sources for comparison + available_sources = sum( + 1 for name in [exe_basename, cmdline_basename, comm] if name + ) + if available_sources < 2: + return False, False + + # Check for cmdline spoofing + cmdline_spoofed = False + if exe_basename and cmdline_basename: + cmdline_spoofed = exe_basename != cmdline_basename + + # Check for comm spoofing (comm is truncated to 15 characters) + comm_spoofed = False + if exe_basename and comm: + comm_spoofed = exe_basename[:15] != comm + + return cmdline_spoofed, comm_spoofed + + def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: + """ + Generate process spoofing detection results + + Args: + tasks: Iterator of task_struct objects + + Yields: + Tuple containing process information and spoofing analysis + """ + for task in tasks: + try: + pid = task.pid + ppid = task.get_parent_pid() + + exe_path, exe_basename, cmdline_basename, comm = ( + self.extract_process_names(self.context, task) + ) + + cmdline_spoofed, comm_spoofed = self._detect_spoofing( + exe_basename, cmdline_basename, comm + ) + + is_deleted = exe_path.endswith(" (deleted)") if exe_path else False + + # Convert None values to strings for TreeGrid compatibility + exe_path_render = exe_path if exe_path else "N/A" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" + comm_render = comm if comm else "N/A" + + yield ( + 0, + ( + pid, + ppid, + exe_path_render, + cmdline_render, + comm_render, + cmdline_spoofed, + comm_spoofed, + is_deleted, + ), + ) + + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning( + f"Unable to process task PID {getattr(task, 'pid', 'unknown')} at {task.vol.offset:#x}: {e}" + ) + continue + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("Exe_Path", str), + ("Cmdline_Basename", str), + ("Comm", str), + ("Cmdline_Spoofed", bool), + ("Comm_Spoofed", bool), + ("Exe_Deleted", bool), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/malware/tty_check.py b/volatility3/framework/plugins/linux/malware/tty_check.py new file mode 100644 index 000000000..00e85f251 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/tty_check.py @@ -0,0 +1,122 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Tty_Check(plugins.PluginInterface): + """Checks tty devices for hooks""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + try: + tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") + except exceptions.SymbolError: + tty_drivers = None + + if not tty_drivers: + raise TypeError( + "This plugin requires the tty_drivers structure." + "This structure is not present in the supplied symbol table." + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + for tty in tty_drivers.to_list( + vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" + ): + try: + ttys = utility.array_of_pointers( + tty.ttys.dereference(), + count=tty.num, + subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct", + context=self.context, + ) + except exceptions.PagedInvalidAddressException: + continue + + for tty_dev in ttys: + if tty_dev == 0: + continue + + try: + name = utility.array_to_string(tty_dev.name) + recv_buf = tty_dev.ldisc.ops.receive_buf + except exceptions.InvalidAddressException: + continue + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, recv_buf + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + ( + name, + format_hints.Hex(recv_buf), + module_name, + symbol_name or renderers.NotAvailableValue(), + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Name", str), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py new file mode 100644 index 000000000..3b6b6f0e5 --- /dev/null +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -0,0 +1,95 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3 import framework +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.objects import utility + +vollog = logging.getLogger(__name__) + + +class ModuleExtract(interfaces.plugins.PluginInterface): + """Recreates an ELF file from a specific address in the kernel""" + + _version = (1, 0, 1) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.IntRequirement( + name="base", + description="Base virtual address to reconstruct an ELF file", + optional=False, + ), + requirements.VersionRequirement( + name="linux_utilities_modules_module_extract", + version=(1, 0, 2), + component=linux_utilities_modules.ModuleExtract, + ), + ] + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + base_address = self.config["base"] + + kernel_layer = self.context.layers[kernel.layer_name] + + if not kernel_layer.is_valid(base_address): + vollog.error( + f"Given base address ({base_address:#x}) is not valid in the kernel address space. Unable to extract file." + ) + return + + module = kernel.object(object_type="module", offset=base_address, absolute=True) + + elf_data = linux_utilities_modules.ModuleExtract.extract_module( + self.context, self.config["kernel"], module + ) + if not elf_data: + vollog.error( + f"Unable to reconstruct the ELF for module struct at {base_address:#x}" + ) + return + + module_name = utility.array_to_string(module.name) + file_name = self.open.sanitize_filename( + f"kernel_module.{module_name}.{base_address:#x}.elf" + ) + + 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, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Base", format_hints.Hex), + ("File Size", int), + ("File output", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py new file mode 100644 index 000000000..d91f37587 --- /dev/null +++ b/volatility3/framework/plugins/linux/modxview.py @@ -0,0 +1,21 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import modxview + +vollog = logging.getLogger(__name__) + + +class Modxview( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=modxview.Modxview, + removal_date="2026-06-07", +): + """Centralize lsmod, check_modules and hidden_modules results to efficiently \ +spot modules presence and taints (deprecated).""" + + _version = (1, 0, 0) + _required_framework_version = (2, 17, 0) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 2499f009e..f00733a54 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -36,8 +36,7 @@ class MountInfo(plugins.PluginInterface): """Lists mount points on processes mount namespaces""" _required_framework_version = (2, 2, 0) - - _version = (1, 2, 1) + _version = (1, 2, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -47,8 +46,8 @@ class MountInfo(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) @@ -94,11 +93,14 @@ class MountInfo(plugins.PluginInterface): return None mnt_root_path = mnt_root.path() - superblock = mnt.get_mnt_sb() mnt_id: int = mnt.mnt_id parent_id: int = mnt.mnt_parent.mnt_id + superblock = mnt.get_mnt_sb() + if not (superblock and superblock.is_readable()): + return None + st_dev = f"{superblock.major}:{superblock.minor}" mnt_opts: List[str] = [] @@ -153,9 +155,11 @@ class MountInfo(plugins.PluginInterface): if not ( task and task.fs - and task.fs.root + and task.fs.is_readable() and task.nsproxy + and task.nsproxy.is_readable() and task.nsproxy.mnt_ns + and task.nsproxy.mnt_ns.is_readable() ): # This task doesn't have all the information required. # It should be a kernel < 2.6.30 @@ -277,7 +281,7 @@ class MountInfo(plugins.PluginInterface): if sb_ptr in seen_sb_ptr: continue - seen_sb_ptr.add(sb_ptr) + seen_sb_ptr.add(int(sb_ptr)) superblock = sb_ptr.dereference() diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 73496dfd9..741241039 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -1,738 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 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 typing import Iterator, List, Tuple -from volatility3 import framework -from volatility3.framework import ( - constants, - interfaces, - renderers, - exceptions, -) -from volatility3.framework.renderers import format_hints -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import netfilter vollog = logging.getLogger(__name__) -@dataclass -class Proto: - name: str - hooks: Tuple[str] = field(default_factory=tuple) - - -PROTO_NOT_IMPLEMENTED = Proto(name="UNSPEC") - -NF_INET_HOOKS = ("PRE_ROUTING", "LOCAL_IN", "FORWARD", "LOCAL_OUT", "POST_ROUTING") -NF_DEC_HOOKS = ( - "PRE_ROUTING", - "LOCAL_IN", - "FORWARD", - "LOCAL_OUT", - "POST_ROUTING", - "HELLO", - "ROUTE", -) -NF_ARP_HOOKS = ("IN", "OUT", "FORWARD") -NF_NETDEV_HOOKS = ("INGRESS", "EGRESS") -LARGEST_HOOK_NUMBER = max( - len(NF_INET_HOOKS), len(NF_DEC_HOOKS), len(NF_ARP_HOOKS), len(NF_NETDEV_HOOKS) -) - - -class AbstractNetfilter(ABC): - """Netfilter Abstract Base Classes handling details across various - Netfilter implementations, including constants, helpers, and common - routines. - """ - - PROTO_HOOKS = ( - PROTO_NOT_IMPLEMENTED, # NFPROTO_UNSPEC - Proto(name="INET", hooks=NF_INET_HOOKS), # From kernels 3.14 - Proto(name="IPV4", hooks=NF_INET_HOOKS), - Proto(name="ARP", hooks=NF_ARP_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="NETDEV", hooks=NF_NETDEV_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="BRIDGE", hooks=NF_INET_HOOKS), - PROTO_NOT_IMPLEMENTED, - PROTO_NOT_IMPLEMENTED, - Proto(name="IPV6", hooks=NF_INET_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="DECNET", hooks=NF_DEC_HOOKS), # Removed in kernel 6.1 - ) - NF_MAX_HOOKS = LARGEST_HOOK_NUMBER + 1 - - def __init__( - self, context: interfaces.context.ContextInterface, kernel_module_name: str - ): - self._context = context - self.vmlinux = context.modules[kernel_module_name] - self.layer_name = self.vmlinux.layer_name - - # Set data sizes - self.ptr_size = self.vmlinux.get_type("pointer").size - self.list_head_size = self.vmlinux.get_type("list_head").size - - lsmod_required_version = Netfilter._required_lsmod_version - lsmod_current_version = lsmod.Lsmod._version - if not requirements.VersionRequirement.matches_required( - lsmod_required_version, lsmod_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.lsmod.Lsmod version not suitable: required {lsmod_required_version} found {lsmod_current_version}" - ) - - linuxutils_required_version = Netfilter._required_linuxutils_version - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - modules = lsmod.Lsmod.list_modules(context, kernel_module_name) - self.handlers = linux.LinuxUtilities.generate_kernel_handler_info( - context, kernel_module_name, modules - ) - - @classmethod - def run_all( - cls, context: interfaces.context.ContextInterface, kernel_module_name: str - ) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: - """It calls each subclass symtab_checks() to test the required - conditions to that specific kernel implementation. - - Args: - context: The volatility3 context on which to operate - kernel_module_name: The name of the table containing the kernel symbols - - Yields: - The kmsg records. Same as _run() - """ - vmlinux = context.modules[kernel_module_name] - - implementation_inst = None # type: ignore - for subclass in framework.class_subclasses(cls): - if not subclass.symtab_checks(vmlinux=vmlinux): - vollog.log( - constants.LOGLEVEL_VVVV, - "Netfilter implementation '%s' doesn't match this memory dump", - subclass.__name__, - ) - continue - - vollog.log( - constants.LOGLEVEL_VVVV, - "Netfilter implementation '%s' matches!", - subclass.__name__, - ) - implementation_inst = subclass( - context=context, kernel_module_name=kernel_module_name - ) - # More than one class could be executed for an specific kernel version - # For instance: Netfilter Ingress hooks - yield from implementation_inst._run() - - if implementation_inst is None: - vollog.error("Unsupported Netfilter kernel implementation") - - def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: - """Iterates over namespaces and protocols, executing various callbacks that - allow customization of the code to the specific data structure used in a - particular kernel implementation - - get_hooks_container(net, proto_name, hook_name) - It returns the data structure used in a specific kernel implementation - to store the hooks for a respective namespace and protocol, basically: - For Ingress hooks: - network_namespace[] -> net_device[] -> nf_hooks_ingress[] - For egress hooks: - network_namespace[] -> net_device[] -> nf_hooks_egress[] - For all the other Netfilter hooks: - <= 4.2.8 - nf_hooks[] - >= 4.3 - network_namespace[] -> nf.hooks[] - - get_hook_ops(hook_container, proto_idx, hook_idx) - Give the 'hook_container' got in get_hooks_container(), it - returns an iterable of 'nf_hook_ops' elements for a respective protocol - and hook type. - - Returns: - netns [int]: Network namespace id - proto_name [str]: Protocol name - hook_name [str]: Hook name - priority [int]: Priority - hook_ops_hook [int]: Hook address - module_name [str]: Linux kernel module name - hooked [bool]: "True" if the network stack has been hijacked - """ - for netns, net in self.get_net_namespaces(): - for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop(): - hooks_container = self.get_hooks_container(net, proto_name, hook_name) - - for hook_container in hooks_container: - for hook_ops in self.get_hook_ops( - hook_container, proto_idx, hook_idx - ): - if not hook_ops: - continue - - priority = int(hook_ops.priority) - hook_ops_hook = hook_ops.hook - module_name = self.get_module_name_for_address(hook_ops_hook) - hooked = module_name is None - - yield netns, proto_name, hook_name, priority, hook_ops_hook, module_name, hooked - - @classmethod - @abstractmethod - def symtab_checks(cls, vmlinux: interfaces.context.ModuleInterface) -> bool: - """This method on each sublasss will be called to evaluate if the kernel - being analyzed fulfill the type & symbols requirements for the implementation. - The first class returning True will be instantiated and called via the - run() method. - - Returns: - bool: True if the kernel being analyzed fulfill the class requirements. - """ - - def _proto_hook_loop(self) -> Iterator[Tuple[int, str, int, str]]: - """Flattens the protocol families and hooks""" - for proto_idx, proto in enumerate(AbstractNetfilter.PROTO_HOOKS): - if proto == PROTO_NOT_IMPLEMENTED: - continue - if proto.name not in self.subscribed_protocols(): - # This protocol is not managed in this object - continue - for hook_idx, hook_name in enumerate(proto.hooks): - yield proto_idx, proto.name, hook_idx, hook_name - - def build_nf_hook_ops_array(self, nf_hook_entries): - """Function helper to build the nf_hook_ops array when it is not part of the - struct 'nf_hook_entries' definition. - - nf_hook_ops was stored adjacent in memory to the nf_hook_entry array, in the - new struct 'nf_hook_entries'. However, this 'nf_hooks_ops' array 'orig_ops' is - not part of the 'nf_hook_entries' struct. So, we need to calculate the offset. - - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; - } - """ - nf_hook_entry_size = self.vmlinux.get_type("nf_hook_entry").size - orig_ops_addr = ( - nf_hook_entries.hooks.vol.offset - + nf_hook_entry_size * nf_hook_entries.num_hook_entries - ) - orig_ops = self._context.object( - object_type=self.get_symbol_fullname("array"), - offset=orig_ops_addr, - subtype=self.vmlinux.get_type("pointer"), - layer_name=self.layer_name, - count=nf_hook_entries.num_hook_entries, - ) - - return orig_ops - - def subscribed_protocols(self) -> Tuple[str]: - """Allows to select which PROTO_HOOKS protocols will be processed by the - Netfiler subclass. - """ - - # Most implementation handlers respond to these protocols, except for - # the ingress hook, which specifically handles the 'NETDEV' protocol. - # However, there is no corresponding Netfilter hook implementation for - # the INET protocol in the kernel. AFAIU, this is used as - # 'NFPROTO_INET = NFPROTO_IPV4 || NFPROTO_IPV6' - # in other parts of the kernel source code. - return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") - - def get_module_name_for_address(self, addr) -> str: - """Helper to obtain the module and symbol name in the format needed for the - output of this plugin. - """ - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( - self.vmlinux, self.handlers, addr - ) - - if module_name == "UNKNOWN": - module_name = None - - if symbol_name != "N/A": - module_name = f"[{symbol_name}]" - - return module_name - - def get_net_namespaces(self): - """Common function to retrieve the different namespaces. - From 4.3 on, all the implementations use network namespaces. - """ - nethead = self.vmlinux.object_from_symbol("net_namespace_list") - symbol_net_name = self.get_symbol_fullname("net") - for net in nethead.to_list(symbol_net_name, "list"): - net_ns_id = net.ns.inum - yield net_ns_id, net - - def get_hooks_container(self, net, proto_name, hook_name): - """Returns the data structure used in a specific kernel implementation to store - the hooks for a respective namespace and protocol. - - Except for kernels < 4.3, all the implementations use network namespaces. - Also the data structure which contains the hooks, even though it changes its - implementation and/or data type, it is always in this location. - """ - yield net.nf.hooks - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - """Given the hook_container obtained from get_hooks_container(), it - returns an iterable of 'nf_hook_ops' elements for a corresponding protocol - and hook type. - - This is the most variable/unstable part of all Netfilter hook designs, it - changes almost in every single implementation. - """ - raise NotImplementedError("You must implement this method") - - def get_symbol_fullname(self, symbol_basename: str) -> str: - """Given a short symbol or type name, it returns its full name""" - return self.vmlinux.symbol_table_name + constants.BANG + symbol_basename - - @staticmethod - def get_member_type( - vol_type: interfaces.objects.Template, member_name: str - ) -> List[str]: - """Returns a list of types/subtypes belonging to the given type member. - - Args: - vol_type (interfaces.objects.Template): A vol3 type object - member_name (str): The member name - - Returns: - list: A list of types/subtypes - """ - _size, vol_obj = vol_type.vol.members[member_name] - type_name = vol_obj.type_name - type_basename = type_name.split(constants.BANG)[1] - member_type = [type_basename] - cur_type = vol_obj - while hasattr(cur_type, "subtype"): - subtype_name = cur_type.subtype.type_name - subtype_basename = subtype_name.split(constants.BANG)[1] - member_type.append(subtype_basename) - cur_type = cur_type.subtype - - return member_type - - -class NetfilterImp_to_4_3(AbstractNetfilter): - """At this point, Netfilter hooks were implemented as a linked list of struct - 'nf_hook_ops' type. One linked list per protocol per hook type. - It was like that until 4.2.8. - - struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_symbol("nf_hooks") - - def get_net_namespaces(self): - # In kernels <= 4.2.8 netfilter hooks are not implemented per namespaces - netns, net = renderers.NotAvailableValue(), renderers.NotAvailableValue() - yield netns, net - - def get_hooks_container(self, net, proto_name, hook_name): - nf_hooks = self.vmlinux.object_from_symbol("nf_hooks") - if not nf_hooks: - return - - yield nf_hooks - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - -class NetfilterImp_4_3_to_4_9(AbstractNetfilter): - """Netfilter hooks were added to network namepaces in 4.3. - It is still implemented as a linked list of 'struct nf_hook_ops' type but inside a - network namespace. One linked list per protocol per hook type. - - struct net { ... struct netns_nf nf; ... } - struct netns_nf { ... - struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") - == ["array", "array", "list_head"] - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - -class NetfilterImp_4_9_to_4_14(AbstractNetfilter): - """In this range of kernel versions, the doubly-linked lists of netfilter hooks were - replaced by an array of arrays of 'nf_hook_entry' pointers in a singly-linked lists. - struct net { ... struct netns_nf nf; ... } - struct netns_nf { .. - struct nf_hook_entry __rcu *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - - Also in v4.10 the struct nf_hook_entry changed, a hook function pointer was added to - it. However, for simplicity of this design, we will still take the hook address from - the 'nf_hook_ops'. As per v5.0-rc2, the hook address is duplicated in both sides. - - v4.9: - struct nf_hook_entry { - struct nf_hook_entry *next; - struct nf_hook_ops ops; - const struct nf_hook_ops *orig_ops; }; - - v4.10: - struct nf_hook_entry { - struct nf_hook_entry *next; - nf_hookfn *hook; - void *priv; - const struct nf_hook_ops *orig_ops; }; - (*) Even though the hook address is in the struct 'nf_hook_entry', we use the - original 'nf_hook_ops' hook address value, the one which was filled by the user, to - make it uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["array", "array", "pointer", "nf_hook_entry"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type - ) - - def _get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entry_list = hook_container[proto_idx][hook_idx] - while nf_hook_entry_list: - yield nf_hook_entry_list.orig_ops - nf_hook_entry_list = nf_hook_entry_list.next - - -class NetfilterImp_4_14_to_4_16(AbstractNetfilter): - """'nf_hook_ops' was removed from struct 'nf_hook_entry'. Instead, it was stored - adjacent in memory to the 'nf_hook_entry' array, in the new struct 'nf_hook_entries' - However, 'orig_ops' is not part of the 'nf_hook_entries' struct definition. So, we - have to craft it by hand. - - struct net { ... struct netns_nf nf; ... } - struct netns_nf { - struct nf_hook_entries *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - struct nf_hook_entry { - nf_hookfn *hook; - void *priv; } - - (*) Even though the hook address is in the struct 'nf_hook_entry', we use the - original 'nf_hook_ops' hook address value, the one which was filled by the user, to - make it uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["array", "array", "pointer", "nf_hook_entries"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type - ) - - def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): - """This allows to support different hook array implementations from this version - on. For instance, in kernels >= 4.16 this multi-dimensional array is split in - one-dimensional array of pointers to 'nf_hooks_entries' per each protocol.""" - return nf_hooks_addr[proto_idx][hook_idx] - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entries = self.get_nf_hook_entries(hook_container, proto_idx, hook_idx) - if not nf_hook_entries: - return - - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) - for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: - nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) - yield nf_hook_ops - - -class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16): - """The multidimensional array of nf_hook_entries was split in a one-dimensional - array per each protocol. - - struct net { - struct netns_nf nf; ... } - struct netns_nf { - struct nf_hook_entries * hooks_ipv4[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_ipv6[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_arp[NF_ARP_NUMHOOKS]; - struct nf_hook_entries * hooks_bridge[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_decnet[NF_DN_NUMHOOKS]; ... } - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - struct nf_hook_entry { - nf_hookfn *hook; - void *priv; } - - (*) Even though the hook address is in the struct nf_hook_entry, we use the original - nf_hook_ops hook address value, the one which was filled by the user, to make it - uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks_ipv4") - ) - - def get_hooks_container(self, net, proto_name, hook_name): - try: - if proto_name == "IPV4": - net_nf_hooks = net.nf.hooks_ipv4 - elif proto_name == "ARP": - net_nf_hooks = net.nf.hooks_arp - elif proto_name == "BRIDGE": - net_nf_hooks = net.nf.hooks_bridge - elif proto_name == "IPV6": - net_nf_hooks = net.nf.hooks_ipv6 - elif proto_name == "DECNET": - net_nf_hooks = net.nf.hooks_decnet - else: - return - - yield net_nf_hooks - - except AttributeError: - # Protocol family disabled at kernel compilation - # CONFIG_NETFILTER_FAMILY_ARP=n || - # CONFIG_NETFILTER_FAMILY_BRIDGE=n || - # CONFIG_DECNET=n - pass - - def _get_nf_hook_entries_ptr(self, nf_hooks_addr, proto_idx, hook_idx): - nf_hook_entries_ptr = nf_hooks_addr[hook_idx] - return nf_hook_entries_ptr - - def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): - return nf_hooks_addr[hook_idx] - - -class AbstractNetfilterNetDev(AbstractNetfilter): - """Base class to handle the Netfilter NetDev hooks. - It won't be executed. It has some common functions to all Netfilter NetDev hook - implementions. - - Netfilter NetDev hooks are set per network device which belongs to a network - namespace. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return False - - def subscribed_protocols(self): - return ("NETDEV",) - - def get_hooks_container(self, net, proto_name, hook_name): - net_device_type = self.vmlinux.get_type("net_device") - net_device_name = self.get_symbol_fullname("net_device") - for net_device in net.dev_base_head.to_list(net_device_name, "dev_list"): - if hook_name == "INGRESS": - if net_device_type.has_member("nf_hooks_ingress"): - # CONFIG_NETFILTER_INGRESS=y - yield net_device.nf_hooks_ingress - - elif hook_name == "EGRESS": - if net_device_type.has_member("nf_hooks_egress"): - # CONFIG_NETFILTER_EGRESS=y - yield net_device.nf_hooks_egress - - -class NetfilterNetDevImp_4_2_to_4_9(AbstractNetfilterNetDev): - """This is the first version of Netfilter Ingress hooks which was implemented using - a doubly-linked list of 'nf_hook_ops'. - struct list_head nf_hooks_ingress; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["list_head"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hooks_ingress = hook_container - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - return nf_hooks_ingress.to_list(nf_hook_ops_name, "list") - - -class NetfilterNetDevImp_4_9_to_4_14(AbstractNetfilterNetDev): - """In 4.9 it was changed to a simple singly-linked list. - struct nf_hook_entry * nf_hooks_ingress; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["pointer", "nf_hook_entry"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hooks_ingress_ptr = hook_container - if not nf_hooks_ingress_ptr: - return - - while nf_hooks_ingress_ptr: - nf_hook_entry = nf_hooks_ingress_ptr.dereference() - orig_ops = nf_hook_entry.orig_ops.dereference() - yield orig_ops - nf_hooks_ingress_ptr = nf_hooks_ingress_ptr.next - - -class NetfilterNetDevImp_4_14_to_latest(AbstractNetfilterNetDev): - """In 4.14 the hook list was converted to an array of pointers inside the struct - 'nf_hook_entries': - struct nf_hook_entries * nf_hooks_ingress; - struct nf_hook_entries { - u16 num_hook_entries; - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["pointer", "nf_hook_entries"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entries = hook_container - if not nf_hook_entries: - return - - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) - for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: - nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) - yield nf_hook_ops - - -class Netfilter(interfaces.plugins.PluginInterface): - """Lists Netfilter hooks.""" - - _required_framework_version = (2, 0, 0) - - _version = (1, 1, 0) - - _required_linuxutils_version = (2, 1, 0) - _required_lsmod_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=cls._required_lsmod_version - ), - requirements.VersionRequirement( - name="linuxutils", - component=linux.LinuxUtilities, - version=cls._required_linuxutils_version, - ), - ] - - def _format_fields(self, fields): - ( - netns, - proto_name, - hook_name, - priority, - hook_func, - module_name, - hooked, - ) = fields - return ( - netns, - proto_name, - hook_name, - priority, - format_hints.Hex(hook_func), - module_name or renderers.NotAvailableValue(), - str(hooked), - ) - - def _generator(self): - kernel_module_name = self.config["kernel"] - for fields in AbstractNetfilter.run_all( - context=self.context, kernel_module_name=kernel_module_name - ): - yield (0, self._format_fields(fields)) - - def run(self): - headers = [ - ("Net NS", int), - ("Proto", str), - ("Hook", str), - ("Priority", int), - ("Handler", format_hints.Hex), - ("Module", str), - ("Is Hooked", str), - ] - return renderers.TreeGrid(headers, self._generator()) +class Netfilter( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=netfilter.Netfilter, + removal_date="2026-06-07", +): + """Lists Netfilter hooks (deprecated).""" + + _version = (2, 0, 0) + _required_framework_version = (2, 22, 0) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 005fc9acc..2d20a2fb1 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -5,10 +5,15 @@ import math import logging import datetime +import time +import tarfile from dataclasses import dataclass, astuple -from typing import List, Set, Type, Iterable +from typing import IO, List, Set, Type, Iterable, Tuple, Union +from io import BytesIO +from pathlib import PurePath -from volatility3.framework import renderers, interfaces +from volatility3.framework.constants import architectures +from volatility3.framework import constants, renderers, interfaces, exceptions from volatility3.framework.renderers import format_hints from volatility3.framework.interfaces import plugins from volatility3.framework.configuration import requirements @@ -37,6 +42,11 @@ class InodeUser: modification_time: str change_time: str path: str + inode_size: int + + @classmethod + def format_symlink(cls, symlink_source: str, symlink_dest: str) -> str: + return f"{symlink_source} -> {symlink_dest}" @dataclass @@ -80,6 +90,7 @@ class InodeInternal: access_time_dt = self.inode.get_access_time() modification_time_dt = self.inode.get_modification_time() change_time_dt = self.inode.get_change_time() + inode_size = int(self.inode.i_size) inode_user = InodeUser( superblock_addr=superblock_addr, @@ -95,6 +106,7 @@ class InodeInternal: modification_time=modification_time_dt, change_time=change_time_dt, path=self.path, + inode_size=inode_size, ) return inode_user @@ -104,7 +116,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -112,10 +124,15 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"], + architectures=architectures.LINUX_ARCHS, ), - requirements.PluginRequirement( - name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0) + requirements.VersionRequirement( + name="mountinfo", component=mountinfo.MountInfo, version=(1, 2, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.ListRequirement( name="type", @@ -147,11 +164,17 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): Otherwise, it returns the same symlink_path """ # i_link (fast symlinks) were introduced in 4.2 - if inode and inode.is_link and inode.has_member("i_link") and inode.i_link: - i_link_str = inode.i_link.dereference().cast( + if ( + inode + and inode.is_link + and inode.has_member("i_link") + and inode.i_link + and inode.i_link.is_readable() + ): + symlink_dest = inode.i_link.dereference().cast( "string", max_length=255, encoding="utf-8", errors="replace" ) - symlink_path = f"{symlink_path} -> {i_link_str}" + symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest) return symlink_path @@ -212,12 +235,14 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, + follow_symlinks: bool = True, ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: context: The context that the plugin will operate within vmlinux_module_name: The name of the kernel module on which to operate + follow_symlinks: Whether to follow symlinks or not Yields: An InodeInternal object @@ -253,10 +278,19 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): if not root_inode.is_valid(): continue + if not (root_inode.i_mapping and root_inode.i_mapping.is_readable()): + # Retrieving data from the page cache requires a valid address space + continue + # Inode already processed? + # Store a primitive int (instead of the pointer value) to track + # addresses we've already seen. Storing the full `objects.Pointer` + # uses too much memory, and we don't need all of the information + # that it contains. if root_inode_ptr in seen_inodes: continue - seen_inodes.add(root_inode_ptr) + + seen_inodes.add(int(root_inode_ptr)) root_path = mountpoint @@ -284,12 +318,21 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): if not file_inode.is_valid(): continue + if not (file_inode.i_mapping and file_inode.i_mapping.is_readable()): + # Retrieving data from the page cache requires a valid address space + continue + # Inode already processed? + # Store a primitive int (instead of the pointer value) to track + # addresses we've already seen. Storing the full `objects.Pointer` + # uses too much memory, and we don't need all of the information + # that it contains. if file_inode_ptr in seen_inodes: continue - seen_inodes.add(file_inode_ptr) + seen_inodes.add(int(file_inode_ptr)) - file_path = cls._follow_symlink(file_inode_ptr, file_path) + if follow_symlinks: + file_path = cls._follow_symlink(file_inode_ptr, file_path) inode_in = InodeInternal( superblock=superblock, mountpoint=mountpoint, @@ -316,10 +359,12 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): if self.config["find"]: if inode_in.path == self.config["find"]: inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) break # Only the first match else: inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) def generate_timeline(self): @@ -341,11 +386,15 @@ 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 - @staticmethod - def format_fields_with_headers(headers, generator): + @classmethod + def format_fields_with_headers(cls, headers, generator): """Uses the headers type to cast the fields obtained from the generator""" for level, fields in generator: formatted_fields = [] @@ -377,6 +426,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): ("ModificationTime", datetime.datetime), ("ChangeTime", datetime.datetime), ("FilePath", str), + ("InodeSize", int), ] return renderers.TreeGrid( @@ -389,7 +439,7 @@ class InodePages(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _version = (3, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -397,10 +447,10 @@ class InodePages(plugins.PluginInterface): requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"], + architectures=architectures.LINUX_ARCHS, ), - requirements.PluginRequirement( - name="files", plugin=Files, version=(1, 0, 0) + requirements.VersionRequirement( + name="files", component=Files, version=(1, 0, 0) ), requirements.StringRequirement( name="find", @@ -412,57 +462,133 @@ class InodePages(plugins.PluginInterface): description="Inode address", optional=True, ), - requirements.StringRequirement( + requirements.BooleanRequirement( name="dump", - description="Output file path", + description="Extract inode content", + default=False, optional=True, ), ] - @staticmethod + @classmethod def write_inode_content_to_file( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, inode: interfaces.objects.ObjectInterface, filename: str, open_method: Type[interfaces.plugins.FileHandlerInterface], - vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a file Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate inode: The inode to dump filename: Filename for writing the inode content open_method: class for constructing output files - vmlinux_layer: The kernel layer to obtain the page size + """ + try: + with open_method(filename) as file_obj: + cls.write_inode_content_to_stream(context, layer_name, inode, file_obj) + except OSError as e: + vollog.error("Unable to write to file (%s): %s", filename, e) + + @classmethod + def write_inode_content_to_stream( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + inode: interfaces.objects.ObjectInterface, + stream: IO, + ) -> None: + """Extracts the inode's contents from the page cache and saves them to a stream + + Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate + inode: The inode to dump + stream: An IO stream to write to, typically FileHandlerInterface or BytesIO """ if not inode.is_reg: vollog.error("The inode is not a regular file") - return + return None - # By using truncate/seek, provided the filesystem supports it, a sparse file will be + layer = context.layers[layer_name] + # By using truncate/seek, provided the filesystem supports it, and the + # stream is a File interface, a sparse file will be # created, saving both disk space and I/O time. # Additionally, using the page index will guarantee that each page is written at the # appropriate file position. + inode_size = inode.i_size try: - with open_method(filename) as f: - inode_size = inode.i_size - f.truncate(inode_size) + stream_initialized = False + for page_idx, page_content in inode.get_contents(): + current_fp = page_idx * layer.page_size + max_length = inode_size - current_fp + page_bytes_len = min(max_length, len(page_content)) + if current_fp >= inode_size or current_fp + page_bytes_len > inode_size: + vollog.error( + "Page out of file bounds: inode 0x%x, inode size %d, page index %d", + inode.vol.offset, + inode_size, + page_idx, + ) + continue + page_bytes = page_content[:page_bytes_len] - for page_idx, page_content in inode.get_contents(): - current_fp = page_idx * vmlinux_layer.page_size - max_length = inode_size - current_fp - page_bytes = page_content[:max_length] - if current_fp + len(page_bytes) > inode_size: - vollog.error( - "Page out of file bounds: inode 0x%x, inode size %d, page index %d", - inode.vol.offset, - inode_size, - page_idx, - ) - f.seek(current_fp) - f.write(page_bytes) + if not stream_initialized: + # Lazy initialization to avoid truncating the stream until we are + # certain there is something to write + stream.truncate(inode_size) + stream_initialized = True - except IOError as e: - vollog.error("Unable to write to file (%s): %s", filename, e) + stream.seek(current_fp) + stream.write(page_bytes) + except exceptions.LinuxPageCacheException: + vollog.error( + f"Error dumping cached pages for inode at {inode.vol.offset:#x}" + ) + + def _generate_inode_fields( + self, + inode: interfaces.objects.ObjectInterface, + vmlinux_layer: interfaces.layers.TranslationLayerInterface, + filename: Union[renderers.NotApplicableValue, str], + ) -> Iterable[Tuple[int, int, int, int, bool, str]]: + inode_size = inode.i_size + try: + for page_obj in inode.get_pages(): + if page_obj.mapping != inode.i_mapping: + vollog.warning( + f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page" + ) + continue + page_vaddr = page_obj.vol.offset + page_paddr = page_obj.to_paddr() + page_mapping_addr = page_obj.mapping + page_index = page_obj.index + page_file_offset = page_index * vmlinux_layer.page_size + dump_safe = ( + page_file_offset < inode_size + and page_mapping_addr + and page_mapping_addr.is_readable() + ) + page_flags_list = page_obj.get_flags_list() + page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) + fields = ( + page_vaddr, + page_paddr, + page_mapping_addr, + page_index, + dump_safe, + page_flags, + filename, + ) + + yield 0, fields + except exceptions.LinuxPageCacheException: + vollog.warning(f"Page cache for inode at {inode.vol.offset:#x} is corrupt") def _generator(self): vmlinux_module_name = self.config["kernel"] @@ -471,7 +597,7 @@ class InodePages(plugins.PluginInterface): if self.config["inode"] and self.config["find"]: vollog.error("Cannot use --inode and --find simultaneously") - return + return None if self.config["find"]: inodes_iter = Files.get_inodes( @@ -482,46 +608,33 @@ class InodePages(plugins.PluginInterface): if inode_in.path == self.config["find"]: inode = inode_in.inode break # Only the first match - + else: + vollog.error("Unable to find inode with path %s", self.config["find"]) + return None elif self.config["inode"]: inode = vmlinux.object("inode", self.config["inode"], absolute=True) else: vollog.error("You must use either --inode or --find") - return + return None if not inode.is_valid(): vollog.error("Invalid inode at 0x%x", inode.vol.offset) - return + return None if not inode.is_reg: vollog.error("The inode is not a regular file") - return - - inode_size = inode.i_size - for page_obj in inode.get_pages(): - page_vaddr = page_obj.vol.offset - page_paddr = page_obj.to_paddr() - page_mapping_addr = page_obj.mapping - page_index = int(page_obj.index) - page_file_offset = page_index * vmlinux_layer.page_size - dump_safe = page_file_offset < inode_size - page_flags_list = page_obj.get_flags_list() - page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) - fields = ( - page_vaddr, - page_paddr, - page_mapping_addr, - page_index, - dump_safe, - page_flags, - ) - - yield 0, fields + return None + filename = renderers.NotApplicableValue() if self.config["dump"]: - filename = self.config["dump"] - vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename) - self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer) + open_method = self.open + inode_address = inode.vol.offset + filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp") + vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename) + self.write_inode_content_to_file( + self.context, vmlinux_layer.name, inode, filename, open_method + ) + yield from self._generate_inode_fields(inode, vmlinux_layer, filename) def run(self): headers = [ @@ -531,6 +644,280 @@ class InodePages(plugins.PluginInterface): ("Index", int), ("DumpSafe", bool), ("Flags", str), + ("Output File", str), + ] + + return renderers.TreeGrid( + headers, Files.format_fields_with_headers(headers, self._generator()) + ) + + +class RecoverFs(plugins.PluginInterface): + """Recovers the cached filesystem (directories, files, symlinks) into a compressed tarball. + + Details: level 0 directories are named after the UUID of the parent superblock; metadata aren't replicated to extracted objects; objects modification time is set to the plugin run time; absolute symlinks + are converted to relative symlinks to prevent referencing the analyst's filesystem. + Troubleshooting: to fix extraction errors related to long paths, please consider using https://github.com/mxmlnkn/ratarmount. + """ + + _version = (1, 0, 1) + _required_framework_version = (2, 21, 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="files", component=Files, version=(1, 1, 0) + ), + requirements.VersionRequirement( + name="inodepages", component=InodePages, version=(3, 0, 0) + ), + requirements.BooleanRequirement( + name="tmpfs_only", + description="Extracts only files from tmpfs file systems", + default=False, + optional=True, + ), + requirements.ChoiceRequirement( + name="compression_format", + description="Compression format (default: gz)", + choices=["gz", "bz2", "xz"], + default="gz", + optional=True, + ), + ] + + def _tar_add_reg_inode( + self, + context: interfaces.context.ContextInterface, + layer_name: str, + tar: tarfile.TarFile, + reg_inode_in: InodeInternal, + path_prefix: str = "", + mtime: float = None, + ) -> int: + """Extracts a REG inode content and writes it to a TarFile object. + + Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate + tar: The TarFile object to write to + reg_inode_in: The inode to extract content from + path_prefix: A custom path prefix to prepend the inode path with + mtime: The modification time to set the TarInfo object to + + Returns: + The number of extracted bytes + """ + inode_content_buffer = BytesIO() + InodePages.write_inode_content_to_stream( + context, layer_name, reg_inode_in.inode, inode_content_buffer + ) + inode_content_buffer.seek(0) + handle_buffer_size = inode_content_buffer.getbuffer().nbytes + + tar_info = tarfile.TarInfo(path_prefix + reg_inode_in.path) + # The tarfile module only has read support for sparse files: + # https://docs.python.org/3.12/library/tarfile.html#tarfile.LNKTYPE:~:text=and%20longlink%20extensions%2C-,read%2Donly%20support,-for%20all%20variants + tar_info.type = tarfile.REGTYPE + tar_info.size = handle_buffer_size + tar_info.mode = 0o444 + if mtime is not None: + tar_info.mtime = mtime + tar.addfile(tar_info, inode_content_buffer) + + return handle_buffer_size + + def _tar_add_dir( + self, + tar: tarfile.TarFile, + directory_path: str, + mtime: float = None, + ) -> None: + """Adds a directory path to a TarFile object, based on a DIR inode. + + Args: + tar: The TarFile object to write to + directory_path: The directory path to create + mtime: The modification time to set the TarInfo object to + """ + tar_info = tarfile.TarInfo(directory_path) + tar_info.type = tarfile.DIRTYPE + tar_info.mode = 0o755 + if mtime is not None: + tar_info.mtime = mtime + tar.addfile(tar_info) + + def _tar_add_lnk( + self, + tar: tarfile.TarFile, + symlink_source: str, + symlink_dest: str, + symlink_source_prefix: str = "", + mtime: float = None, + ) -> None: + """Adds a symlink to a TarFile object. + + Args: + tar: The TarFile object to write to + symlink_source: The symlink source path + symlink_dest: The symlink target/destination + symlink_source_prefix: A custom path prefix to prepend the symlink source with + mtime: The modification time to set the TarInfo object to + """ + # Patch symlinks pointing to absolute paths, + # to prevent referencing the host filesystem. + if symlink_dest.startswith("/"): + relative_dest = PurePath(symlink_dest).relative_to(PurePath("/")) + # Remove the leading "/" to prevent an extra undesired "../" in the output + symlink_dest = ( + PurePath( + *[".."] * len(PurePath(symlink_source.lstrip("/")).parent.parts) + ) + / relative_dest + ).as_posix() + tar_info = tarfile.TarInfo(symlink_source_prefix + symlink_source) + tar_info.type = tarfile.SYMTYPE + tar_info.linkname = symlink_dest + tar_info.mode = 0o444 + if mtime is not None: + tar_info.mtime = mtime + tar.addfile(tar_info) + + def _generator(self): + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + tar_buffer = BytesIO() + tar = tarfile.open( + fileobj=tar_buffer, + mode=f"w:{self.config['compression_format']}", + ) + # Set a unique timestamp for all extracted files + mtime = time.time() + + inodes_iter = Files.get_inodes( + context=self.context, + vmlinux_module_name=vmlinux_module_name, + follow_symlinks=False, + ) + + # Prefix paths with the superblock UUID's to prevent overlaps. + # Switch to device major and device minor for older kernels (< 2.6.39-rc1). + uuid_as_prefix = vmlinux.get_type("super_block").has_member("s_uuid") + if not uuid_as_prefix: + vollog.warning( + "super_block struct does not support s_uuid attribute. Consequently, level 0 directories won't refer to the superblock uuid's, but to its device_major:device_minor numbers." + ) + + 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. + if not ( + inode_in.inode.is_reg or inode_in.inode.is_dir or inode_in.inode.is_link + ): + continue + + if not inode_in.path.startswith("/"): + vollog.debug( + f'Skipping processing of potentially smeared "{inode_in.path}" inode name as it does not starts with a "/".' + ) + continue + + sb_type = inode_in.superblock.get_type() + if not sb_type: + vollog.debug( + f"Unable to read superblock type for inode at {inode_in.inode.vol.offset}" + ) + continue + + if self.config["tmpfs_only"] and sb_type != "tmpfs": + vollog.debug(f"Skipping non-tmpfs filesystem {sb_type}") + continue + + # Construct the output path + if uuid_as_prefix: + prefix = f"/{inode_in.superblock.uuid}" + else: + prefix = f"/{inode_in.superblock.major}:{inode_in.superblock.minor}" + prefixed_path = prefix + inode_in.path + + # Sanity check for already processed paths + if prefixed_path in visited_paths: + vollog.log( + constants.LOGLEVEL_VV, + f'Already processed prefixed inode path: "{prefixed_path}".', + ) + continue + elif prefix not in seen_prefixes: + self._tar_add_dir(tar, prefix, mtime) + seen_prefixes.add(prefix) + + visited_paths.add(prefixed_path) + extracted_file_size = renderers.NotApplicableValue() + + # Inodes parent directory is yielded first, which + # ensures that a file parent path will exist beforehand. + # tarfile will take care of creating it anyway. + if inode_in.inode.is_reg: + extracted_file_size = self._tar_add_reg_inode( + self.context, + vmlinux_layer.name, + tar, + inode_in, + prefix, + mtime, + ) + elif inode_in.inode.is_dir: + self._tar_add_dir(tar, prefixed_path, mtime) + elif ( + inode_in.inode.is_link + and inode_in.inode.has_member("i_link") + and inode_in.inode.i_link + and inode_in.inode.i_link.is_readable() + ): + symlink_dest = inode_in.inode.i_link.dereference().cast( + "string", max_length=255, encoding="utf-8", errors="replace" + ) + self._tar_add_lnk(tar, inode_in.path, symlink_dest, prefix, mtime) + # Set path to a user friendly representation before yielding + inode_in.path = InodeUser.format_symlink(inode_in.path, symlink_dest) + else: + continue + + inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out) + (extracted_file_size,)) + + tar.close() + tar_buffer.seek(0) + output_filename = f"recovered_fs.tar.{self.config['compression_format']}" + with self.open(output_filename) as f: + f.write(tar_buffer.getvalue()) + + def run(self): + headers = [ + ("SuperblockAddr", format_hints.Hex), + ("MountPoint", str), + ("Device", str), + ("InodeNum", int), + ("InodeAddr", format_hints.Hex), + ("FileType", str), + ("InodePages", int), + ("CachedPages", int), + ("FileMode", str), + ("AccessTime", datetime.datetime), + ("ModificationTime", datetime.datetime), + ("ChangeTime", datetime.datetime), + ("FilePath", str), + ("InodeSize", int), + ("Recovered FileSize", int), ] return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index edafe97e0..b4b1643e1 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -3,7 +3,7 @@ # import logging -from typing import List +from typing import List, Iterable from volatility3.framework import renderers, interfaces, constants from volatility3.framework.symbols import linux @@ -19,8 +19,7 @@ class PIDHashTable(plugins.PluginInterface): """Enumerates processes through the PID hash table""" _required_framework_version = (2, 0, 0) - - _version = (1, 0, 1) + _version = (1, 0, 3) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -30,8 +29,8 @@ class PIDHashTable(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) @@ -219,7 +218,7 @@ class PIDHashTable(plugins.PluginInterface): return None - def get_tasks(self) -> interfaces.objects.ObjectInterface: + def get_tasks(self) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerates processes through the PID hash table Yields: @@ -232,14 +231,16 @@ class PIDHashTable(plugins.PluginInterface): yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid)) - def _generator( - self, decorate_comm: bool = False - ) -> interfaces.objects.ObjectInterface: + def _generator(self, decorate_comm: bool = False): for task in self.get_tasks(): - offset, pid, tid, ppid, name = pslist.PsList.get_task_fields( - task, decorate_comm + task_fields = pslist.PsList.get_task_fields(task, decorate_comm) + fields = ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, ) - fields = format_hints.Hex(offset), pid, tid, ppid, name yield 0, fields def run(self): diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index e7d38b107..e9a126374 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -21,7 +21,8 @@ class Maps(plugins.PluginInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 3) + MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @classmethod @@ -33,8 +34,8 @@ class Maps(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pid", @@ -82,18 +83,24 @@ class Maps(plugins.PluginInterface): Returns: Yields vmas based on the task and filtered based on the filter function """ - if task.mm: - for vma in task.mm.get_vma_iter(): - if filter_func(vma): - yield vma - else: - vollog.debug( - f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func" - ) - else: + mm_pointer = task.mm + if not mm_pointer: vollog.debug( - f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread." + f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread" ) + return + + if not mm_pointer.is_readable(): + vollog.error(f"Task {task.pid} has an invalid mm member") + return + + for vma in mm_pointer.get_vma_iter(): + if filter_func(vma): + yield vma + else: + vollog.debug( + f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func" + ) @classmethod def vma_dump( @@ -124,9 +131,7 @@ class Maps(plugins.PluginInterface): proc_layer_name = task.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - pid, excp.invalid_address, excp.layer_name - ) + f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) return None vm_size = vm_end - vm_start @@ -164,7 +169,9 @@ class Maps(plugins.PluginInterface): address_list = self.config.get("address", None) if not address_list: # do not filter as no address_list was supplied - vma_filter_func = lambda _: True + def vma_filter_func(_): + return True + else: # filter for any vm_start that matches the supplied address config def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool: @@ -173,31 +180,32 @@ class Maps(plugins.PluginInterface): ] # if any of the user supplied addresses would fall within this vma return true - if addrs_in_vma: - return True - else: - return False + return bool(addrs_in_vma) vma_filter_func = vma_filter_function + for task in tasks: - if not task.mm: + if not (task.mm and task.mm.is_readable()): continue name = utility.array_to_string(task.comm) for vma in self.list_vmas(task, filter_func=vma_filter_func): flags = vma.get_protection() page_offset = vma.get_page_offset() - major = 0 - minor = 0 - inode = 0 - if vma.vm_file != 0: + inode_num = None + try: dentry = vma.vm_file.get_dentry() - if dentry != 0: - inode_object = dentry.d_inode - major = inode_object.i_sb.major - minor = inode_object.i_sb.minor - inode = inode_object.i_ino + inode_ptr = dentry.d_inode + inode_num = inode_ptr.i_ino + major = inode_ptr.i_sb.major + minor = inode_ptr.i_sb.minor + except exceptions.InvalidAddressException: + if not inode_num: + inode_num = 0 + major = 0 + minor = 0 + path = vma.get_name(self.context, task) file_output = "Disabled" @@ -237,8 +245,8 @@ class Maps(plugins.PluginInterface): format_hints.Hex(page_offset), major, minor, - inode, - path, + inode_num, + path or renderers.NotAvailableValue(), file_output, ), ) diff --git a/volatility3/framework/plugins/linux/psaux.py b/volatility3/framework/plugins/linux/psaux.py index a4a23498f..e6653251c 100644 --- a/volatility3/framework/plugins/linux/psaux.py +++ b/volatility3/framework/plugins/linux/psaux.py @@ -14,7 +14,8 @@ from volatility3.plugins.linux import pslist class PsAux(plugins.PluginInterface): """Lists processes with their command line arguments""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 13, 0) + _version = (1, 1, 1) @classmethod def get_requirements(cls): @@ -25,8 +26,8 @@ class PsAux(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pid", @@ -77,7 +78,7 @@ class PsAux(plugins.PluginInterface): return renderers.UnreadableValue() # the arguments are null byte terminated, replace the nulls with spaces - s = argv.decode().split("\x00") + s = argv.decode(encoding="utf8", errors="replace").split("\x00") args = " ".join(s) else: # kernel thread @@ -97,14 +98,8 @@ class PsAux(plugins.PluginInterface): # walk the process list and report the arguments for task in tasks: pid = task.pid - - try: - ppid = task.parent.pid - except exceptions.InvalidAddressException: - ppid = 0 - + ppid = task.get_parent_pid() name = utility.array_to_string(task.comm) - args = self._get_command_line_args(task, name) yield (0, (pid, ppid, name, args)) diff --git a/volatility3/framework/plugins/linux/pscallstack.py b/volatility3/framework/plugins/linux/pscallstack.py new file mode 100644 index 000000000..c22e00161 --- /dev/null +++ b/volatility3/framework/plugins/linux/pscallstack.py @@ -0,0 +1,204 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +import dataclasses +from typing import List, Iterator + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints +from volatility3.framework.constants import architectures +from volatility3.framework.objects import utility +from volatility3.framework.symbols.linux import kallsyms +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +@dataclasses.dataclass +class StackEntry: + position: int + address: int + value: int + name: str = renderers.NotAvailableValue() + type: str = renderers.NotAvailableValue() + module: str = renderers.NotAvailableValue() + + +class PsCallStack(plugins.PluginInterface): + """Enumerates the call stack of each task""" + + _required_framework_version = (2, 19, 0) + + _version = (1, 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="Kallsyms", component=kallsyms.Kallsyms, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="unresolved", + description="Include unresolved stack values", + default=False, + optional=True, + ), + ] + + @classmethod + def get_task_callstack( + cls, + context: interfaces.context.ContextInterface, + module_name: str, + task: interfaces.objects.ObjectInterface, + kas: kallsyms.Kallsyms = None, + include_unresolved=False, + ) -> Iterator[StackEntry]: + """Retrieves the call stack for a given task + + Args: + context: The context used to access memory layers and symbols + module_name: The name of the kernel module on which to operate + task: The task object whose stack is being retrieved + kas: Kallsyms instance for symbol resolution. If not provided or None, a new + instance will be created each time + include_unresolved: If True, includes stack values that could not be resolved + to known symbols. Defaults to False. + + Yields: + StackEntry objects + """ + task_layer = task.get_address_space_layer() + if not task_layer: + return None + + vmlinux = context.modules[module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + if not kas: + kas = kallsyms.Kallsyms( + context=context, + layer_name=vmlinux.layer_name, + module_name=module_name, + ) + + pointer_size = vmlinux.get_type("pointer").size + + thread_size_order = 2 # Safe since kernel 3.15 + # thread_size_order +=1 # If CONFIG_KASAN is enabled in kernels >= 4.0, default: DISABLED + # thread_size_order +=1 # If CONFIG_KASAN_EXTRA is enabled in kernels >= 4.19, default: DISABLED + thread_size = vmlinux_layer.page_size << thread_size_order + task_base_of_stack = vmlinux_layer.canonicalize(task.stack) + task_top_of_stack = task_base_of_stack + thread_size + + byte_order = task.files.vol.data_format.byteorder + rsp_start = task.thread.sp + if not (task_base_of_stack <= rsp_start < task_top_of_stack): + raise exceptions.VolatilityException( + f"Invalid stack pointer {rsp_start:#x} for task {task.pid}" + ) + + current_sp = rsp_start + idx = 0 + while current_sp < task_top_of_stack: + try: + stack_value_bytes = task_layer.read(current_sp, pointer_size) + except exceptions.InvalidAddressException: + break + stack_value = int.from_bytes(stack_value_bytes, byteorder=byte_order) + if not stack_value: + idx += 1 + current_sp += pointer_size + continue + kassymbol = kas.lookup_address(stack_value) + sp_address = current_sp & vmlinux_layer.address_mask + stack_value &= vmlinux_layer.address_mask + if kassymbol: + module_name = kassymbol.module_name or renderers.NotAvailableValue() + yield StackEntry( + position=idx, + address=sp_address, + value=stack_value, + name=kassymbol.name, + type=kassymbol.type, + module=module_name, + ) + elif include_unresolved: + yield StackEntry( + position=idx, + address=sp_address, + value=stack_value, + ) + + idx += 1 + current_sp += pointer_size + + def _generator(self): + module_name = self.config["kernel"] + vmlinux = self.context.modules[module_name] + + kas = kallsyms.Kallsyms( + context=self.context, + layer_name=vmlinux.layer_name, + module_name=self.config["kernel"], + ) + + include_unresolved = self.config.get("unresolved", False) + + pids = self.config.get("pid", None) + filter_func = pslist.PsList.create_pid_filter(pids) + for task in pslist.PsList.list_tasks( + self.context, vmlinux.name, filter_func=filter_func, include_threads=True + ): + task_name = utility.array_to_string(task.comm) + + for stack_entry in self.get_task_callstack( + context=self.context, + module_name=vmlinux.name, + task=task, + kas=kas, + include_unresolved=include_unresolved, + ): + fields = ( + task.pid, + task_name, + stack_entry.position, + format_hints.Hex(stack_entry.address), + format_hints.Hex(stack_entry.value), + stack_entry.name, + stack_entry.type, + stack_entry.module, + ) + yield 0, fields + + def run(self): + return renderers.TreeGrid( + [ + ("TID", int), + ("Comm", str), + ("Position", int), + ("Address", format_hints.Hex), + ("Value", format_hints.Hex), + ("Name", str), + ("Type", str), + ("Module", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index b05d69c7a..71caa0853 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -2,7 +2,9 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import datetime -from typing import Any, Callable, Iterable, List, Tuple +import dataclasses +import contextlib +from typing import Any, Callable, Iterable, List, Optional from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements @@ -14,12 +16,25 @@ from volatility3.plugins import timeliner from volatility3.plugins.linux import elfs +@dataclasses.dataclass +class TaskFields: + offset: int + user_pid: int + user_tid: int + user_ppid: int + name: str + uid: Optional[int] + gid: Optional[int] + euid: Optional[int] + egid: Optional[int] + creation_time: Optional[datetime.datetime] + + class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the processes present in a particular linux memory image.""" - _required_framework_version = (2, 0, 0) - - _version = (2, 3, 0) + _required_framework_version = (2, 13, 0) + _version = (4, 1, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -29,8 +44,8 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="elfs", plugin=elfs.Elfs, version=(2, 0, 0) + requirements.VersionRequirement( + name="elfs", component=elfs.Elfs, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -38,6 +53,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): element_type=int, optional=True, ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="threads", description="Include user threads", @@ -59,7 +79,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ] @classmethod - def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[Any], bool]: + def create_pid_filter( + cls, pid_list: Optional[List[int]] = None + ) -> Callable[[Any], bool]: """Constructs a filter function for process IDs. Args: @@ -68,7 +90,6 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: Function which, when provided a process object, returns True if the process is to be filtered out of the list """ - # FIXME: mypy #4973 or #2608 pid_list = pid_list or [] filter_list = [x for x in pid_list if x is not None] if filter_list: @@ -83,7 +104,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_task_fields( cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False - ) -> Tuple[int, int, int, int, str, datetime.datetime]: + ) -> TaskFields: """Extract the fields needed for the final output Args: @@ -92,21 +113,34 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): and of Kernel threads in square brackets. Defaults to False. Returns: - A tuple with the fields to show in the plugin output. + A TaskFields object with the fields to show in the plugin output. """ - pid = task.tgid - tid = task.pid - ppid = task.parent.tgid if task.parent else 0 name = utility.array_to_string(task.comm) - start_time = task.get_create_time() if decorate_comm: if task.is_kernel_thread: name = f"[{name}]" elif task.is_user_thread: name = f"{{{name}}}" - task_fields = (task.vol.offset, pid, tid, ppid, name, start_time) - return task_fields + # This function may be called with a partially initialized/uninitialized task. + # Ensure it always returns a valid TaskFields object, ready for use in a plugin. + valid_cred = task.cred and task.cred.is_readable() + creation_time = None + with contextlib.suppress(Exception): + creation_time = task.get_create_time() + + return TaskFields( + offset=task.vol.offset, + user_pid=task.tgid, + user_tid=task.pid, + user_ppid=task.get_parent_pid(), + name=name, + uid=task.cred.uid if valid_cred else None, + gid=task.cred.gid if valid_cred else None, + euid=task.cred.euid if valid_cred else None, + egid=task.cred.egid if valid_cred else None, + creation_time=creation_time, + ) def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str: """Extract the elf for the process if requested @@ -149,6 +183,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): file_output = "VMA start matching task start_code not found" return file_output + @staticmethod + def _format_cred(cred): + return renderers.NotAvailableValue() if cred is None else cred + def _generator( self, pid_filter: Callable[[Any], bool], @@ -180,18 +218,28 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: file_output = "Disabled" - offset, pid, tid, ppid, name, creation_time = self.get_task_fields( - task, decorate_comm - ) + task_fields = self.get_task_fields(task, decorate_comm) - yield 0, ( - format_hints.Hex(offset), - pid, - tid, - ppid, - name, - creation_time or renderers.NotAvailableValue(), - file_output, + task_uid = self._format_cred(task_fields.uid) + task_gid = self._format_cred(task_fields.gid) + 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, + ), ) @classmethod @@ -217,14 +265,27 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): init_task = vmlinux.object_from_symbol(symbol_name="init_task") # Note that the init_task itself is not yielded, since "ps" also never shows it. - for task in init_task.tasks: - if filter_func(task): - continue + seen = set() + for forward in (True, False): + for task in init_task.tasks.to_list( + symbol_type=init_task.vol.type_name, + member="tasks", + forward=forward, + ): + if task.vol.offset in seen: + continue + seen.add(task.vol.offset) - yield task + if not task.is_valid(): + continue - if include_threads: - yield from task.get_threads() + if filter_func(task): + continue + + yield task + + if include_threads: + yield from task.get_threads() def run(self): pids = self.config.get("pid") @@ -239,6 +300,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ("TID", int), ("PPID", int), ("COMM", str), + ("UID", int), + ("GID", int), + ("EUID", int), + ("EGID", int), ("CREATION TIME", datetime.datetime), ("File output", str), ] @@ -252,10 +317,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): for task in self.list_tasks( self.context, self.config["kernel"], filter_func, include_threads=True ): - offset, user_pid, user_tid, _user_ppid, name, creation_time = ( - self.get_task_fields(task) + task_fields = self.get_task_fields(task) + description = f"Process {task_fields.user_pid}/{task_fields.user_tid} {task_fields.name} ({task_fields.offset})" + + yield ( + description, + timeliner.TimeLinerType.CREATED, + task_fields.creation_time, ) - - description = f"Process {user_pid}/{user_tid} {name} ({offset})" - - yield (description, timeliner.TimeLinerType.CREATED, creation_time) diff --git a/volatility3/framework/plugins/linux/psscan.py b/volatility3/framework/plugins/linux/psscan.py index 40784a647..0013bc1d8 100644 --- a/volatility3/framework/plugins/linux/psscan.py +++ b/volatility3/framework/plugins/linux/psscan.py @@ -2,15 +2,15 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import Iterable, List, Tuple +from typing import Iterable, List import struct from enum import Enum from volatility3.framework import renderers, interfaces, symbols, constants, exceptions from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility from volatility3.framework.layers import scanners from volatility3.framework.renderers import format_hints +from volatility3.plugins.linux import pslist vollog = logging.getLogger(__name__) @@ -27,8 +27,8 @@ class DescExitStateEnum(Enum): class PsScan(interfaces.plugins.PluginInterface): """Scans for processes present in a particular linux image.""" - _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _required_framework_version = (2, 13, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -38,37 +38,16 @@ class PsScan(interfaces.plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] - def _get_task_fields( - self, task: interfaces.objects.ObjectInterface - ) -> Tuple[int, int, int, str, str]: - """Extract the fields needed for the final output - - Args: - task: A task object from where to get the fields. - Returns: - A tuple with the fields to show in the plugin output. - """ - pid = task.tgid - tid = task.pid - ppid = 0 - - if task.parent.is_readable(): - ppid = task.parent.tgid - name = utility.array_to_string(task.comm) - exit_state = DescExitStateEnum(task.exit_state).name - - task_fields = ( - format_hints.Hex(task.vol.offset), - pid, - tid, - ppid, - name, - exit_state, - ) - return task_fields - def _generator(self): """Generates the tasks found from scanning.""" @@ -78,8 +57,18 @@ class PsScan(interfaces.plugins.PluginInterface): for task in self.scan_tasks( self.context, vmlinux_module_name, vmlinux.layer_name ): - row = self._get_task_fields(task) - yield (0, row) + task_fields = pslist.PsList.get_task_fields(task) + exit_state = DescExitStateEnum(task.exit_state).name + fields = ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, + exit_state, + ) + + yield (0, fields) @classmethod def scan_tasks( @@ -100,7 +89,9 @@ class PsScan(interfaces.plugins.PluginInterface): vmlinux = context.modules[vmlinux_module_name] # check if this image is 32bit or 64bit - is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) + is_32bit = not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=vmlinux.symbol_table_name + ) if is_32bit: pack_format = "I" else: @@ -133,7 +124,7 @@ class PsScan(interfaces.plugins.PluginInterface): ) elif len(kernel_layer.dependencies) == 0: vollog.error( - f"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + "Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." ) raise exceptions.LayerException( kernel_layer_name, f"Layer {kernel_layer_name} has no dependencies" diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index efe5223df..e7bbdb8d5 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -2,17 +2,21 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging + from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class PsTree(interfaces.plugins.PluginInterface): - """Plugin for listing processes in a tree based on their parent process - ID.""" + """Plugin for listing processes in a tree based on their parent process ID.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 13, 0) + _version = (1, 1, 1) @classmethod def get_requirements(cls): @@ -23,8 +27,8 @@ class PsTree(interfaces.plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 2, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.ListRequirement( name="pid", @@ -52,19 +56,41 @@ class PsTree(interfaces.plugins.PluginInterface): Args: pid: PID to find the level in the hierarchy """ - seen = set([pid]) + seen_ppids = set() + seen_offsets = set() + level = 0 proc = self._tasks.get(pid) - while proc and proc.parent and proc.parent.pid not in seen: + + while proc: + # we don't want swapper in the tree + if proc.pid == 0: + break + if proc.is_thread_group_leader: - parent_pid = proc.parent.pid + parent_pid = proc.get_parent_pid() else: parent_pid = proc.tgid + if parent_pid in seen_ppids or proc.vol.offset in seen_offsets: + break + + # only pid 1 (init/systemd) or 2 (kthreadd) should have swapper as a parent + # any other process with a ppid of 0 is smeared or terminated + if parent_pid == 0 and proc.pid > 2: + vollog.debug( + "Smeared process with parent PID of 0 and PID greater than 2 ({proc.pid}) is being skipped." + ) + break + + seen_ppids.add(parent_pid) + seen_offsets.add(proc.vol.offset) + child_list = self._children.setdefault(parent_pid, set()) child_list.add(proc.pid) proc = self._tasks.get(parent_pid) + level += 1 self._levels[pid] = level @@ -100,20 +126,36 @@ class PsTree(interfaces.plugins.PluginInterface): def yield_processes(pid): task = self._tasks[pid] - row = pslist.PsList.get_task_fields(task, decorate_comm) - # update the first element, the offset, in the row tuple to use format_hints.Hex - # as a simple int is returned from get_task_fields. - row = (format_hints.Hex(row[0]),) + row[1:] + task_fields = pslist.PsList.get_task_fields(task, decorate_comm) + fields = ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, + ) + yield (self._levels[task_fields.user_tid] - 1, fields) - tid = task.pid - yield (self._levels[tid] - 1, row) + seen_children = set() + + for child_pid in sorted(self._children.get(task_fields.user_tid, [])): + if child_pid in seen_children: + break + seen_children.add(child_pid) - for child_pid in sorted(self._children.get(tid, [])): yield from yield_processes(child_pid) + seen_processes = set() + for pid, level in self._levels.items(): if level == 1: - yield from yield_processes(pid) + for fields in yield_processes(pid): + pid = fields[1] + if pid in seen_processes: + break + seen_processes.add(pid) + + yield fields def run(self): filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index e467ee644..356d5e72c 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -19,7 +19,7 @@ class Ptrace(plugins.PluginInterface): """Enumerates ptrace's tracer and tracee tasks""" _required_framework_version = (2, 10, 0) - _version = (1, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -29,8 +29,8 @@ class Ptrace(plugins.PluginInterface): description="Linux kernel", architectures=architectures.LINUX_ARCHS, ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 2, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), ] diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py new file mode 100644 index 000000000..9288d85f0 --- /dev/null +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -0,0 +1,474 @@ +# 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 +# + +import logging +import struct +from typing import List, Set + +from volatility3.framework import exceptions, constants +from volatility3.framework import renderers +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols import linux +from volatility3.framework import symbols +from volatility3.plugins.linux import lsof, pslist, sockstat +from volatility3.framework.layers import scanners +from volatility3.framework.symbols.linux import network + +vollog = logging.getLogger(__name__) + + +class Sockscan(plugins.PluginInterface): + """Scans for network connections found in memory layer.""" + + _required_framework_version = (2, 6, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="SockHandlers", component=sockstat.SockHandlers, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="lsof", component=lsof.Lsof, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 1, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="linux_net", component=network.NetSymbols, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + ] + + def _canonicalize_symbol_addrs( + self, kernel_module_name: str, symbol_names: List[str] + ) -> Set[bytes]: + """Takes a list of symbol names and converts the address of each to the bytes + as they would appear in memory so that they can be scanned for. + + Symbols that cannot be found are ignored and not included in the results. + + Args: + kernel_module_name: The name of the kernel module on which to operate + symbol_names: A list of symbol names to be looked up + + Returns: + A set of bytes which are the packed addresses. + """ + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated + # to the correct size so that they can be accurately located in the physical layer. + if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name): + pack_format = "Q" # 64 bit + else: + pack_format = "I" # 32 bit + + packed_needles = set() + for symbol_name in symbol_names: + try: + needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset + except exceptions.SymbolError: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to find symbol {symbol_name} this will not be scanned for.", + ) + continue + # use canonicalize to set the appropriate sign extension for the addr + addr = kernel_layer.canonicalize(needle_addr) + packed_addr = struct.pack(pack_format, addr) + packed_needles.add(packed_addr) + vollog.log( + constants.LOGLEVEL_VVVV, + f"Will scan for {symbol_name} using the bytes: {packed_addr.hex()}", + ) + + # make a warning if no symbols at all could be resolved. + if not packed_needles: + vollog.warning( + "_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information." + ) + + return packed_needles + + def _find_memory_layer_name(self, kernel_module_name: str): + """Find the memory layer below the kernel. Only returns a single layer, + and will warn the user if multiple layers are found. + + Args: + kernel_module_name: The name of the kernel module on which to operate. + + Returns: + memory_layer_name: The name of the layer below the kernel to be scanned. + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # TODO: Update plugin to support multiple dependencies. e.g. a memory layer and swap file. + # This is a shared problem with psscan and having a generic solution would be useful. + + # Find the memory layer to scan, and provide warnings if more than one is located. + if len(kernel_layer.dependencies) > 1: + vollog.warning( + f"Kernel layer depends on multiple layers however only {kernel_layer.dependencies[0]} will be scanned by this plugin." + ) + elif len(kernel_layer.dependencies) == 0: + vollog.error( + "Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + ) + raise exceptions.LayerException( + vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies" + ) + + memory_layer_name = kernel_layer.dependencies[0] + + return memory_layer_name + + def _find_file_ops_needles(self, kernel_module_name: str): + """Retrieves socket file symbols and the offset to the 'f_op' pointer. + + Args: + kernel_module_name (str): The name of the kernel module to search. + + Returns: + Tuple[List[int], int]: A list of file symbol addresses and, + the offset to the 'f_op' pointer. + """ + + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[kernel_module_name] + + file_ops_symbol_names = [ + "socket_file_ops", + "sockfs_dentry_operations", + ] + file_ops_needles = self._canonicalize_symbol_addrs( + kernel_module_name, file_ops_symbol_names + ) + # get file struct to find the offset to the f_op pointer + # this is so that the file object can be created at the correct offset, + # the results of the scanner will be for the f_op member within the file + f_op_offset = vmlinux.get_type("file").relative_child_offset("f_op") + + return (file_ops_needles, f_op_offset) + + def _find_sk_destruct_needles(self, kernel_module_name: str): + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[kernel_module_name] + + socket_destructor_symbol_names = [ + "sock_def_destruct", + "packet_sock_destruct", + "unix_sock_destructor", + "netlink_sock_destruct", + "inet_sock_destruct", + ] + socket_destructor_needles = self._canonicalize_symbol_addrs( + kernel_module_name, socket_destructor_symbol_names + ) + # get sock struct to find the offset to the sk_destruct pointer + # this is so that the sock object can be created at the correct offset, + # the results of the scanner will be for the sk_destruct member within the scock + sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset( + "sk_destruct" + ) + return (socket_destructor_needles, sk_destruct_offset) + + def _walk_file_ops_needles( + self, + kernel_module_name: str, + physical_memory_layer_name: str, + needle_addr: int, + f_op_offset: int, + ): + """ + This method attempts to walk from the f_op member of files to the + corresponding socket. If sucessful the socket object is created on the + memory layer and returned. + + Args: + kernel_module_name (str): The name of the kernel module from which, + to retrieve the file operations. + physical_memory_layer_name (str): The name of the physical memory layer that was scanned + needle_addr: The address of the needle that was found during the scanning + f_op_offset: The offset to the f_op member of the file type + + Returns: + psock: The sock object that was built on the memory layer + """ + + vmlinux = self.context.modules[kernel_module_name] + try: + # create file in the memory_layer, the native layer matches the + # kernel so that pointers can be followed + sock_physical_addr = needle_addr - f_op_offset + pfile = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "file", + offset=sock_physical_addr, + layer_name=physical_memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + dentry = pfile.get_dentry() + if not dentry: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate dentry", + ) + return None + + d_inode = dentry.d_inode + if not d_inode: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry", + ) + return None + + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + if not (socket and socket.sk): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid", + ) + return None + + # sucessfully trversed from file to sock, this will exist in the + # kernel layer, and need to be translated to the memory layer. + vsock = socket.sk.dereference() + + # get virtual offset + virtual_sock_offset = vsock.vol.offset + + # translate this offset to physical + native_layer = self.context.layers[vmlinux.layer_name] + physical_sock_offset, _physical_layer_name = native_layer.translate( + virtual_sock_offset + ) + + # build sock on the memory_layer using the physical_sock_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=physical_sock_offset, + layer_name=physical_memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + return psock + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", + ) + return None + + def _extract_sock_fields(self, psock, sock_handler): + try: + sock_physical_addr = psock.vol.offset + sock_type = psock.get_type() + + family = psock.get_family() + # remove results with no family + if family is None: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", + ) + return None + + # TODO: invesitgate options for more invalid address handling in proccess_sock + # and the later formatting of it's results. + sock_fields = sock_handler.process_sock(psock) + # if no sock_fields we're able to be extracted then skip this result. + if not sock_fields: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.", + ) + return None + + sock, sock_stat, extended = sock_fields + src, src_port, dst, dst_port, state = sock_stat + protocol = sock.get_protocol() + + # format results + src = renderers.NotAvailableValue() if src is None else str(src) + src_port = ( + renderers.NotAvailableValue() if src_port is None else str(src_port) + ) + dst = renderers.NotAvailableValue() if dst is None else str(dst) + dst_port = ( + renderers.NotAvailableValue() if dst_port is None else str(dst_port) + ) + state = renderers.NotAvailableValue() if state is None else str(state) + protocol = ( + renderers.NotAvailableValue() if protocol is None else str(protocol) + ) + # extended attributes is a dict, so this is formated to string show each + # key and value pair, seperated with a comma. + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else renderers.NotAvailableValue() + ) + + # remove empty results + if (src == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue) + ): + if state == "UNCONNECTED": + return None + elif src_port == "0" and dst_port == "0": + return None + return ( + format_hints.Hex(sock_physical_addr), + family, + sock_type, + protocol, + src, + src_port, + dst, + dst_port, + state, + socket_filter_str, + ) + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", + ) + return None + + def _generator(self, kernel_module_name: str): + """Scans for sockets. Each row represents a kernel socket. + + Args: + kernel_module_name: The name of the kernel module on which to operate + + Yields: + addr: Physical offset + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get the memory layer that is to be scanned. + memory_layer_name = self._find_memory_layer_name(kernel_module_name) + memory_layer = self.context.layers[memory_layer_name] + + # use the init process to build a sock handler + # TODO: look into options so that sockstat.SockHandlers so that process_sock can + # be used without a task object. + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + sock_handler = sockstat.SockHandlers( + self.context, kernel_module_name, init_task + ) + + # get progress_callback in order to use this in the scanners. + # TODO: perhaps add more detail to progress, showing method in progress and number of hits found + progress_callback = self._progress_callback + + # Method 1 - find sockets by file operations, then follow pointers to sockets + file_ops_needles, f_op_offset = self._find_file_ops_needles(kernel_module_name) + + # Method 2 - find sockets by socket destructor directly inside sock objects + socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( + kernel_module_name + ) + + # TODO Method 3 - find sock by sk_error_report symbols + # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] + # this would be similar to Method 2, but using a different pointer within sock. + + # add a set of seen addresses to stop possible duplication of results. + seen_sock_physical_addr = set() + + # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. + for needle_addr, match in memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), + progress_callback, + ): + psock = None + sock_physical_addr = None + + # if match is from socket_destructor_needles simply calculate the offset to the sock + if match in socket_destructor_needles: + sock_physical_addr = needle_addr - sk_destruct_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + # if match is from file_ops_needles attempt to walk from file object to the sock + if match in file_ops_needles: + psock = self._walk_file_ops_needles( + kernel_module_name, memory_layer_name, needle_addr, f_op_offset + ) + + if psock is not None and sock_physical_addr not in seen_sock_physical_addr: + seen_sock_physical_addr.add(sock_physical_addr) + + fields = self._extract_sock_fields(psock, sock_handler) + if fields: + yield (0, fields) + + def run(self): + + tree_grid_args = [ + ("Sock Offset", format_hints.Hex), + ("Family", str), + ("Type", str), + ("Proto", str), + ("Source Addr", str), + ("Source Port", str), + ("Destination Addr", str), + ("Destination Port", str), + ("State", str), + ("Filter", str), + ] + + return renderers.TreeGrid( + tree_grid_args, + self._generator(self.config["kernel"]), + ) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index 0ddd3e26d..a74e84f92 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -5,13 +5,15 @@ import logging from typing import Callable, Tuple, List, Dict -from volatility3.framework import interfaces, exceptions, constants, objects -from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints +from volatility3.framework import interfaces, exceptions, constants, objects, renderers +from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.symbols import linux from volatility3.plugins.linux import lsof +from volatility3.plugins.linux import pslist +from volatility3.framework.symbols.linux import network vollog = logging.getLogger(__name__) @@ -20,19 +22,29 @@ vollog = logging.getLogger(__name__) class SockHandlers(interfaces.configuration.VersionableInterface): """Handles several socket families extracting the sockets information.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 22, 0) + _version = (4, 0, 0) + _net_version_required = (1, 0, 0) - _version = (3, 0, 0) - - def __init__(self, vmlinux, task, *args, **kwargs): + def __init__(self, context, vmlinux_name, task, *args, **kwargs): super().__init__(*args, **kwargs) - self._vmlinux = vmlinux + self._vmlinux = context.modules[vmlinux_name] + self._symbol_table = context.symbol_space[self._vmlinux.symbol_table_name] self._task = task + if not requirements.VersionRequirement.matches_required( + network.NetSymbols.version, self._net_version_required + ): + raise ValueError( + f"Version mismatch of volatility library NetSymbols version ({network.NetSymbols.version}) and needed version ({self._net_version_required})" + ) + + network.NetSymbols.apply(self._symbol_table) + try: netns_id = task.nsproxy.net_ns.get_inode() except AttributeError: - netns_id = NotAvailableValue() + netns_id = renderers.NotAvailableValue() self._netdevices = self._build_network_devices_map(netns_id) @@ -67,7 +79,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): ) for net_dev in net.dev_base_head.to_list(net_device_symname, "dev_list"): if ( - isinstance(netns_id, NotAvailableValue) + isinstance(netns_id, renderers.NotAvailableValue) or net.get_inode() != netns_id ): continue @@ -251,7 +263,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): # Kernel >= 3.7.10 src_port = netlink_sock.get_portid() except AttributeError: - src_port = NotAvailableValue() + src_port = renderers.NotAvailableValue() dst_addr = f"group:0x{netlink_sock.dst_group:08x}" module = netlink_sock.module @@ -261,7 +273,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): try: dst_port = netlink_sock.get_dst_portid() except AttributeError: - dst_port = NotAvailableValue() + dst_port = renderers.NotAvailableValue() state = netlink_sock.get_state() @@ -372,7 +384,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): bt_sock = sock.cast("bt_sock") def bt_addr(addr): - return ":".join(reversed(["%02x" % x for x in addr.b])) + return ":".join(reversed([f"{x:02x}" for x in addr.b])) src_addr = src_port = dst_addr = dst_port = None bt_protocol = bt_sock.get_protocol() @@ -438,8 +450,7 @@ class Sockstat(plugins.PluginInterface): """Lists all network connections for all processes.""" _required_framework_version = (2, 0, 0) - - _version = (3, 0, 0) + _version = (3, 0, 4) @classmethod def get_requirements(cls): @@ -450,19 +461,19 @@ class Sockstat(plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="SockHandlers", component=SockHandlers, version=(3, 0, 0) + name="SockHandlers", component=SockHandlers, version=(4, 0, 0) ), - requirements.PluginRequirement( - name="lsof", plugin=lsof.Lsof, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsof", component=lsof.Lsof, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) ), - requirements.BooleanRequirement( - name="unix", - description=("Show UNIX domain Sockets only"), - default=False, - optional=True, + requirements.VersionRequirement( + name="linux_net", component=network.NetSymbols, version=(1, 0, 0) ), requirements.ListRequirement( name="pids", @@ -512,32 +523,38 @@ class Sockstat(plugins.PluginInterface): fd_num, filp, _full_path = fd_internal.fd_fields task = fd_internal.task + if not (filp.f_op and filp.f_op.is_readable()): + continue + if filp.f_op not in (sfop_addr, dfop_addr): continue dentry = filp.get_dentry() - if not dentry: + if not (dentry and dentry.is_readable()): continue d_inode = dentry.d_inode - if not d_inode: + if not (d_inode and d_inode.is_readable()): continue socket_alloc = linux.LinuxUtilities.container_of( d_inode, "socket_alloc", "vfs_inode", vmlinux ) - socket = socket_alloc.socket - - if not (socket and socket.sk): + if not socket_alloc: + continue + socket = socket_alloc.socket + if not (socket.sk and socket.sk.is_readable()): continue - sock = socket.sk.dereference() - sock_type = sock.get_type() - family = sock.get_family() + try: + sock_type = sock.get_type() + family = sock.get_family() + sock_handler = SockHandlers(context, vmlinux.name, task) + sock_fields = sock_handler.process_sock(sock) + except exceptions.InvalidAddressException: + continue - sock_handler = SockHandlers(vmlinux, task) - sock_fields = sock_handler.process_sock(sock) if not sock_fields: continue @@ -548,7 +565,7 @@ class Sockstat(plugins.PluginInterface): try: netns_id = net.get_inode() except AttributeError: - netns_id = NotAvailableValue() + netns_id = renderers.NotAvailableValue() yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields @@ -563,14 +580,15 @@ class Sockstat(plugins.PluginInterface): `sock_stat` and `protocol` formatted. """ sock_stat = [ - NotAvailableValue() if field is None else str(field) for field in sock_stat + renderers.NotAvailableValue() if field is None else str(field) + for field in sock_stat ] if protocol is None: - protocol = NotAvailableValue() + protocol = renderers.NotAvailableValue() return tuple(sock_stat), protocol - def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): + def _generator(self, pids: List[int], netns_id_arg: int, kernel_module_name: str): """Enumerate tasks sockets. Each row represents a kernel socket. Args: @@ -591,9 +609,13 @@ class Sockstat(plugins.PluginInterface): tasks: String with a list of tasks and FDs using a socket. It can also have extended information such as socket filters, bpf info, etc. """ - filter_func = lsof.pslist.PsList.create_pid_filter(pids) + vmlinux = self.context.modules[kernel_module_name] + symbol_table = self.context.symbol_space[vmlinux.symbol_table_name] + network.NetSymbols.apply(symbol_table) + + filter_func = pslist.PsList.create_pid_filter(pids) socket_generator = self.list_sockets( - self.context, symbol_table, filter_func=filter_func + self.context, kernel_module_name, filter_func=filter_func ) for ( @@ -614,7 +636,7 @@ class Sockstat(plugins.PluginInterface): socket_filter_str = ( ",".join(f"{k}={v}" for k, v in extended.items()) if extended - else NotAvailableValue() + else renderers.NotAvailableValue() ) task_comm = utility.array_to_string(task.comm) @@ -638,7 +660,7 @@ class Sockstat(plugins.PluginInterface): def run(self): pids = self.config.get("pids") netns_id = self.config["netns"] - symbol_table = self.config["kernel"] + kernel_module_name = self.config["kernel"] tree_grid_args = [ ("NetNS", int), @@ -658,4 +680,6 @@ class Sockstat(plugins.PluginInterface): ("Filter", str), ] - return TreeGrid(tree_grid_args, self._generator(pids, netns_id, symbol_table)) + return renderers.TreeGrid( + tree_grid_args, self._generator(pids, netns_id, kernel_module_name) + ) diff --git a/volatility3/framework/plugins/linux/tracing/__init__.py b/volatility3/framework/plugins/linux/tracing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py new file mode 100644 index 000000000..4b7c2ebb3 --- /dev/null +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -0,0 +1,272 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf + +import logging +from typing import List, Generator +from enum import Enum +from dataclasses import dataclass + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.constants import architectures + +vollog = logging.getLogger(__name__) + + +# https://docs.python.org/3.13/library/enum.html#enum.IntFlag +class FtraceOpsFlags(Enum): + """Denote the state of an ftrace_ops struct. + Based on https://elixir.bootlin.com/linux/v6.13-rc3/source/include/linux/ftrace.h#L255. + """ + + FTRACE_OPS_FL_ENABLED = 1 << 0 + FTRACE_OPS_FL_DYNAMIC = 1 << 1 + FTRACE_OPS_FL_SAVE_REGS = 1 << 2 + FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED = 1 << 3 + FTRACE_OPS_FL_RECURSION = 1 << 4 + FTRACE_OPS_FL_STUB = 1 << 5 + FTRACE_OPS_FL_INITIALIZED = 1 << 6 + FTRACE_OPS_FL_DELETED = 1 << 7 + FTRACE_OPS_FL_ADDING = 1 << 8 + FTRACE_OPS_FL_REMOVING = 1 << 9 + FTRACE_OPS_FL_MODIFYING = 1 << 10 + FTRACE_OPS_FL_ALLOC_TRAMP = 1 << 11 + FTRACE_OPS_FL_IPMODIFY = 1 << 12 + FTRACE_OPS_FL_PID = 1 << 13 + FTRACE_OPS_FL_RCU = 1 << 14 + FTRACE_OPS_FL_TRACE_ARRAY = 1 << 15 + FTRACE_OPS_FL_PERMANENT = 1 << 16 + FTRACE_OPS_FL_DIRECT = 1 << 17 + FTRACE_OPS_FL_SUBOP = 1 << 18 + + +@dataclass +class ParsedFtraceOps: + """Parsed ftrace_ops struct representation, containing a selection of forensics valuable + information.""" + + ftrace_ops_offset: int + callback_symbol: str + callback_address: int + hooked_symbols: str + module_name: str + module_address: int + flags: str + + +class CheckFtrace(interfaces.plugins.PluginInterface): + """Detect ftrace hooking + + Investigate the ftrace infrastructure to uncover kernel attached callbacks, which can be leveraged + to hook kernel functions and modify their behaviour.""" + + _version = (4, 0, 0) + _required_framework_version = (2, 19, 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=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.BooleanRequirement( + name="show_ftrace_flags", + description="Show ftrace flags associated with an ftrace_ops struct", + optional=True, + default=False, + ), + ] + + @classmethod + def extract_hash_table_filters( + cls, + ftrace_ops: interfaces.objects.ObjectInterface, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Wrap the process of walking to every ftrace_func_entry of an ftrace_ops. + Those are stored in a hash table of filters that indicates the addresses hooked. + + Args: + ftrace_ops: The ftrace_ops struct to walk through + + Return, None, None: + An iterable of ftrace_func_entry structs + """ + + if hasattr(ftrace_ops, "func_hash"): + ftrace_hash = ftrace_ops.func_hash.filter_hash + else: + ftrace_hash = ftrace_ops.filter_hash + + try: + current_bucket_ptr = ftrace_hash.buckets.first + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VV, + f"ftrace_func_entry list of ftrace_ops@{ftrace_ops.vol.offset:#x} is empty/invalid. Skipping it...", + ) + return + + while current_bucket_ptr.is_readable(): + yield current_bucket_ptr.dereference().cast("ftrace_func_entry") + current_bucket_ptr = current_bucket_ptr.next + + @classmethod + def parse_ftrace_ops( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + known_modules: List[linux_utilities_modules.ModuleInfo], + ftrace_ops: interfaces.objects.ObjectInterface, + ) -> Generator[ParsedFtraceOps, None, None]: + """Parse an ftrace_ops struct to highlight ftrace kernel hooking. + Iterates over embedded ftrace_func_entry entries, which point to hooked memory areas. + + Args: + known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through run_modules_scanners(). + ftrace_ops: The ftrace_ops struct to parse + + Yields: + An iterable of ParsedFtraceOps dataclasses, containing a selection of useful fields (callback, hook, module) related to an ftrace_ops struct + """ + kernel = context.modules[kernel_module_name] + callback = ftrace_ops.func + + mod_info, callback_symbol = ( + linux_utilities_modules.Modules.module_lookup_by_address( + context, + kernel_module_name, + known_modules, + callback, + ) + ) + + if mod_info: + module_address = mod_info.start + module_name = mod_info.name + else: + callback_symbol = module_address = module_name = None + + vollog.debug( + f"Could not determine ftrace_ops@{ftrace_ops.vol.offset:#x} callback {callback:#x} module origin.", + ) + + # Iterate over ftrace_func_entry list + for ftrace_func_entry in cls.extract_hash_table_filters(ftrace_ops): + hook_address = ftrace_func_entry.ip.cast("pointer") + + # Determine the symbols associated with a hook + hooked_symbols = kernel.get_symbols_by_absolute_location(hook_address) + hooked_symbols = ",".join( + [ + hooked_symbol.split(constants.BANG)[-1] + for hooked_symbol in hooked_symbols + ] + ) + formatted_ftrace_flags = ",".join( + [flag.name for flag in FtraceOpsFlags if flag.value & ftrace_ops.flags] + ) + yield ParsedFtraceOps( + ftrace_ops.vol.offset, + callback_symbol, + callback, + hooked_symbols, + module_name, + module_address, + formatted_ftrace_flags, + ) + + @classmethod + def iterate_ftrace_ops_list( + cls, context: interfaces.context.ContextInterface, kernel_name: str + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Iterate over (ftrace_ops *)ftrace_ops_list. + + Returns: + An iterable of ftrace_ops structs + """ + kernel = context.modules[kernel_name] + current_frace_ops_ptr = kernel.object_from_symbol("ftrace_ops_list") + ftrace_list_end = kernel.object_from_symbol("ftrace_list_end") + + while current_frace_ops_ptr.is_readable(): + # ftrace_list_end is not considered a valid struct + # see kernel function test_rec_ops_needs_regs + if current_frace_ops_ptr != ftrace_list_end.vol.offset: + yield current_frace_ops_ptr.dereference() + current_frace_ops_ptr = current_frace_ops_ptr.next + else: + break + + def _generator(self): + kernel_name = self.config["kernel"] + kernel = self.context.modules[kernel_name] + + if not kernel.has_symbol("ftrace_ops_list"): + vollog.error( + 'The provided symbol table does not include the "ftrace_ops_list" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupted.' + ) + return + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + for ftrace_ops in self.iterate_ftrace_ops_list(self.context, kernel_name): + for ftrace_ops_parsed in self.parse_ftrace_ops( + self.context, + kernel_name, + known_modules, + ftrace_ops, + ): + formatted_results = ( + format_hints.Hex(ftrace_ops_parsed.ftrace_ops_offset), + ftrace_ops_parsed.callback_symbol or renderers.NotAvailableValue(), + format_hints.Hex(ftrace_ops_parsed.callback_address), + ftrace_ops_parsed.hooked_symbols or renderers.NotAvailableValue(), + ftrace_ops_parsed.module_name or renderers.NotAvailableValue(), + ( + format_hints.Hex(ftrace_ops_parsed.module_address) + if ftrace_ops_parsed.module_address is not None + else renderers.NotAvailableValue() + ), + ) + if self.config["show_ftrace_flags"]: + formatted_results += (ftrace_ops_parsed.flags,) + yield (0, formatted_results) + + def run(self): + columns = [ + ("ftrace_ops address", format_hints.Hex), + ("Callback", str), + ("Callback address", format_hints.Hex), + ("Hooked symbols", str), + ("Module", str), + ("Module address", format_hints.Hex), + ] + + if self.config.get("show_ftrace_flags"): + columns.append(("Flags", str)) + + return renderers.TreeGrid( + columns, + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py new file mode 100644 index 000000000..ff922784d --- /dev/null +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -0,0 +1,144 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Tuple, Generator, Optional + +from volatility3.framework import renderers, interfaces, constants, exceptions +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class PerfEvents(plugins.PluginInterface): + """Lists performance events for each process.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + ] + + @classmethod + def list_perf_events(cls, context, vmlinux_module_name: str) -> Generator[ + Tuple[ + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + Optional[str], + Optional[str], + Optional[str], + Optional[int], + ], + None, + None, + ]: + """ + Walks the `perf_event_list` of each `task_struct` and reports valid event structures found + This plugin is one of several to detect eBPF based malware + + Args: + context: + vmlinux_module_name: + + Returns: + A tuple of the task struct, performance event object, event name, program name, full name, and program address + """ + vmlinux = context.modules[vmlinux_module_name] + + if not vmlinux.has_type("perf_event") or not vmlinux.get_type( + "perf_event" + ).has_member("owner_entry"): + vollog.warning( + "This kernel does not have performance events enabled (CONFIG_PERF_EVENTS). Cannot proceed." + ) + return + + 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" + ): + # if the names are smeared then bail + try: + event_name = utility.pointer_to_string(event.pmu.name, count=64) + try: + full_name = utility.array_to_string( + event.prog.aux.ksym.name, count=512 + ) + except AttributeError: + full_name = None + + program_name = utility.array_to_string(event.prog.aux.name) + except exceptions.InvalidAddressException: + continue + + # if the kernel has the prog member then ensure it is not 0 + if hasattr(event, "prog"): + program_address = event.prog + if program_address == 0: + continue + + else: + program_address = None + + yield task, event_name, program_name, full_name, program_address + + def _generator(self): + for ( + task, + event_name, + program_name, + full_name, + program_address, + ) in self.list_perf_events(self.context, self.config["kernel"]): + task_name = utility.array_to_string(task.comm) + + # We at least need one useful string... + if event_name is None and program_name is None and full_name is None: + continue + + if program_address is not None: + program_address = format_hints.Hex(program_address) + else: + program_address = renderers.NotAvailableValue() + + yield ( + 0, + ( + task.pid, + task_name, + event_name or renderers.NotAvailableValue(), + program_name or renderers.NotAvailableValue(), + full_name or renderers.NotAvailableValue(), + program_address, + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Event", str), + ("Short Program Name", str), + ("Full Name", str), + ("Address", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py new file mode 100644 index 000000000..8666ffd6a --- /dev/null +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -0,0 +1,282 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf + +import logging +from dataclasses import dataclass +from typing import Iterable, List, Optional + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.constants import architectures +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +@dataclass +class ParsedTracepointFunc: + """Parsed tracepoint_func struct, containing a selection of forensics valuable + information.""" + + tracepoint_name: str + tracepoint_address: int + probe_name: str + probe_address: int + probe_priority: int + module_name: str + module_address: int + + +class CheckTracepoints(interfaces.plugins.PluginInterface): + """Detect tracepoints hooking + + Investigate the tracepoints subsystem to uncover kernel attached probes, which can be leveraged + to hook kernel functions and modify their behaviour.""" + + _version = (2, 0, 0) + _required_framework_version = (2, 19, 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=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + ] + + @classmethod + def iterate_tracepoint_funcs( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + tracepoint: interfaces.objects.ObjectInterface, + ) -> Optional[Iterable[interfaces.objects.ObjectInterface]]: + """Extract probes represented by tracepoint_func structs from a + tracepoint funcs member. + + Args: + tracepoint: The tracepoint struct to parse + + Yields: + An iterable of tracepoint_func structs + """ + + layer = context.layers[layer_name] + # Ignore tracepoints without attached probes + if not tracepoint.funcs.is_readable(): + return None + + current_tracepoint_func = tracepoint.funcs.dereference() + # Inspired by kernel's debug_print_probes() + while ( + layer.is_valid(current_tracepoint_func.vol.offset) + and current_tracepoint_func.func.is_readable() + ): + yield current_tracepoint_func + current_tracepoint_func = context.object( + tracepoint.get_symbol_table_name() + constants.BANG + "tracepoint_func", + layer_name, + current_tracepoint_func.vol.offset + current_tracepoint_func.vol.size, + ) + + @classmethod + def parse_tracepoint( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + known_modules: List[linux_utilities_modules.ModuleInfo], + tracepoint: interfaces.objects.ObjectInterface, + run_hidden_modules: bool = True, + ) -> Optional[Iterable[ParsedTracepointFunc]]: + """Parse a tracepoint struct to highlight tracepoints kernel hooking. + + Args: + known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through run_modules_scanners(). + tracepoint: The tracepoint struct to parse + run_hidden_modules: Whether to run the hidden_modules plugin or not. Note: it won't be run, even if specified, \ + if the "hidden_modules" key is present in known_modules. + + Yields: + An iterable of ParsedTracepointFunc dataclasses, containing a selection of useful fields related to a tracepoint struct + """ + kernel = context.modules[kernel_module_name] + + for tracepoint_func in cls.iterate_tracepoint_funcs( + context, kernel.layer_name, tracepoint + ): + try: + tracepoint_name = utility.pointer_to_string(tracepoint.name, count=512) + except exceptions.InvalidAddressException: + vollog.debug( + f"Tracepoint function at {tracepoint.vol.offset:#x} is smeared." + ) + continue + + probe_handler_address = tracepoint_func.func + probe_handler_symbol = module_address = module_name = None + + # Try to lookup within the known modules if the probe_handler address fits + mod_info, probe_handler_symbol = ( + linux_utilities_modules.Modules.module_lookup_by_address( + context, + kernel_module_name, + known_modules, + probe_handler_address, + ) + ) + + # Fetch more information about the module + if mod_info is not None: + module_address = mod_info.offset + module_name = mod_info.name + else: + vollog.debug( + f"Could not determine tracepoint@{tracepoint.vol.offset:#x} probe handler {probe_handler_address:#x} module origin.", + ) + + if hasattr(tracepoint_func, "prio"): + prio = tracepoint_func.prio + else: + prio = None + + yield ParsedTracepointFunc( + tracepoint_name, + tracepoint.vol.offset, + probe_handler_symbol, + probe_handler_address, + prio, + module_name, + module_address, + ) + + @classmethod + def iterate_tracepoints_array( + cls, context: interfaces.context.ContextInterface, kernel_name: str + ) -> List[interfaces.objects.ObjectInterface]: + """Iterate over (tracepoint_ptr_t *)__start___tracepoints_ptrs. + Handles CONFIG_HAVE_ARCH_PREL32_RELOCATIONS. + + Returns: + A list of tracepoint structs + """ + + kernel = context.modules[kernel_name] + + tracepoints = [] + tracepoints_start = kernel.object_from_symbol("__start___tracepoints_ptrs") + tracepoints_end = kernel.get_absolute_symbol_address( + "__stop___tracepoints_ptrs" + ) + tracepoints_array_size = tracepoints_end - tracepoints_start.vol.offset + # kernel's tracepoint_ptr_deref() and tracepoint_ptr_t + # adjust depending on the use of PC-relative addressing + # or not. + # Relocation is commonly used to store pointers as offsets + # relative to their own address rather than absolute addresses/pointers. + config_have_arch_prel32_relocations = ( + tracepoints_start.vol.subtype.type_name + == kernel.symbol_table_name + constants.BANG + "int" + ) + if config_have_arch_prel32_relocations: + tracepoints_relative_offsets = tracepoints_start.cast( + "array", + count=tracepoints_array_size // kernel.get_type("int").size, + subtype=kernel.get_type("int"), + ) + for relative_offset in tracepoints_relative_offsets: + # relative_offset is the value stored at relative_offset.vol.offset + # See kernel's offset_to_ptr(). Example: + # 0xffff9da125e0 = 0x7af138 + 0xffff9d2634a8 + absolute_address = relative_offset + relative_offset.vol.offset + tracepoint = kernel.object( + "tracepoint", + absolute_address, + absolute=True, + ) + tracepoints.append(tracepoint) + else: + tracepoints = utility.array_of_pointers( + tracepoints_start, + tracepoints_array_size // kernel.get_type("pointer").size, + kernel.symbol_table_name + constants.BANG + "tracepoint", + context, + ) + + return tracepoints + + def _generator(self): + kernel_name = self.config["kernel"] + kernel = self.context.modules[kernel_name] + kernel_layer = self.context.layers[kernel.layer_name] + + if not kernel.has_symbol("__start___tracepoints_ptrs"): + vollog.error( + 'The provided symbol table does not include the "__start___tracepoints_ptrs" symbol.' + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupted." + ) + return + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + tracepoints = self.iterate_tracepoints_array(self.context, kernel_name) + + for tracepoint in tracepoints: + if not kernel_layer.is_valid(tracepoint.vol.offset): + continue + + for tracepoint_parsed in self.parse_tracepoint( + self.context, kernel_name, known_modules, tracepoint + ): + formatted_results = ( + tracepoint_parsed.tracepoint_name, + format_hints.Hex(tracepoint_parsed.tracepoint_address), + tracepoint_parsed.probe_name or renderers.NotAvailableValue(), + format_hints.Hex(tracepoint_parsed.probe_address), + tracepoint_parsed.probe_priority or renderers.NotAvailableValue(), + tracepoint_parsed.module_name or renderers.NotAvailableValue(), + ( + format_hints.Hex(tracepoint_parsed.module_address) + if tracepoint_parsed.module_address is not None + else renderers.NotAvailableValue() + ), + ) + yield ( + 0, + formatted_results, + ) + + def run(self): + columns = [ + ("tracepoint", str), + ("tracepoint address", format_hints.Hex), + ("Probe", str), + ("Probe address", format_hints.Hex), + ("Probe priority", int), + ("Module", str), + ("Module address", format_hints.Hex), + ] + + return renderers.TreeGrid( + columns, + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index 45238ef8c..36bfb1b5a 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -1,97 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List - -from volatility3.framework import interfaces, renderers, exceptions, constants -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux -from volatility3.plugins.linux import lsmod +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import tty_check as ttycheck vollog = logging.getLogger(__name__) -class tty_check(plugins.PluginInterface): - """Checks tty devices for hooks""" +class tty_check( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=ttycheck.Tty_Check, + removal_date="2026-06-07", +): + """Checks tty devices for hooks (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - - handlers = linux.LinuxUtilities.generate_kernel_handler_info( - self.context, vmlinux.name, modules - ) - - try: - tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") - except exceptions.SymbolError: - tty_drivers = None - - if not tty_drivers: - raise TypeError( - "This plugin requires the tty_drivers structure." - "This structure is not present in the supplied symbol table." - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - for tty in tty_drivers.to_list( - vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" - ): - try: - ttys = utility.array_of_pointers( - tty.ttys.dereference(), - count=tty.num, - subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct", - context=self.context, - ) - except exceptions.PagedInvalidAddressException: - continue - - for tty_dev in ttys: - if tty_dev == 0: - continue - - name = utility.array_to_string(tty_dev.name) - - recv_buf = tty_dev.ldisc.ops.receive_buf - - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( - vmlinux, handlers, recv_buf - ) - - yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name)) - - def run(self): - return renderers.TreeGrid( - [ - ("Name", str), - ("Address", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py new file mode 100644 index 000000000..2fde3aadc --- /dev/null +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -0,0 +1,136 @@ +# 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 +# + +import logging +import re +from typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.layers import scanners +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class VmaRegExScan(plugins.PluginInterface): + """Scans all virtual memory areas for tasks using RegEx.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 2) + + MAXSIZE_DEFAULT = 128 + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.StringRequirement( + name="pattern", description="RegEx pattern", optional=False + ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size in bytes for displayed context", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), + ] + + def _generator(self, regex_pattern, tasks): + regex_pattern = bytes(regex_pattern, "UTF-8") + vollog.debug(f"RegEx Pattern: {regex_pattern}") + + for task in tasks: + if not task.mm: + continue + name = utility.array_to_string(task.comm) + + # attempt to create a process layer for each task and skip those + # that cannot (e.g. kernel threads) + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + continue + + # get the proc_layer object from the context + proc_layer = self.context.layers[proc_layer_name] + + # get process sections for scanning + sections = [ + (start, size) for (start, size) in task.get_process_memory_sections() + ] + + for offset in proc_layer.scan( + context=self.context, + scanner=scanners.RegExScanner(regex_pattern), + sections=sections, + progress_callback=self._progress_callback, + ): + result_data = proc_layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) + + # reapply the regex in order to extract just the match + regex_result = re.match(regex_pattern, result_data) + + if regex_result: + # the match is within the results_data (e.g. it fits within MAXSIZE_DEFAULT) + # extract just the match itself + regex_match = regex_result.group(0) + text_result = str(regex_match, encoding="UTF-8", errors="replace") + bytes_result = regex_match + else: + # the match is not with the results_data (e.g. it doesn't fit within MAXSIZE_DEFAULT) + text_result = str(result_data, encoding="UTF-8", errors="replace") + bytes_result = result_data + + user_pid = task.tgid + yield ( + 0, + ( + user_pid, + name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("Text", str), + ("Hex", bytes), + ], + self._generator( + self.config.get("pattern"), + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ), + ), + ) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 9fe06b0c8..d9466f512 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging from typing import Iterable, List, Tuple from volatility3.framework import interfaces, renderers @@ -10,12 +11,14 @@ from volatility3.framework.renderers import format_hints from volatility3.plugins import yarascan from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class VmaYaraScan(interfaces.plugins.PluginInterface): """Scans all virtual memory areas for tasks using yara.""" - _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + _required_framework_version = (2, 22, 0) + _version = (1, 0, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -27,11 +30,14 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): description="Process IDs to include (all other processes are excluded)", optional=True, ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), - requirements.PluginRequirement( - name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) ), requirements.ModuleRequirement( name="kernel", @@ -50,6 +56,8 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): # use yarascan to parse the yara options provided and create the rules rules = yarascan.YaraScan.process_yara_options(dict(self.config)) + sanity_check = 1024 * 1024 * 1024 # 1 GB + # filter based on the pid option if provided filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for task in pslist.PsList.list_tasks( @@ -66,32 +74,49 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): # get the proc_layer object from the context proc_layer = self.context.layers[proc_layer_name] - for start, end in self.get_vma_maps(task): - for match in rules.match( - data=proc_layer.read(start, end - start, True) - ): - if yarascan.YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield 0, ( - format_hints.Hex(offset + start), - task.tgid, - match.rule, - name, - value, - ) + max_vma_size = 0 + vma_maps_to_scan = [] + for start, size in self.get_vma_maps(task): + if size > sanity_check: + vollog.debug( + f"VMA at 0x{start:x} over sanity-check size, not scanning" + ) + continue + max_vma_size = max(max_vma_size, size) + vma_maps_to_scan.append((start, size)) - @staticmethod + if not vma_maps_to_scan: + vollog.warning(f"No VMAs were found for task {task.tgid}, not scanning") + continue + + scanner = yarascan.YaraScanner(rules=rules) + scanner.chunk_size = max_vma_size + + # scan the VMA data (in one contiguous block) with the yarascanner + for start, size in vma_maps_to_scan: + for offset, rule_name, name, value in scanner( + proc_layer.read(start, size, pad=True), start + ): + layer_data = renderers.LayerData( + context=self.context, + offset=offset, + layer_name=proc_layer.name, + length=len(value), + ) + yield ( + 0, + ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + layer_data, + ), + ) + + @classmethod def get_vma_maps( + cls, task: interfaces.objects.ObjectInterface, ) -> Iterable[Tuple[int, int]]: """Creates a map of start/end addresses for each virtual memory area in a task. @@ -114,7 +139,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): ("PID", int), ("Rule", str), ("Component", str), - ("Value", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/linux/vmcoreinfo.py b/volatility3/framework/plugins/linux/vmcoreinfo.py new file mode 100644 index 000000000..658626014 --- /dev/null +++ b/volatility3/framework/plugins/linux/vmcoreinfo.py @@ -0,0 +1,54 @@ +# 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 typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols import linux +from volatility3.framework.renderers import format_hints + + +class VMCoreInfo(plugins.PluginInterface): + """Enumerate VMCoreInfo tables""" + + _required_framework_version = (2, 11, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.TranslationLayerRequirement( + name="primary", description="Memory layer to scan" + ), + requirements.VersionRequirement( + name="VMCoreInfo", component=linux.VMCoreInfo, version=(1, 0, 0) + ), + ] + + def _generator(self): + layer_name = self.config["primary"] + for ( + vmcoreinfo_offset, + vmcoreinfo, + ) in linux.VMCoreInfo.search_vmcoreinfo_elf_note( + context=self.context, + layer_name=layer_name, + ): + for key, value in vmcoreinfo.items(): + if key.startswith("SYMBOL(") or key == "KERNELOFFSET": + value = hex(value) + else: + value = str(value) + + yield 0, (format_hints.Hex(vmcoreinfo_offset), key, value) + + def run(self): + headers = [ + ("Offset", format_hints.Hex), + ("Key", str), + ("Value", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/mac/bash.py b/volatility3/framework/plugins/mac/bash.py index a52ae616a..ac10d4f4a 100644 --- a/volatility3/framework/plugins/mac/bash.py +++ b/volatility3/framework/plugins/mac/bash.py @@ -12,7 +12,7 @@ from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.layers import scanners from volatility3.framework.objects import utility -from volatility3.framework.symbols.linux.bash import BashIntermedSymbols +from volatility3.framework.symbols.linux import bash from volatility3.plugins import timeliner from volatility3.plugins.mac import pslist @@ -30,8 +30,23 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), requirements.ListRequirement( name="pid", @@ -44,7 +59,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): def _generator(self, tasks): darwin = self.context.modules[self.config["kernel"]] is_32bit = not symbols.symbol_table_is_64bit( - self.context, darwin.symbol_table_name + context=self.context, symbol_table_name=darwin.symbol_table_name ) if is_32bit: pack_format = "I" @@ -53,7 +68,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): pack_format = "Q" bash_json_file = "bash64" - bash_table_name = BashIntermedSymbols.create( + bash_table_name = bash.BashIntermedSymbols.create( self.context, self.config_path, "linux", bash_json_file ) diff --git a/volatility3/framework/plugins/mac/check_syscall.py b/volatility3/framework/plugins/mac/check_syscall.py index 5c22e6463..ed86b1a41 100644 --- a/volatility3/framework/plugins/mac/check_syscall.py +++ b/volatility3/framework/plugins/mac/check_syscall.py @@ -31,8 +31,8 @@ class Check_syscall(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/mac/check_sysctl.py b/volatility3/framework/plugins/mac/check_sysctl.py index 4f64eaed8..d9c9a4dbd 100644 --- a/volatility3/framework/plugins/mac/check_sysctl.py +++ b/volatility3/framework/plugins/mac/check_sysctl.py @@ -33,8 +33,8 @@ class Check_sysctl(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] @@ -60,7 +60,7 @@ class Check_sysctl(plugins.PluginInterface): return var_str def _process_sysctl_list(self, kernel, sysctl_list, recursive=0): - if type(sysctl_list) == volatility3.framework.objects.Pointer: + if type(sysctl_list) is volatility3.framework.objects.Pointer: sysctl_list = sysctl_list.dereference().cast("sysctl_oid_list") sysctl = sysctl_list.slh_first @@ -93,10 +93,9 @@ class Check_sysctl(plugins.PluginInterface): val = self._parse_global_variable_sysctls(kernel, name) elif ctltype == "CTLTYPE_NODE": if sysctl.oid_handler == 0: - for info in self._process_sysctl_list( + yield from self._process_sysctl_list( kernel, sysctl.oid_arg1, recursive=1 - ): - yield info + ) val = "Node" diff --git a/volatility3/framework/plugins/mac/check_trap_table.py b/volatility3/framework/plugins/mac/check_trap_table.py index 60f237208..6e0f4b8a9 100644 --- a/volatility3/framework/plugins/mac/check_trap_table.py +++ b/volatility3/framework/plugins/mac/check_trap_table.py @@ -29,8 +29,8 @@ class Check_trap_table(plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) diff --git a/volatility3/framework/plugins/mac/kauth_listeners.py b/volatility3/framework/plugins/mac/kauth_listeners.py index ed43bfb42..ca236c04a 100644 --- a/volatility3/framework/plugins/mac/kauth_listeners.py +++ b/volatility3/framework/plugins/mac/kauth_listeners.py @@ -26,11 +26,13 @@ class Kauth_listeners(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 1, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="kauth_scopes", plugin=kauth_scopes.Kauth_scopes, version=(2, 0, 0) + requirements.VersionRequirement( + name="kauth_scopes", + component=kauth_scopes.Kauth_scopes, + version=(2, 0, 0), ), ] diff --git a/volatility3/framework/plugins/mac/kauth_scopes.py b/volatility3/framework/plugins/mac/kauth_scopes.py index afb320a07..6420d9955 100644 --- a/volatility3/framework/plugins/mac/kauth_scopes.py +++ b/volatility3/framework/plugins/mac/kauth_scopes.py @@ -31,8 +31,8 @@ class Kauth_scopes(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 1, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] @@ -80,7 +80,7 @@ class Kauth_scopes(interfaces.plugins.PluginInterface): ( identifier, format_hints.Hex(scope.ks_idata), - len([l for l in scope.get_listeners()]), + len([listener for listener in scope.get_listeners()]), format_hints.Hex(callback), module_name, symbol_name, diff --git a/volatility3/framework/plugins/mac/kevents.py b/volatility3/framework/plugins/mac/kevents.py index 2a8692b77..e36de8c84 100644 --- a/volatility3/framework/plugins/mac/kevents.py +++ b/volatility3/framework/plugins/mac/kevents.py @@ -71,8 +71,8 @@ class Kevents(interfaces.plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 2, 0) @@ -119,8 +119,7 @@ class Kevents(interfaces.plugins.PluginInterface): return None for klist in klist_array: - for kn in mac.MacUtilities.walk_slist(klist, "kn_link"): - yield kn + yield from mac.MacUtilities.walk_slist(klist, "kn_link") @classmethod def _get_task_kevents(cls, kernel, task): diff --git a/volatility3/framework/plugins/mac/list_files.py b/volatility3/framework/plugins/mac/list_files.py index c18b0b7a2..423e2e0da 100644 --- a/volatility3/framework/plugins/mac/list_files.py +++ b/volatility3/framework/plugins/mac/list_files.py @@ -28,8 +28,13 @@ class List_Files(plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="mount", plugin=mount.Mount, version=(2, 0, 0) + requirements.VersionRequirement( + name="mount", component=mount.Mount, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="mac_utilities", + component=mac.MacUtilities, + version=(1, 3, 0), ), ] 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/lsof.py b/volatility3/framework/plugins/mac/lsof.py index 6832b837f..3191aeff6 100644 --- a/volatility3/framework/plugins/mac/lsof.py +++ b/volatility3/framework/plugins/mac/lsof.py @@ -29,8 +29,8 @@ class Lsof(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", diff --git a/volatility3/framework/plugins/mac/malfind.py b/volatility3/framework/plugins/mac/malfind.py index 3094ada85..fd8915aac 100644 --- a/volatility3/framework/plugins/mac/malfind.py +++ b/volatility3/framework/plugins/mac/malfind.py @@ -13,7 +13,7 @@ from volatility3.plugins.mac import pslist class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 0, 1) @classmethod def get_requirements(cls): @@ -23,8 +23,8 @@ class Malfind(interfaces.plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -68,9 +68,7 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly( - data, vma.links.start, architecture - ) + disasm = renderers.Disassembly(data, vma.links.start, architecture) yield ( 0, @@ -99,7 +97,7 @@ class Malfind(interfaces.plugins.PluginInterface): ("End", format_hints.Hex), ("Protection", str), ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), + ("Disasm", renderers.Disassembly), ], self._generator( list_tasks(self.context, self.config["kernel"], filter_func=filter_func) diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index ff654e1a7..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 @@ -11,8 +12,8 @@ from volatility3.framework.symbols import mac class Mount(plugins.PluginInterface): - """A module containing a collection of plugins that produce data typically - found in Mac's mount command""" + """A module containing a collection of plugins that produce data typically \ +found in Mac's mount command""" _required_framework_version = (2, 0, 0) @@ -49,8 +50,7 @@ class Mount(plugins.PluginInterface): list_head = kernel.object_from_symbol(symbol_name="mountlist") - for mount in mac.MacUtilities.walk_tailq(list_head, "mnt_list"): - yield mount + yield from mac.MacUtilities.walk_tailq(list_head, "mnt_list") def _generator(self): for mount in self.list_mounts(self.context, self.config["kernel"]): diff --git a/volatility3/framework/plugins/mac/netstat.py b/volatility3/framework/plugins/mac/netstat.py index 76bba25f6..2eb7132f2 100644 --- a/volatility3/framework/plugins/mac/netstat.py +++ b/volatility3/framework/plugins/mac/netstat.py @@ -29,8 +29,8 @@ class Netstat(plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) diff --git a/volatility3/framework/plugins/mac/proc_maps.py b/volatility3/framework/plugins/mac/proc_maps.py index fe5179dfa..87f3559ea 100644 --- a/volatility3/framework/plugins/mac/proc_maps.py +++ b/volatility3/framework/plugins/mac/proc_maps.py @@ -28,8 +28,8 @@ class Maps(interfaces.plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -115,9 +115,7 @@ class Maps(interfaces.plugins.PluginInterface): proc_layer_name = task.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - pid, excp.invalid_address, excp.layer_name - ) + f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) return None vm_size = vm_end - vm_start @@ -154,7 +152,9 @@ class Maps(interfaces.plugins.PluginInterface): address_list = self.config.get("address", None) if not address_list: # do not filter as no address_list was supplied - vma_filter_func = lambda _: True + def vma_filter_func(_): + return True + else: # filter for any vm_start that matches the supplied address config def vma_filter_function(task: interfaces.objects.ObjectInterface) -> bool: diff --git a/volatility3/framework/plugins/mac/psaux.py b/volatility3/framework/plugins/mac/psaux.py index 28c238263..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 @@ -24,8 +25,8 @@ class Psaux(plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 9b570f3f9..904e4e201 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -4,7 +4,7 @@ import datetime import logging -from typing import Callable, Dict, Iterable, List +from typing import Callable, Dict, Iterable, List, Optional from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements @@ -56,7 +56,7 @@ class PsList(interfaces.plugins.PluginInterface): """Returns the list_tasks method based on the selector Args: - method: Must be one fo the available methods in get_task_choices + method: Must be one of the available methods in get_task_choices Returns: list_tasks method for listing tasks @@ -82,8 +82,12 @@ class PsList(interfaces.plugins.PluginInterface): return list_tasks @classmethod - def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[int], bool]: - filter_func = lambda _: False + def create_pid_filter( + cls, pid_list: Optional[List[int]] = None + ) -> Callable[[int], bool]: + def filter_func(_): + return False + # FIXME: mypy #4973 or #2608 pid_list = pid_list or [] filter_list = [x for x in pid_list if x is not None] @@ -131,7 +135,7 @@ class PsList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_module_name: The name of the the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate filter_func: A function which takes a process object and returns True if the process should be ignored/filtered Returns: @@ -176,7 +180,7 @@ class PsList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_module_name: The name of the the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate filter_func: A function which takes a task object and returns True if the task should be ignored/filtered Returns: @@ -220,7 +224,7 @@ class PsList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_module_name: The name of the the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate filter_func: A function which takes a task object and returns True if the task should be ignored/filtered Returns: @@ -255,7 +259,7 @@ class PsList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_module_name: The name of the the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate filter_func: A function which takes a task object and returns True if the task should be ignored/filtered Returns: @@ -293,7 +297,7 @@ class PsList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_module_name: The name of the the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate filter_func: A function which takes a task object and returns True if the task should be ignored/filtered Returns: diff --git a/volatility3/framework/plugins/mac/pstree.py b/volatility3/framework/plugins/mac/pstree.py index e62d5eb72..260029b11 100644 --- a/volatility3/framework/plugins/mac/pstree.py +++ b/volatility3/framework/plugins/mac/pstree.py @@ -10,8 +10,7 @@ from volatility3.plugins.mac import pslist class PsTree(plugins.PluginInterface): - """Plugin for listing processes in a tree based on their parent process - ID.""" + """Plugin for listing processes in a tree based on their parent process ID.""" _required_framework_version = (2, 0, 0) @@ -29,8 +28,8 @@ class PsTree(plugins.PluginInterface): description="Kernel module for the OS", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), ] diff --git a/volatility3/framework/plugins/mac/socket_filters.py b/volatility3/framework/plugins/mac/socket_filters.py index 49e77163e..2675ccdd0 100644 --- a/volatility3/framework/plugins/mac/socket_filters.py +++ b/volatility3/framework/plugins/mac/socket_filters.py @@ -32,8 +32,8 @@ class Socket_filters(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/mac/timers.py b/volatility3/framework/plugins/mac/timers.py index 8a267bd55..aef8c5e1c 100644 --- a/volatility3/framework/plugins/mac/timers.py +++ b/volatility3/framework/plugins/mac/timers.py @@ -31,8 +31,8 @@ class Timers(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 3, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/mac/trustedbsd.py b/volatility3/framework/plugins/mac/trustedbsd.py index a03e2a903..3d76a018b 100644 --- a/volatility3/framework/plugins/mac/trustedbsd.py +++ b/volatility3/framework/plugins/mac/trustedbsd.py @@ -33,8 +33,8 @@ class Trustedbsd(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 3, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py new file mode 100644 index 000000000..2c188ba4c --- /dev/null +++ b/volatility3/framework/plugins/regexscan.py @@ -0,0 +1,97 @@ +# 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 +# + +import logging +import re +from typing import List + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.layers import scanners +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class RegExScan(plugins.PluginInterface): + """Scans kernel memory using RegEx patterns.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + MAXSIZE_DEFAULT = 128 + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.StringRequirement( + name="pattern", description="RegEx pattern", optional=False + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size in bytes for displayed context", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + ] + + def _generator(self, context, layer_name, pattern, maxsize): + layer = self.context.layers[layer_name] + vollog.debug(f"RegEx Pattern: {pattern}") + + # Convert string pattern to bytes for RegExScanner + pattern_bytes = pattern.encode("utf-8") + + # Compile the pattern here to ensure consistency + try: + compiled_pattern = re.compile(pattern_bytes) + except re.error as e: + vollog.error(f"Invalid regex pattern: {e}") + raise ValueError(f"Invalid regex pattern: {e}") + + for offset in layer.scan( + context=context, scanner=scanners.RegExScanner(pattern_bytes) + ): + result_data = layer.read(offset, maxsize, pad=True) + + # reapply the regex in order to extract just the match + regex_result = compiled_pattern.search(result_data) + + if regex_result: + # the match is within the results_data (e.g. it fits within maxsize) + # extract just the match itself + regex_match = regex_result.group(0) + text_result = str(regex_match, encoding="UTF-8", errors="replace") + bytes_result = regex_match + else: + # the match is not with the results_data (e.g. it doesn't fit within maxsize) + text_result = str(result_data, encoding="UTF-8", errors="replace") + bytes_result = result_data + + yield 0, (format_hints.Hex(offset), text_result, bytes_result) + + def run(self): + pattern = self.config.get("pattern") + maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) + layer_name = self.config["primary"] + context = self.context + + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Text", str), + ("Hex", bytes), + ], + self._generator(context, layer_name, pattern, maxsize), + ) 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/timeliner.py b/volatility3/framework/plugins/timeliner.py index c754e43ef..f65868705 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -25,10 +25,14 @@ class TimeLinerType(enum.IntEnum): CHANGED = 4 -class TimeLinerInterface(metaclass=abc.ABCMeta): +class TimeLinerInterface( + interfaces.configuration.VersionableInterface, metaclass=abc.ABCMeta +): """Interface defining methods that timeliner will use to generate a body file.""" + _version = (1, 0, 0) + @abc.abstractmethod def generate_timeline( self, @@ -41,8 +45,8 @@ class TimeLinerInterface(metaclass=abc.ABCMeta): class Timeliner(interfaces.plugins.PluginInterface): - """Runs all relevant plugins that provide time related information and - orders the results by time.""" + """Runs all relevant plugins that provide time related information and \ +orders the results by time.""" _required_framework_version = (2, 0, 0) _version = (1, 1, 0) @@ -54,7 +58,9 @@ class Timeliner(interfaces.plugins.PluginInterface): self.automagics: Optional[List[interfaces.automagic.AutomagicInterface]] = None @classmethod - def get_usable_plugins(cls, selected_list: List[str] = None) -> List[Type]: + def get_usable_plugins( + cls, selected_list: Optional[List[str]] = None + ) -> List[Type]: # Initialize for the run plugin_list = list(framework.class_subclasses(TimeLinerInterface)) @@ -143,9 +149,7 @@ class Timeliner(interfaces.plugins.PluginInterface): times = self.timeline.get((plugin_name, item), {}) if times.get(timestamp_type, None) is not None: vollog.debug( - "Multiple timestamps for the same plugin/file combination found: {} {}".format( - plugin_name, item - ) + f"Multiple timestamps for the same plugin/file combination found: {plugin_name} {item}" ) times[timestamp_type] = timestamp self.timeline[(plugin_name, item)] = times @@ -206,8 +210,7 @@ class Timeliner(interfaces.plugins.PluginInterface): ) vollog.log(logging.DEBUG, traceback.format_exc()) - for data_item in sorted(data, key=self._sort_function): - yield data_item + yield from sorted(data, key=self._sort_function) # Write out a body file if necessary if self.config.get("create-bodyfile", True): diff --git a/volatility3/framework/plugins/vmscan.py b/volatility3/framework/plugins/vmscan.py index 64377d7d8..19d997605 100644 --- a/volatility3/framework/plugins/vmscan.py +++ b/volatility3/framework/plugins/vmscan.py @@ -26,6 +26,8 @@ class VMCSTest(enum.IntFlag): class PageStartScanner(interfaces.layers.ScannerInterface): + _version = (1, 0, 0) + def __init__(self, signatures: List[bytes], page_size: int = 0x1000): super().__init__() if not len(signatures): @@ -52,7 +54,7 @@ class PageStartScanner(interfaces.layers.ScannerInterface): class Vmscan(plugins.PluginInterface): - """Scans for Intel VT-d structues and generates VM volatility configs for them""" + """Scans for Intel VT-d structures and generates VM volatility configs for them""" _required_framework_version = (2, 2, 0) _version = (1, 0, 0) @@ -69,6 +71,11 @@ class Vmscan(plugins.PluginInterface): requirements.TranslationLayerRequirement( name="primary", description="Physical base memory layer" ), + requirements.VersionRequirement( + name="page_start_scanner", + component=PageStartScanner, + version=(1, 0, 0), + ), requirements.IntRequirement( name="log-threshold", description="Number of criteria failed to log to debug output", diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index 1e918d61c..0d1450127 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -1,650 +1,21 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -import dataclasses -import datetime -import enum -import itertools import logging -from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union -from volatility3.framework import interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import registry -from volatility3.framework.renderers import conversion -from volatility3.framework.symbols.windows.extensions import registry as reg_extensions -from volatility3.plugins import timeliner -from volatility3.plugins.windows.registry import hivelist +from volatility3.framework import deprecation, interfaces +from volatility3.plugins.windows.registry import amcache vollog = logging.getLogger(__name__) -####################################################################### -# More information about the following enums can be found in the report -# 'Analysis of the AmCache` by Blanche Lagny, 2019 -####################################################################### - -class Win8FileValName(enum.Enum): - """ - An enumeration that creates a helpful mapping of opaque Windows 8 Amcache - 'File' subkey value names to their human-readable equivalent. - """ - - ProgramID = "100" - SHA1Hash = "101" - Product = "0" - Company = "1" - Size = "6" - SizeOfImage = "7" - PEHeaderChecksum = "9" - LastModTime = "11" # REG_QWORD FILETIME - CreateTime = "12" # REG_QWORD FILETIME - Path = "15" - LastModTime2 = "17" # REG_QWORD FILETIME - Version = "d" - CompileTime = "f" # REG_QWORD UNIX EPOCH - - -class Win8ProgramValName(enum.Enum): - """ - An enumeration that creates a helpful mapping of opaque Windows 8 Amcache - 'Program' subkey value names to their human-readable equivalent. - """ - - Product = "0" - Version = "1" - Publisher = "2" - InstallTime = "a" - MSIProductCode = "11" - MSIPackageCode = "12" - ProductCode = "f" - PackageCode = "10" - - -class Win10InvAppFileValName(enum.Enum): - """ - An enumeration containing the most useful Windows 10 Amcache - 'InventoryApplicationFile' subkey value names. - """ - - FileId = "FileId" - LinkDate = "LinkDate" - LowerCaseLongPath = "LowerCaseLongPath" - ProductName = "ProductName" - ProductVersion = "ProductVersion" - ProgramID = "ProgramId" - Publisher = "Publisher" - - -class Win10InvAppValName(enum.Enum): - """ - An enumeration containing the most useful Windows 10 Amcache - 'InventoryApplication' subkey value names. - """ - - InstallDate = "InstallDate" - Name = "Name" - Publisher = "Publisher" - RootDirPath = "RootDirPath" - Version = "Version" - - -class Win10DriverBinaryValName(enum.Enum): - """ - An enumeration containing the most useful Windows 10 Amcache - 'InventoryDriverBinary' subkey value names. - """ - - DriverId = "DriverId" - DriverName = "DriverName" - DriverCompany = "DriverCompany" - Product = "Product" - Service = "Service" - DriverTimeStamp = "DriverTimeStamp" - - -class AmcacheEntryType(enum.IntEnum): - Driver = 1 - Program = 2 - File = 3 - - -NullableString = Union[str, None, interfaces.renderers.BaseAbsentValue] -NullableDatetime = Union[datetime.datetime, None, interfaces.renderers.BaseAbsentValue] - - -@dataclasses.dataclass -class _AmcacheEntry: - """ - A class containing all information about an entry from the Amcache registry hive. - Because all values could potentially be paged out of memory or malformed, they are all - a union between their expected value and `interfaces.renderers.BaseAbsentValue`. - """ - - entry_type: str - path: NullableString = renderers.NotApplicableValue() - company: NullableString = renderers.NotApplicableValue() - last_modify_time: NullableDatetime = renderers.NotApplicableValue() - last_modify_time_2: NullableDatetime = renderers.NotApplicableValue() - install_time: NullableDatetime = renderers.NotApplicableValue() - compile_time: NullableDatetime = renderers.NotApplicableValue() - sha1_hash: NullableString = renderers.NotApplicableValue() - service: NullableString = renderers.NotApplicableValue() - product_name: NullableString = renderers.NotApplicableValue() - product_version: NullableString = renderers.NotApplicableValue() - - -def _entry_sort_key(entry_tuple: Tuple[NullableString, _AmcacheEntry]) -> str: - """Sorts entries by program ID. This is broken out as a function here - to ensure consistency in sorting between the `group_by` and `sorted` function - invocations. - """ - program_id, _ = entry_tuple - key = program_id if isinstance(program_id, str) else "" - return key - - -def _get_string_value( - values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str -) -> NullableString: - try: - value = values[name] - except KeyError: - return renderers.NotAvailableValue() - - data = value.decode_data() - if not isinstance(data, bytes): - return renderers.UnparsableValue() - - return data.decode("utf-16le", errors="replace").rstrip("\u0000") - - -def _get_datetime_filetime_value( - values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str -) -> NullableDatetime: - try: - value = values[name] - except KeyError: - return renderers.NotAvailableValue() - - data = value.decode_data() - if not isinstance(data, int): - return renderers.UnparsableValue() - - return conversion.wintime_to_datetime(data) - - -def _get_datetime_utc_epoch_value( - values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str -) -> NullableDatetime: - try: - value = values[name] - except KeyError: - return renderers.NotAvailableValue() - - data = value.decode_data() - if not isinstance(data, (int, float)): - return renderers.UnparsableValue() - - try: - return datetime.datetime.fromtimestamp(float(data), datetime.timezone.utc) - except (ValueError, OverflowError, OSError): - return renderers.UnparsableValue() - - -def _get_datetime_str_value( - values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str -) -> NullableDatetime: - try: - value = values[name] - except KeyError: - return renderers.NotAvailableValue() - - data = value.decode_data() - if not isinstance(data, int): - return renderers.UnparsableValue() - - if isinstance(data, str): - try: - return datetime.datetime.strptime(data, "%m/%d/%Y %H:%M:%S") - except ValueError: - return renderers.UnparsableValue() - else: - return renderers.UnparsableValue() - - -class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): - """Extract information on executed applications from the AmCache.""" +class Amcache( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=amcache.Amcache, + removal_date="2026-09-25", +): + """Extract information on executed applications from the AmCache (deprecated).""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) - ), - ] - - def generate_timeline( - self, - ) -> 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 - 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 - if isinstance(entry.install_time, datetime.datetime): - 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 - - @classmethod - def get_amcache_hive( - cls, - context: interfaces.context.ContextInterface, - config_path: str, - kernel: interfaces.context.ModuleInterface, - ) -> Optional[registry.RegistryHive]: - """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" - return next( - hivelist.HiveList.list_hives( - context=context, - base_config_path=interfaces.configuration.path_join( - config_path, "hivelist" - ), - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_string="amcache", - ), - None, - ) - - @classmethod - def parse_file_key( - cls, file_key: reg_extensions.CM_KEY_NODE - ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: - """Parses File entries from the Windows 8 `Root\\File` key. - - :param programs_key: The `Root\\File` registry key. - - :return: An iterator of tuples, where the first member is the program ID string for - correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. - """ - - val_enum = Win8FileValName - - wanted_values = [key.value for key in val_enum] - - for file_entry_key in itertools.chain( - *(key.get_subkeys() for key in file_key.get_subkeys()) - ): - vollog.debug(f"Checking Win8 File key {file_entry_key.get_name()}") - values = { - str(value.get_name()): value - for value in file_entry_key.get_values() - if value.get_name() in wanted_values - } - - program_id = _get_string_value(values, val_enum.ProgramID.value) - path = _get_string_value(values, val_enum.Path.value) - company = _get_string_value(values, val_enum.Company.value) - last_mod_time = _get_datetime_filetime_value( - values, val_enum.LastModTime.value - ) - last_mod_time_2 = _get_datetime_filetime_value( - values, val_enum.LastModTime2.value - ) - install_time = _get_datetime_filetime_value( - values, val_enum.CreateTime.value - ) - compile_time = _get_datetime_utc_epoch_value( - values, val_enum.CompileTime.value - ) - sha1_hash = _get_string_value(values, val_enum.SHA1Hash.value) - 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 - ), - product_name=product_name, - ) - - @classmethod - def parse_programs_key( - cls, programs_key: reg_extensions.CM_KEY_NODE - ) -> Iterator[Tuple[str, _AmcacheEntry]]: - """Parses Program entries from the Windows 8 `Root\\Programs` key. - - :param programs_key: The `Root\\Programs` registry key. - - :return: An iterator of tuples, where the first member is the program ID string for - correlating `Root\\File` entries, and the second member is the `AmcacheEntry`. - """ - val_enum = Win8ProgramValName - - wanted_values = [key.value for key in val_enum] - for program_key in programs_key.get_subkeys(): - values = { - str(value.get_name()): value - for value in program_key.get_values() - if value.get_name() in wanted_values - } - vollog.debug(f"Parsing Win8 Program key {program_key.get_name()}") - program_id = program_key.get_name().strip().strip("\u0000") - - product = _get_string_value(values, val_enum.Product.value) - company = _get_string_value(values, val_enum.Publisher.value) - install_time = _get_datetime_utc_epoch_value( - values, val_enum.InstallTime.value - ) - 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 - ), - install_time=install_time, - product_name=product, - product_version=version, - ) - - @classmethod - def parse_inventory_app_key( - cls, inv_app_key: reg_extensions.CM_KEY_NODE - ) -> Iterator[Tuple[str, _AmcacheEntry]]: - """Parses InventoryApplication entries from the Windows 10 `Root\\InventoryApplication` key. - - :param programs_key: The `Root\\InventoryApplication` registry key. - - :return: An iterator of tuples, where the first member is the program ID string for - correlating `Root\\InventoryApplicationFile` entries, and the second member is the `AmcacheEntry`. - """ - val_enum = Win10InvAppValName - - wanted_values = [key.value for key in val_enum] - - for program_key in inv_app_key.get_subkeys(): - program_id = program_key.get_name() - - values = { - str(value.get_name()): value - for value in program_key.get_values() - if value.get_name() in wanted_values - } - - name = _get_string_value(values, val_enum.Name.value) - version = _get_string_value(values, val_enum.Version.value) - publisher = _get_string_value(values, val_enum.Publisher.value) - path = _get_string_value(values, val_enum.RootDirPath.value) - install_date = _get_datetime_str_value(values, val_enum.InstallDate.value) - last_mod = conversion.wintime_to_datetime( - program_key.LastWriteTime.QuadPart - ) - - 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, - ) - - @classmethod - def parse_inventory_app_file_key( - cls, inv_app_file_key: reg_extensions.CM_KEY_NODE - ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: - """Parses executable file entries from the `Root\\InventoryApplicationFile` registry key. - - :param inv_app_file_key: The `Root\\InventoryApplicationFile` registry key. - :return: An iterator of tuples, where the first member is the program ID string for correlating - with it's parent `InventoryApplication` program entry, and the second member is the `Amcache` entry. - """ - - val_enum = Win10InvAppFileValName - - wanted_values = [key.value for key in val_enum] - - for file_key in inv_app_file_key.get_subkeys(): - - vollog.debug( - f"Parsing Win10 InventoryApplicationFile key {file_key.get_name()}" - ) - - values = { - str(value.get_name()): value - for value in file_key.get_values() - if value.get_name() in wanted_values - } - - last_mod = conversion.wintime_to_datetime(file_key.LastWriteTime.QuadPart) - path = _get_string_value(values, val_enum.LowerCaseLongPath.value) - linkdate = _get_datetime_str_value(values, val_enum.LinkDate.value) - sha1_hash = _get_string_value(values, val_enum.FileId.value) - publisher = _get_string_value(values, val_enum.Publisher.value) - prod_name = _get_string_value(values, val_enum.ProductName.value) - 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 - ), - product_name=prod_name, - product_version=prod_ver, - ) - - @classmethod - def parse_driver_binary_key( - cls, driver_binary_key: reg_extensions.CM_KEY_NODE - ) -> Iterator[_AmcacheEntry]: - """Parses information about installed drivers from the `Root\\InventoryDriverBinary` registry key. - - :param driver_binary_key: The `Root\\InventoryDriverBinary` registry key - :return: An iterator of `AmcacheEntry`s - """ - val_enum = Win10DriverBinaryValName - - 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() - if value.get_name() in wanted_values - } - - # Depending on the Windows version, the key name will be either the name - # of the driver, or its SHA1 hash. - if "/" in str(binary_key.get_name()): - driver_name = str(binary_key.get_name()) - sha1_hash = _get_string_value(values, val_enum.DriverId.name) - else: - sha1_hash = str(binary_key.get_name()) - driver_name = _get_string_value(values, val_enum.DriverName.name) - - if isinstance(sha1_hash, str): - sha1_hash = sha1_hash[4:] if sha1_hash.startswith("0000") else sha1_hash - - company, product, service, last_write_time, driver_timestamp = ( - _get_string_value(values, val_enum.DriverCompany.name), - _get_string_value(values, val_enum.Product.name), - _get_string_value(values, val_enum.Service.name), - conversion.wintime_to_datetime(binary_key.LastWriteTime.QuadPart), - _get_datetime_utc_epoch_value(values, val_enum.DriverTimeStamp.name), - ) - - yield _AmcacheEntry( - entry_type=AmcacheEntryType.Driver.name, - path=driver_name, - company=company, - last_modify_time=last_write_time, - compile_time=driver_timestamp, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash - ), - service=service, - product_name=product, - ) - - def _generator(self) -> Iterator[Tuple[int, _AmcacheEntry]]: - kernel = self.context.modules[self.config["kernel"]] - - def indented( - entry_gen: Iterable[_AmcacheEntry], indent: int = 0 - ) -> Iterator[Tuple[int, _AmcacheEntry]]: - for item in entry_gen: - yield indent, item - - # Building the dictionary ahead of time is much better for performance - # vs looking up each service's DLL individually. - amcache = self.get_amcache_hive(self.context, self.config_path, kernel) - if amcache is None: - return - - try: - yield from indented( - self.parse_driver_binary_key( - amcache.get_key("Root\\InventoryDriverBinary") # type: ignore - ) - ) - except KeyError: - # Registry key not found - pass - - try: - programs: Dict[str, _AmcacheEntry] = { - program_id: entry - for program_id, entry in self.parse_programs_key( - amcache.get_key("Root\\Programs") - ) # type: ignore - } - except KeyError: - programs = {} - - try: - files = sorted( - list( - self.parse_file_key(amcache.get_key("Root\\File")), # type: ignore - ), - key=_entry_sort_key, - ) - except KeyError: - files = [] - - for program_id, file_entries in itertools.groupby( - files, - key=_entry_sort_key, - ): - files_indent = 0 - if isinstance(program_id, str): - try: - program_entry = programs.pop(program_id.strip().strip("\u0000")) - yield (0, program_entry) - - files_indent = 1 - except KeyError: - # No parent program for this file entry - pass - for _, entry in file_entries: - yield files_indent, entry - - for empty_program in programs.values(): - yield 0, empty_program - - try: - programs: Dict[str, _AmcacheEntry] = dict( - self.parse_inventory_app_key( - amcache.get_key("Root\\InventoryApplication") # type: ignore - ) - ) - except KeyError: - programs = {} - - try: - files = sorted( - list( - self.parse_inventory_app_file_key(amcache.get_key("Root\\InventoryApplicationFile")), # type: ignore - ), - key=_entry_sort_key, - ) - except KeyError: - files = [] - - for program_id, file_entries in itertools.groupby( - files, - key=_entry_sort_key, - ): - files_indent = 0 - - if isinstance(program_id, str): - try: - program_entry = programs.pop(program_id.strip().strip("\u0000")) - yield (0, program_entry) - files_indent = 1 - except KeyError: - # No parent program for this file entry - pass - - for _, entry in file_entries: - yield files_indent, entry - - for empty_program in programs.values(): - yield 0, empty_program - - def run(self): - - return renderers.TreeGrid( - [ - ("EntryType", str), - ("Path", str), - ("Company", str), - ("LastModifyTime", datetime.datetime), - ("LastModifyTime2", datetime.datetime), - ("InstallTime", datetime.datetime), - ("CompileTime", datetime.datetime), - ("SHA1", str), - ("Service", str), - ("ProductName", str), - ("ProductVersion", str), - ], - ( - (indent, dataclasses.astuple(entry)) - for indent, entry in self._generator() - ), - ) + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/bigpools.py b/volatility3/framework/plugins/windows/bigpools.py index 393c2a417..fabdd4306 100644 --- a/volatility3/framework/plugins/windows/bigpools.py +++ b/volatility3/framework/plugins/windows/bigpools.py @@ -21,7 +21,7 @@ class BigPools(interfaces.plugins.PluginInterface): """List big page pools.""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -50,8 +50,7 @@ class BigPools(interfaces.plugins.PluginInterface): def list_big_pools( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, tags: Optional[list] = None, show_free: bool = False, ): @@ -59,15 +58,13 @@ class BigPools(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel tags: An optional list of pool tags to filter big page pool tags by Yields: A big page pool object """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] big_page_table_offset = ntkrnlmp.get_symbol("PoolBigPageTable").address big_page_table = ntkrnlmp.object( @@ -83,8 +80,10 @@ class BigPools(interfaces.plugins.PluginInterface): big_page_table_type = ntkrnlmp.get_type("_POOL_TRACKER_BIG_PAGES") except exceptions.SymbolError: # We have to manually load a symbol table - is_vista_or_later = versions.is_vista_or_later(context, symbol_table) - is_win10 = versions.is_win10(context, symbol_table) + is_vista_or_later = versions.is_vista_or_later( + context, ntkrnlmp.symbol_table_name + ) + is_win10 = versions.is_win10(context, ntkrnlmp.symbol_table_name) if is_win10: big_pools_json_filename = "bigpools-win10" elif is_vista_or_later: @@ -92,7 +91,7 @@ class BigPools(interfaces.plugins.PluginInterface): else: big_pools_json_filename = "bigpools" - if symbols.symbol_table_is_64bit(context, symbol_table): + if symbols.symbol_table_is_64bit(context, ntkrnlmp.symbol_table_name): big_pools_json_filename += "-x64" else: big_pools_json_filename += "-x86" @@ -100,16 +99,17 @@ class BigPools(interfaces.plugins.PluginInterface): new_table_name = intermed.IntermediateSymbolTable.create( context=context, config_path=configuration.path_join( - context.symbol_space[symbol_table].config_path, "bigpools" + context.symbol_space[ntkrnlmp.symbol_table_name].config_path, + "bigpools", ), sub_path=os.path.join("windows", "bigpools"), filename=big_pools_json_filename, - table_mapping={"nt_symbols": symbol_table}, + table_mapping={"nt_symbols": ntkrnlmp.symbol_table_name}, class_types={ "_POOL_TRACKER_BIG_PAGES": extensions.pool.POOL_TRACKER_BIG_PAGES }, ) - module = context.module(new_table_name, layer_name, offset=0) + module = context.module(new_table_name, ntkrnlmp.layer_name, offset=0) big_page_table_type = module.get_type("_POOL_TRACKER_BIG_PAGES") big_pools = ntkrnlmp.object( @@ -132,12 +132,10 @@ class BigPools(interfaces.plugins.PluginInterface): tags = [tag for tag in self.config["tags"].split(",")] else: tags = None - kernel = self.context.modules[self.config["kernel"]] for big_pool in self.list_big_pools( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], tags=tags, show_free=self.config.get("show-free"), ): diff --git a/volatility3/framework/plugins/windows/cachedump.py b/volatility3/framework/plugins/windows/cachedump.py index 6e667984a..3c474bc2c 100644 --- a/volatility3/framework/plugins/windows/cachedump.py +++ b/volatility3/framework/plugins/windows/cachedump.py @@ -1,184 +1,21 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from struct import unpack -from typing import Tuple -from Crypto.Cipher import ARC4, AES -from Crypto.Hash import HMAC - -from volatility3.framework import interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import registry -from volatility3.framework.symbols.windows import versions -from volatility3.plugins.windows import hashdump, lsadump -from volatility3.plugins.windows.registry import hivelist +from volatility3.framework import deprecation, interfaces +from volatility3.plugins.windows.registry import cachedump vollog = logging.getLogger(__name__) -class Cachedump(interfaces.plugins.PluginInterface): - """Dumps lsa secrets from memory""" +class Cachedump( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=cachedump.Cachedump, + removal_date="2026-09-25", +): + """Dumps lsa secrets from memory (deprecated)""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) - ), - requirements.PluginRequirement( - name="lsadump", plugin=lsadump.Lsadump, version=(1, 0, 0) - ), - requirements.PluginRequirement( - name="hashdump", plugin=hashdump.Hashdump, version=(1, 1, 0) - ), - ] - - @staticmethod - def get_nlkm( - sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool - ): - return lsadump.Lsadump.get_secret_by_name( - sechive, "NL$KM", lsakey, is_vista_or_later - ) - - @staticmethod - def decrypt_hash(edata: bytes, nlkm: bytes, ch, xp: bool): - if xp: - hmac_md5 = HMAC.new(nlkm, ch) - rc4key = hmac_md5.digest() - rc4 = ARC4.new(rc4key) - data = rc4.encrypt(edata) # lgtm [py/weak-cryptographic-algorithm] - else: - # Based on code from http://lab.mediaservice.net/code/cachedump.rb - aes = AES.new(nlkm[16:32], AES.MODE_CBC, ch) - data = b"" - for i in range(0, len(edata), 16): - buf = edata[i : i + 16] - if len(buf) < 16: - buf += (16 - len(buf)) * b"\00" - data += aes.decrypt(buf) - return data - - @staticmethod - def parse_cache_entry(cache_data: bytes) -> Tuple[int, int, int, bytes, bytes]: - (uname_len, domain_len) = unpack(" Tuple[str, str, str, bytes]: - """Get the data from the cache and separate it into the username, domain name, and hash data""" - uname_offset = 72 - pad = 2 * ((uname_len / 2) % 2) - domain_offset = int(uname_offset + uname_len + pad) - pad = 2 * ((domain_len / 2) % 2) - domain_name_offset = int(domain_offset + domain_len + pad) - hashh = dec_data[:0x10] - username = dec_data[uname_offset : uname_offset + uname_len].decode( - "utf-16-le", "replace" - ) - domain = dec_data[domain_offset : domain_offset + domain_len].decode( - "utf-16-le", "replace" - ) - domain_name = dec_data[ - domain_name_offset : domain_name_offset + domain_name_len - ].decode("utf-16-le", "replace") - - return (username, domain, domain_name, hashh) - - def _generator(self, syshive, sechive): - if not syshive or not sechive: - if syshive is None: - vollog.warning("Unable to locate SYSTEM hive") - if sechive is None: - vollog.warning("Unable to locate SECURITY hive") - return None - - bootkey = hashdump.Hashdump.get_bootkey(syshive) - if not bootkey: - vollog.warning("Unable to find bootkey") - return None - - kernel = self.context.modules[self.config["kernel"]] - - vista_or_later = versions.is_vista_or_later( - context=self.context, symbol_table=kernel.symbol_table_name - ) - - lsakey = lsadump.Lsadump.get_lsa_key(sechive, bootkey, vista_or_later) - if not lsakey: - vollog.warning("Unable to find lsa key") - return None - - nlkm = self.get_nlkm(sechive, lsakey, vista_or_later) - if not nlkm: - vollog.warning("Unable to find nlkma key") - return None - - cache = hashdump.Hashdump.get_hive_key(sechive, "Cache") - if not cache: - vollog.warning("Unable to find cache key") - return None - - for cache_item in cache.get_values(): - if cache_item.Name == "NL$Control": - continue - - data = sechive.read(cache_item.Data + 4, cache_item.DataLength) - if data is None: - continue - ( - uname_len, - domain_len, - domain_name_len, - enc_data, - ch, - ) = self.parse_cache_entry(data) - # Skip if nothing in this cache entry - if uname_len == 0 or len(ch) == 0: - continue - dec_data = self.decrypt_hash(enc_data, nlkm, ch, not vista_or_later) - - (username, domain, domain_name, hashh) = self.parse_decrypted_cache( - dec_data, uname_len, domain_len, domain_name_len - ) - yield (0, (username, domain, domain_name, hashh)) - - def run(self): - offset = self.config.get("offset", None) - - syshive = sechive = None - kernel = self.context.modules[self.config["kernel"]] - - for hive in hivelist.HiveList.list_hives( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - hive_offsets=None if offset is None else [offset], - ): - if hive.get_name().split("\\")[-1].upper() == "SYSTEM": - syshive = hive - if hive.get_name().split("\\")[-1].upper() == "SECURITY": - sechive = hive - - return renderers.TreeGrid( - [("Username", str), ("Domain", str), ("Domain name", str), ("Hash", bytes)], - self._generator(syshive, sechive), - ) + _version = (1, 0, 2) diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index 562846def..2a65f76bb 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -28,7 +28,7 @@ class Callbacks(interfaces.plugins.PluginInterface): """Lists kernel callbacks and notification routines.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (3, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -38,17 +38,17 @@ class Callbacks(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="driverirp", plugin=driverirp.DriverIrp, version=(1, 0, 0) + requirements.VersionRequirement( + name="driverirp", component=driverirp.DriverIrp, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="handles", plugin=handles.Handles, version=(1, 0, 0) + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(4, 0, 0) ), ] @@ -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 @@ -187,7 +186,9 @@ class Callbacks(interfaces.plugins.PluginInterface): The name of the constructed symbol table """ native_types = context.symbol_space[nt_symbol_table].natives - is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=nt_symbol_table + ) table_mapping = {"nt_symbols": nt_symbol_table} if is_64bit: @@ -209,8 +210,7 @@ class Callbacks(interfaces.plugins.PluginInterface): def scan( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, callback_symbol_table: str, ) -> Iterable[ Tuple[ @@ -223,18 +223,21 @@ class Callbacks(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols + kernel_module_name: Name of the module for the kernel callback_symbol_table: The name of the table containing the callback object symbols (_SHUTDOWN_PACKET etc.) Returns: A list of callback objects found by scanning the `layer_name` layer for callback pool signatures """ + kernel = context.modules[kernel_module_name] + is_vista_or_later = versions.is_vista_or_later( - context=context, symbol_table=nt_symbol_table + context=context, symbol_table=kernel.symbol_table_name ) - type_map = handles.Handles.get_type_map(context, layer_name, nt_symbol_table) + type_map = handles.Handles.get_type_map( + context=context, kernel_module_name=kernel_module_name + ) constraints = cls.create_callback_scan_constraints( context, callback_symbol_table, is_vista_or_later @@ -245,7 +248,7 @@ class Callbacks(interfaces.plugins.PluginInterface): mem_object, _header, ) in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, nt_symbol_table, constraints + context, kernel_module_name, constraints ): try: if isinstance(mem_object, callbacks._SHUTDOWN_PACKET): @@ -345,27 +348,24 @@ class Callbacks(interfaces.plugins.PluginInterface): def list_notify_routines( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[Tuple[str, int, Optional[str]]]: """Lists all kernel notification routines. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module of the kernel callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] is_vista_or_later = versions.is_vista_or_later( - context=context, symbol_table=symbol_table + context=context, symbol_table=ntkrnlmp.symbol_table_name ) full_type_name = callback_table_name + constants.BANG + "_GENERIC_CALLBACK" @@ -410,16 +410,14 @@ class Callbacks(interfaces.plugins.PluginInterface): def _list_registry_callbacks_legacy( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[Tuple[str, int, None]]: """ Lists all registry callbacks from the old format via the CmpCallBackVector. """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] full_type_name = ( callback_table_name + constants.BANG + "_EX_CALLBACK_ROUTINE_BLOCK" ) @@ -457,16 +455,13 @@ class Callbacks(interfaces.plugins.PluginInterface): def _list_registry_callbacks_new( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[Tuple[str, int, Optional[str]]]: """ Lists all registry callbacks via the CallbackListHead. """ - - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] full_type_name = callback_table_name + constants.BANG + "_CM_CALLBACK_ENTRY" symbol_offset = ntkrnlmp.get_symbol("CallbackListHead").address @@ -490,36 +485,33 @@ class Callbacks(interfaces.plugins.PluginInterface): def list_registry_callbacks( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[Tuple[str, int, Optional[str]]]: """Lists all registry callbacks. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module of the kernel callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] if ntkrnlmp.has_symbol("CmpCallBackVector") and ntkrnlmp.has_symbol( "CmpCallBackCount" ): yield from cls._list_registry_callbacks_legacy( - context, layer_name, symbol_table, callback_table_name + context, kernel_module_name, callback_table_name ) elif ntkrnlmp.has_symbol("CallbackListHead") and ntkrnlmp.has_symbol( "CmpCallBackCount" ): yield from cls._list_registry_callbacks_new( - context, layer_name, symbol_table, callback_table_name + context, kernel_module_name, callback_table_name ) else: symbols_to_check = [ @@ -534,14 +526,11 @@ class Callbacks(interfaces.plugins.PluginInterface): symbol_status = "exists" vollog.debug(f"symbol {symbol_name} {symbol_status}.") - return None - @classmethod def list_bugcheck_reason_callbacks( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[ Tuple[ @@ -554,16 +543,14 @@ class Callbacks(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module of the kernel callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] try: list_offset = ntkrnlmp.get_symbol( @@ -577,11 +564,15 @@ class Callbacks(interfaces.plugins.PluginInterface): callback_table_name + constants.BANG + "_KBUGCHECK_REASON_CALLBACK_RECORD" ) callback_record = context.object( - object_type=full_type_name, offset=kvo + list_offset, layer_name=layer_name + object_type=full_type_name, + offset=ntkrnlmp.offset + list_offset, + layer_name=ntkrnlmp.layer_name, ) for callback in callback_record.Entry: - if not context.layers[layer_name].is_valid(callback.CallbackRoutine, 64): + if not context.layers[ntkrnlmp.layer_name].is_valid( + callback.CallbackRoutine, 64 + ): continue try: @@ -598,14 +589,17 @@ 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( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, callback_table_name: str, ) -> Iterable[ Tuple[ @@ -618,16 +612,13 @@ class Callbacks(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module of the kernel callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] try: list_offset = ntkrnlmp.get_symbol("KeBugCheckCallbackListHead").address @@ -639,17 +630,20 @@ class Callbacks(interfaces.plugins.PluginInterface): callback_table_name + constants.BANG + "_KBUGCHECK_CALLBACK_RECORD" ) callback_record = context.object( - full_type_name, offset=kvo + list_offset, layer_name=layer_name + full_type_name, + offset=ntkrnlmp.offset + list_offset, + layer_name=ntkrnlmp.layer_name, ) for callback in callback_record.Entry: - if not context.layers[layer_name].is_valid(callback.CallbackRoutine, 64): + if not context.layers[ntkrnlmp.layer_name].is_valid( + callback.CallbackRoutine, 64 + ): continue try: - component = context.object( - symbol_table + constants.BANG + "string", - layer_name=layer_name, + component = ntkrnlmp.object( + "string", offset=callback.Component, max_length=64, errors="replace", @@ -667,7 +661,8 @@ class Callbacks(interfaces.plugins.PluginInterface): ) collection = ssdt.SSDT.build_module_collection( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], ) callback_methods = ( @@ -681,8 +676,7 @@ class Callbacks(interfaces.plugins.PluginInterface): for callback_method in callback_methods: for callback_type, callback_address, callback_detail in callback_method( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], callback_symbol_table, ): if callback_detail is None: diff --git a/volatility3/framework/plugins/windows/cmdline.py b/volatility3/framework/plugins/windows/cmdline.py index 8cfb5576c..733b06605 100644 --- a/volatility3/framework/plugins/windows/cmdline.py +++ b/volatility3/framework/plugins/windows/cmdline.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List +from typing import List, Optional from volatility3.framework import constants, exceptions, renderers, interfaces from volatility3.framework.configuration import requirements @@ -27,8 +27,8 @@ class CmdLine(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -41,7 +41,7 @@ class CmdLine(interfaces.plugins.PluginInterface): @classmethod def get_cmdline( cls, context: interfaces.context.ContextInterface, kernel_table_name: str, proc - ): + ) -> Optional[str]: """Extracts the cmdline from PEB Args: @@ -54,15 +54,16 @@ class CmdLine(interfaces.plugins.PluginInterface): """ proc_layer_name = proc.add_process_layer() + if not proc_layer_name: + return None peb = context.object( kernel_table_name + constants.BANG + "_PEB", layer_name=proc_layer_name, offset=proc.Peb, ) - result_text = peb.ProcessParameters.CommandLine.get_string() - return result_text + return peb.ProcessParameters.CommandLine.get_string() def _generator(self, procs): kernel = self.context.modules[self.config["kernel"]] @@ -70,6 +71,7 @@ class CmdLine(interfaces.plugins.PluginInterface): for proc in procs: process_name = utility.array_to_string(proc.ImageFileName) proc_id = "Unknown" + result_text = None try: proc_id = proc.UniqueProcessId @@ -78,20 +80,26 @@ class CmdLine(interfaces.plugins.PluginInterface): ) except exceptions.SwappedInvalidAddressException as exp: - result_text = f"Required memory at {exp.invalid_address:#x} is inaccessible (swapped)" + vollog.debug( + f"Required memory at {exp.invalid_address:#x} is inaccessible (swapped)" + ) except exceptions.PagedInvalidAddressException as exp: - result_text = f"Required memory at {exp.invalid_address:#x} is not valid (process exited?)" + vollog.debug( + f"Required memory at {exp.invalid_address:#x} is not valid (process exited?)" + ) except exceptions.InvalidAddressException as exp: - result_text = "Process {}: Required memory at {:#x} is not valid (incomplete layer {}?)".format( - proc_id, exp.invalid_address, exp.layer_name + vollog.debug( + f"Process {proc_id}: Required memory at {exp.invalid_address:#x} is not valid (incomplete layer {exp.layer_name}?)" ) + if not result_text: + result_text = renderers.UnreadableValue() + yield (0, (proc.UniqueProcessId, process_name, result_text)) def run(self): - kernel = self.context.modules[self.config["kernel"]] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) return renderers.TreeGrid( @@ -99,8 +107,7 @@ class CmdLine(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 9645ee507..676050b65 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -24,7 +24,7 @@ class CmdScan(interfaces.plugins.PluginInterface): """Looks for Windows Command History lists""" _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -36,10 +36,15 @@ class CmdScan(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="consoles", plugin=consoles.Consoles, version=(1, 0, 0) + requirements.VersionRequirement( + name="consoles", component=consoles.Consoles, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), requirements.BooleanRequirement( name="no_registry", @@ -67,6 +72,7 @@ class CmdScan(interfaces.plugins.PluginInterface): Args: conhost_proc: the process object for conhost.exe + size_filter: size above which vads will not be returned Returns: A list of tuples of: @@ -82,9 +88,8 @@ class CmdScan(interfaces.plugins.PluginInterface): def get_command_history( cls, context: interfaces.context.ContextInterface, - kernel_layer_name: str, - kernel_symbol_table_name: str, config_path: str, + kernel_module_name: str, procs: Generator[interfaces.objects.ObjectInterface, None, None], max_history: Set[int], ) -> Tuple[ @@ -96,11 +101,9 @@ class CmdScan(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_layer_name: The name of the layer on which to operate - kernel_symbol_table_name: The name of the table containing the kernel symbols config_path: The config path where to find symbol files - procs: list of process objects - max_history: an initial set of CommandHistorySize values + procs: List of process objects + max_history: An initial set of CommandHistorySize values Returns: The conhost process object, the command history structure, a dictionary of properties for @@ -134,9 +137,8 @@ class CmdScan(interfaces.plugins.PluginInterface): if conhost_symbol_table is None: conhost_symbol_table = consoles.Consoles.create_conhost_symbol_table( context, - kernel_layer_name, - kernel_symbol_table_name, config_path, + kernel_module_name, proc_layer_name, conhostexe_base, ) @@ -227,7 +229,6 @@ class CmdScan(interfaces.plugins.PluginInterface): "data": command_history.CommandCountMax, } ) - command_history_properties.append( { "level": 1, @@ -236,6 +237,7 @@ class CmdScan(interfaces.plugins.PluginInterface): "data": "", } ) + for ( cmd_index, bucket_cmd, @@ -278,19 +280,16 @@ class CmdScan(interfaces.plugins.PluginInterface): procs: the process list filtered to conhost.exe instances """ - kernel = self.context.modules[self.config["kernel"]] - max_history = set(self.config.get("max_history", [50])) no_registry = self.config.get("no_registry") if no_registry is False: max_history, _ = consoles.Consoles.get_console_settings_from_registry( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - max_history, - [], + context=self.context, + config_path=self.config_path, + kernel_module_name=self.config["kernel"], + max_history=max_history, + max_buffers=[], ) vollog.debug(f"Possible CommandHistorySize values: {max_history}") @@ -302,9 +301,8 @@ class CmdScan(interfaces.plugins.PluginInterface): command_history_properties, ) in self.get_command_history( self.context, - kernel.layer_name, - kernel.symbol_table_name, self.config_path, + self.config["kernel"], procs, max_history, ): @@ -352,15 +350,13 @@ class CmdScan(interfaces.plugins.PluginInterface): def _conhost_proc_filter(self, proc: interfaces.objects.ObjectInterface): """ - Used to filter to only conhost.exe processes + Used to filter only conhost.exe processes """ process_name = utility.array_to_string(proc.ImageFileName) return process_name != "conhost.exe" def run(self): - kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid( [ ("PID", int), @@ -373,8 +369,7 @@ class CmdScan(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=self._conhost_proc_filter, ) ), diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index ad1c9d4bd..efc03ad1b 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -7,7 +7,7 @@ import logging import os import struct -from typing import Tuple, Generator, Set, Dict, Any, Type +from typing import Tuple, Optional, Generator, Set, Dict, Any, Type, List from volatility3.framework import interfaces, symbols, exceptions from volatility3.framework import renderers @@ -29,7 +29,9 @@ class Consoles(interfaces.plugins.PluginInterface): """Looks for Windows console buffers""" _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + + # 2.0.0 - change the signature of `get_console_settings_from_registry` + _version = (3, 0, 0) @classmethod def get_requirements(cls): @@ -41,13 +43,21 @@ class Consoles(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="info", component=info.Info, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), requirements.BooleanRequirement( name="no_registry", @@ -74,7 +84,7 @@ class Consoles(interfaces.plugins.PluginInterface): @classmethod def find_conhost_proc( cls, proc_list: Generator[interfaces.objects.ObjectInterface, None, None] - ) -> Tuple[interfaces.context.ContextInterface, str]: + ) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]: """ Walks the process list and returns the conhost instances. @@ -87,6 +97,7 @@ class Consoles(interfaces.plugins.PluginInterface): for proc in proc_list: if utility.array_to_string(proc.ImageFileName).lower() == "conhost.exe": + proc_id = "Unknown" try: proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() @@ -95,15 +106,13 @@ class Consoles(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) @classmethod def find_conhostexe( - cls, conhost_proc: interfaces.context.ContextInterface - ) -> Tuple[int, int]: + cls, conhost_proc: interfaces.objects.ObjectInterface + ) -> Tuple[Optional[int], Optional[int]]: """ Finds the base address of conhost.exe @@ -127,20 +136,18 @@ class Consoles(interfaces.plugins.PluginInterface): def determine_conhost_version( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, config_path: str, + kernel_module_name: str, conhost_layer_name: str, conhost_base: int, - ) -> Tuple[str, Type]: + ) -> Tuple[Optional[str], Dict[str, Type]]: """Tries to determine which symbol filename to use for the image's console information. This is similar to the netstat plugin. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols config_path: The config path where to find symbol files + kernel_module_name: The name of the module for the kernel conhost_layer_name: The name of the conhot process memory layer conhost_base: the base address of conhost.exe @@ -148,16 +155,20 @@ class Consoles(interfaces.plugins.PluginInterface): The filename of the symbol table to use and the associated class types. """ - is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) + kernel = context.modules[kernel_module_name] + + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) if is_64bit: arch = "x64" else: arch = "x86" - vers = info.Info.get_version_structure(context, layer_name, nt_symbol_table) + vers = info.Info.get_version_structure(context, kernel_module_name) - kuser = info.Info.get_kuser_structure(context, layer_name, nt_symbol_table) + kuser = info.Info.get_kuser_structure(context, kernel_module_name) try: vers_minor_version = int(vers.MinorVersion) @@ -176,12 +187,7 @@ class Consoles(interfaces.plugins.PluginInterface): ) vollog.debug( - "Determined OS Version: {}.{} {}.{}".format( - kuser.NtMajorVersion, - kuser.NtMinorVersion, - vers.MajorVersion, - vers.MinorVersion, - ) + f"Determined OS Version: {kuser.NtMajorVersion}.{kuser.NtMinorVersion} {vers.MajorVersion}.{vers.MinorVersion}" ) if nt_major_version == 10 and arch == "x64": @@ -249,7 +255,7 @@ class Consoles(interfaces.plugins.PluginInterface): ) except (exceptions.InvalidAddressException, TypeError, AttributeError): # the following is IntelLayer specific and might need to be adapted to other architectures. - physical_layer_name = context.layers[layer_name].config.get( + physical_layer_name = context.layers[kernel.layer_name].config.get( "memory_layer", None ) if physical_layer_name: @@ -260,9 +266,7 @@ class Consoles(interfaces.plugins.PluginInterface): if ver: conhost_mod_version = ver[3] vollog.debug( - "Determined conhost.exe's FileVersion: {}".format( - conhost_mod_version - ) + f"Determined conhost.exe's FileVersion: {conhost_mod_version}" ) else: vollog.debug("Could not determine conhost.exe's FileVersion.") @@ -311,12 +315,7 @@ class Consoles(interfaces.plugins.PluginInterface): else: raise NotImplementedError( - "This version of Windows is not supported: {}.{} {}.{}!".format( - nt_major_version, - nt_minor_version, - vers.MajorVersion, - vers_minor_version, - ) + f"This version of Windows is not supported: {nt_major_version}.{nt_minor_version} {vers.MajorVersion}.{vers_minor_version}!" ) vollog.debug(f"Determined symbol filename: {filename}") @@ -327,9 +326,8 @@ class Consoles(interfaces.plugins.PluginInterface): def create_conhost_symbol_table( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, config_path: str, + kernel_module_name: str, conhost_layer_name: str, conhost_base: int, ) -> str: @@ -337,24 +335,29 @@ class Consoles(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols config_path: The config path where to find symbol files + kernel_module_name: The name of the module of the kernel Returns: The name of the constructed symbol table """ - table_mapping = {"nt_symbols": nt_symbol_table} + kernel = context.modules[kernel_module_name] + + table_mapping = {"nt_symbols": kernel.symbol_table_name} symbol_filename, class_types = cls.determine_conhost_version( context, - layer_name, - nt_symbol_table, config_path, + kernel_module_name, conhost_layer_name, conhost_base, ) + if symbol_filename is None: + raise ValueError( + "Symbol filename could not be determined for conhost version" + ) + vollog.debug(f"Using symbol file '{symbol_filename}' and types {class_types}") return intermed.IntermediateSymbolTable.create( @@ -370,24 +373,26 @@ class Consoles(interfaces.plugins.PluginInterface): def get_console_info( cls, context: interfaces.context.ContextInterface, - kernel_layer_name: str, - kernel_table_name: str, config_path: str, + kernel_module_name: str, procs: Generator[interfaces.objects.ObjectInterface, None, None], max_history: Set[int], max_buffers: Set[int], - ) -> Tuple[ - interfaces.context.ContextInterface, - interfaces.context.ContextInterface, - Dict[str, Any], + ) -> Generator[ + Tuple[ + interfaces.objects.ObjectInterface, + Optional[interfaces.objects.ObjectInterface], + List[Any], + ], + None, + None, ]: """Gets the Console Information structure and its related properties for each conhost process Args: context: The context to retrieve required elements (layers, symbol tables) from - kernel_layer_name: The name of the layer on which to operate - kernel_table_name: The name of the table containing the kernel symbols config_path: The config path where to find symbol files + kernel_module_name: The name of the module for the kernel procs: list of process objects max_history: an initial set of CommandHistorySize values max_buffers: an initial list of HistoryBufferMax values @@ -415,6 +420,11 @@ class Consoles(interfaces.plugins.PluginInterface): "Unable to find the location of conhost.exe. Analysis cannot proceed." ) continue + if conhostexe_size is None: + vollog.info( + "Unable to determine the size of conhost.exe. Analysis cannot proceed." + ) + continue vollog.debug(f"Found conhost.exe base at {conhostexe_base:#x}") proc_layer = context.layers[proc_layer_name] @@ -422,9 +432,8 @@ class Consoles(interfaces.plugins.PluginInterface): if conhost_symbol_table is None: conhost_symbol_table = cls.create_conhost_symbol_table( context, - kernel_layer_name, - kernel_table_name, config_path, + kernel_module_name, proc_layer_name, conhostexe_base, ) @@ -434,6 +443,7 @@ class Consoles(interfaces.plugins.PluginInterface): ) found_console_info_for_proc = False + console_info = None # scan for potential _CONSOLE_INFORMATION structures by using the CommandHistorySize for max_history_value in max_history: max_history_bytes = struct.pack("H", max_history_value) @@ -445,7 +455,7 @@ class Consoles(interfaces.plugins.PluginInterface): scanners.BytesScanner(max_history_bytes), sections=[(conhostexe_base, conhostexe_size)], ): - + console_info = None console_properties = [] try: @@ -793,8 +803,7 @@ class Consoles(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, config_path: str, - kernel_layer_name: str, - kernel_symbol_table_name: str, + kernel_module_name: str, max_history: Set[int], max_buffers: Set[int], ) -> Tuple[Set[int], Set[int]]: @@ -805,8 +814,7 @@ class Consoles(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from config_path: The config path where to find symbol files - kernel_layer_name: The name of the layer on which to operate - kernel_symbol_table_name: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel max_history: an initial set of CommandHistorySize values max_buffers: an initial list of HistoryBufferMax values @@ -823,8 +831,7 @@ class Consoles(interfaces.plugins.PluginInterface): for hive in hivelist.HiveList.list_hives( context=context, base_config_path=config_path, - layer_name=kernel_layer_name, - symbol_table=kernel_symbol_table_name, + kernel_module_name=kernel_module_name, hive_offsets=None, ): try: @@ -849,8 +856,6 @@ class Consoles(interfaces.plugins.PluginInterface): procs: the process list filtered to conhost.exe instances """ - kernel = self.context.modules[self.config["kernel"]] - max_history = set(self.config.get("max_history", [50])) max_buffers = set(self.config.get("max_buffers", [4])) no_registry = self.config.get("no_registry") @@ -859,8 +864,7 @@ class Consoles(interfaces.plugins.PluginInterface): max_history, max_buffers = self.get_console_settings_from_registry( self.context, self.config_path, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], max_history, max_buffers, ) @@ -871,9 +875,8 @@ class Consoles(interfaces.plugins.PluginInterface): proc = None for proc, console_info, console_properties in self.get_console_info( self.context, - kernel.layer_name, - kernel.symbol_table_name, self.config_path, + self.config["kernel"], procs, max_history, max_buffers, @@ -931,8 +934,6 @@ class Consoles(interfaces.plugins.PluginInterface): return process_name.lower() != "conhost.exe" def run(self): - kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid( [ ("PID", int), @@ -945,8 +946,7 @@ class Consoles(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=self._conhost_proc_filter, ) ), diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 57dd1822c..74434a3cc 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -24,7 +24,7 @@ vollog = logging.getLogger(__name__) class DebugRegisters(interfaces.plugins.PluginInterface): # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List: @@ -35,10 +35,13 @@ class DebugRegisters(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0) + name="threads", component=threads.Threads, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), ] @@ -108,20 +111,18 @@ class DebugRegisters(interfaces.plugins.PluginInterface): None, None, ]: - kernel = self.context.modules[self.config["kernel"]] - vads_cache: Dict[int, pe_symbols.ranges_type] = {} proc_modules = None procs = pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + context=self.context, kernel_module_name=self.config["kernel"] ) for proc in procs: - for thread in threads.Threads.list_threads(kernel, proc): + for thread in threads.Threads.list_threads( + self.context, self.config["kernel"], proc + ): debug_info = self._get_debug_info(thread) if not debug_info: continue @@ -137,7 +138,7 @@ class DebugRegisters(interfaces.plugins.PluginInterface): # this lookup takes a while, so only perform if we need to if not proc_modules: proc_modules = pe_symbols.PESymbols.get_process_modules( - self.context, kernel.layer_name, kernel.symbol_table_name, None + self.context, self.config["kernel"], None ) path_and_symbol = partial( pe_symbols.PESymbols.path_and_symbol_for_address, diff --git a/volatility3/framework/plugins/windows/deskscan.py b/volatility3/framework/plugins/windows/deskscan.py new file mode 100644 index 000000000..202eaf330 --- /dev/null +++ b/volatility3/framework/plugins/windows/deskscan.py @@ -0,0 +1,87 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Iterable, Tuple + +from volatility3.framework import interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import desktops, windowstations + +vollog = logging.getLogger(__name__) + + +class DeskScan(desktops.Desktops): + """Scans for the Desktop instances of each Window Station""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.implementation = self.scan_desktops + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="desktops", component=desktops.Desktops, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="windowstations", + component=windowstations.WindowStations, + version=(1, 0, 0), + ), + ] + + @classmethod + def scan_desktops( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Iterable[Tuple[int, str, int, str, str, int]]: + """ + Yields the information about each desktop and desktop thread needed for analysis + + The tuple yielded includes the: + Virtual address of the desktop + The window station name + The session id + Desktop name + Process name + Process ID (PID) + """ + kernel = context.modules[kernel_module_name] + + for desktop in windowstations.WindowStations.scan_gui_object( + context, config_path, kernel_module_name, b"Desk", "tagDESKTOP" + ): + desktop_name = desktop.get_name(kernel.symbol_table_name) + if not desktop_name: + continue + + winsta = desktop.get_window_station() + if not winsta: + continue + + winsta_name, session_id = winsta.get_info(kernel.symbol_table_name) + if not winsta_name or session_id is None: + 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, + ) diff --git a/volatility3/framework/plugins/windows/desktops.py b/volatility3/framework/plugins/windows/desktops.py new file mode 100644 index 000000000..f2a985c6b --- /dev/null +++ b/volatility3/framework/plugins/windows/desktops.py @@ -0,0 +1,96 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Iterable + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import windowstations + +vollog = logging.getLogger(__name__) + + +class Desktops(interfaces.plugins.PluginInterface): + """Enumerates the Desktop instances of each Window Station""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.implementation = self.list_desktops + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="windowstations", + component=windowstations.WindowStations, + version=(1, 0, 0), + ), + ] + + @classmethod + def list_desktops( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """ + Uses `scan_window_stations` to find each window station + For each found, enumerates its desktops followed by the + threads of each desktop. + """ + kernel = context.modules[kernel_module_name] + + for ( + winsta, + station_name, + session_id, + ) in windowstations.WindowStations.scan_window_stations( + context, config_path, kernel_module_name + ): + # for each window station, walk its list of desktops + 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, + ) + + def _generator(self): + kernel_name = self.config["kernel"] + + # call the implementation for finding desktops + # yield the information, which will include the owning window station and process + for desktop_info in self.implementation( + self.context, self.config_path, kernel_name + ): + yield 0, desktop_info + + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Window Station", str), + ("Session", int), + ("Desktop", str), + ("Process", str), + ("PID", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/devicetree.py b/volatility3/framework/plugins/windows/devicetree.py index 6f39799c1..17ec1c451 100644 --- a/volatility3/framework/plugins/windows/devicetree.py +++ b/volatility3/framework/plugins/windows/devicetree.py @@ -89,17 +89,16 @@ class DeviceTree(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) + requirements.VersionRequirement( + name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) ), ] def _generator(self) -> Iterator[Tuple]: - kernel = self.context.modules[self.config["kernel"]] - # Scan the Layer for drivers for driver in driverscan.DriverScan.scan_drivers( - self.context, kernel.layer_name, kernel.symbol_table_name + self.context, + self.config["kernel"], ): try: try: diff --git a/volatility3/framework/plugins/windows/direct_system_calls.py b/volatility3/framework/plugins/windows/direct_system_calls.py new file mode 100644 index 000000000..79d02fe67 --- /dev/null +++ b/volatility3/framework/plugins/windows/direct_system_calls.py @@ -0,0 +1,63 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from volatility3.framework import interfaces, deprecation +from collections import namedtuple +from volatility3.plugins.windows.malware import direct_system_calls + +vollog = logging.getLogger(__name__) + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +syscall_finder_type = namedtuple( + "syscall_finder_type", + [ + "get_syscall_target_address", + "wants_syscall_inst", + "rule_str", + "invalid_ops", + "termination_ops", + ], +) + + +class DirectSystemCalls( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=direct_system_calls.DirectSystemCalls, + removal_date="2026-06-07", +): + """Detects the Direct System Call technique used to bypass EDRs (deprecated).""" + + _required_framework_version = (2, 4, 0) + + # 2.0.0 - changes signature of `get_tasks_to_scan` + _version = (2, 0, 0) + + # DLLs that are expected to host system call invocations + valid_syscall_handlers = ("ntdll.dll", "win32u.dll") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.syscall_finder = syscall_finder_type( + # for direct system calls, we find the `syscall` instruction directly, so we already know the address + None, + # yes, we want the syscall instruction present as it is what this technique looks for + True, + # regex to find "\x0f\x05" (syscall) followed later by "\xc3" (ret) + # we allow spacing in between to break naive anti-analysis forms (e.g., TarTarus Gate) + # Standard techniques, such as HellsGate, look like: + # mov r10, rcx + # mov eax, + # syscall + # ret + "/\\x0f\\x05[^\\xc3]{,24}\\xc3/", + # any of these will not be in a workable, malicious direct system call block + ["jmp", "call", "leave", "int3"], + # the expected form is to end with a "ret" back to the calling code + ["ret"], + ) diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index 5a1b37fcf..cb9d3b8bf 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -5,24 +5,24 @@ import contextlib import datetime import logging import re -from typing import List, Optional, Type +from typing import List -from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins import timeliner -from volatility3.plugins.windows import info, pslist, psscan, pedump +from volatility3.plugins.windows import info, pedump, pslist, psscan vollog = logging.getLogger(__name__) class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): - """Lists the loaded modules in a particular windows memory image.""" + """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]: @@ -34,13 +34,21 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="psscan", component=psscan.PsScan, version=(1, 1, 0) + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.VersionRequirement( - name="info", component=info.Info, version=(1, 0, 0) + name="psscan", component=psscan.PsScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pedump", component=pedump.PEDump, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -53,16 +61,16 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): description="Process offset in the physical address space", optional=True, ), - requirements.StringRequirement( - name="name", - description="Specify a regular expression to match dll name(s)", - optional=True, - ), requirements.IntRequirement( name="base", description="Specify a base virtual address in process memory", optional=True, ), + requirements.StringRequirement( + name="name", + description="Specify a regular expression to match dll name(s)", + optional=True, + ), requirements.BooleanRequirement( name="ignore-case", description="Specify case insensitivity for the regular expression name matching", @@ -75,9 +83,6 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): default=False, optional=True, ), - requirements.VersionRequirement( - name="pedump", component=pedump.PEDump, version=(1, 0, 0) - ), ] def _generator(self, procs): @@ -85,17 +90,16 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): self.context, self.config_path, "windows", "pe", class_types=pe.class_types ) - kernel = self.context.modules[self.config["kernel"]] + kuser = info.Info.get_kuser_structure(self.context, self.config["kernel"]) - kuser = info.Info.get_kuser_structure( - self.context, kernel.layer_name, kernel.symbol_table_name - ) nt_major_version = int(kuser.NtMajorVersion) nt_minor_version = int(kuser.NtMinorVersion) + # LoadTime only applies to versions higher or equal to Window 7 (6.1 and higher) dll_load_time_field = (nt_major_version > 6) or ( nt_major_version == 6 and nt_minor_version >= 1 ) + for proc in procs: proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() @@ -135,7 +139,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if dll_load_time_field: # Versions prior to 6.1 won't have the LoadTime attribute - # and 32bit version shouldn't have the Quadpart according to MSDN + # and 32-bit version shouldn't have the Quadpart according to MSDN try: DllLoadTime = conversion.wintime_to_datetime( entry.LoadTime.QuadPart @@ -169,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, ( @@ -182,33 +190,22 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): size_of_image, BaseDllName, FullDllName, + LoadCount, DllLoadTime, file_output, ), ) def generate_timeline(self): - kernel = self.context.modules[self.config["kernel"]] for row in self._generator( pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + context=self.context, kernel_module_name=self.config["kernel"] ) ): _depth, row_data = row if not isinstance(row_data[6], datetime.datetime): continue - description = ( - "DLL Load: Process {} {} Loaded {} ({}) Size {} Offset {}".format( - row_data[0], - row_data[1], - row_data[4], - row_data[5], - row_data[3], - row_data[2], - ) - ) + description = f"DLL Load: Process {row_data[0]} {row_data[1]} Loaded {row_data[4]} ({row_data[5]}) Size {row_data[3]} Offset {row_data[2]}" yield (description, timeliner.TimeLinerType.CREATED, row_data[6]) def run(self): @@ -218,8 +215,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if self.config["offset"]: procs = psscan.PsScan.scan_processes( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], filter_func=psscan.PsScan.create_offset_filter( self.context, kernel.layer_name, @@ -229,8 +225,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: procs = pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) @@ -242,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/driverirp.py b/volatility3/framework/plugins/windows/driverirp.py index c3eb7c5c1..d5452fa9c 100644 --- a/volatility3/framework/plugins/windows/driverirp.py +++ b/volatility3/framework/plugins/windows/driverirp.py @@ -2,11 +2,15 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging + from volatility3.framework import constants from volatility3.framework import renderers, exceptions, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import ssdt, driverscan +from volatility3.plugins.windows import ssdt, driverscan, modules + +vollog = logging.getLogger(__name__) MAJOR_FUNCTIONS = [ "IRP_MJ_CREATE", @@ -54,32 +58,52 @@ class DriverIrp(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) + requirements.VersionRequirement( + name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) ), ] def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - collection = ssdt.SSDT.build_module_collection( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], + ) + + kernel_space_start = modules.Modules.get_kernel_space_start( + context=self.context, + module_name=self.config["kernel"], ) for driver in driverscan.DriverScan.scan_drivers( - self.context, kernel.layer_name, kernel.symbol_table_name + self.context, + self.config["kernel"], ): try: driver_name = driver.get_driver_name() except (ValueError, exceptions.InvalidAddressException): driver_name = renderers.NotApplicableValue() - for i, address in enumerate(driver.MajorFunction): + for i in range(len(driver.MajorFunction)): + try: + irp_handler = driver.MajorFunction[i] + except exceptions.InvalidAddressException: + vollog.debug( + f"Failed to get IRP handler entry at index {i} for driver at {driver.vol.offset:#x}" + ) + continue + + # smear + if irp_handler < kernel_space_start: + continue + module_symbols = collection.get_module_symbols_by_absolute_location( - address + irp_handler ) module_found = False @@ -96,7 +120,7 @@ class DriverIrp(interfaces.plugins.PluginInterface): format_hints.Hex(driver.vol.offset), driver_name, MAJOR_FUNCTIONS[i], - format_hints.Hex(address), + format_hints.Hex(irp_handler), module_name, symbol.split(constants.BANG)[1], ), @@ -109,7 +133,7 @@ class DriverIrp(interfaces.plugins.PluginInterface): format_hints.Hex(driver.vol.offset), driver_name, MAJOR_FUNCTIONS[i], - format_hints.Hex(address), + format_hints.Hex(irp_handler), module_name, renderers.NotAvailableValue(), ), @@ -122,7 +146,7 @@ class DriverIrp(interfaces.plugins.PluginInterface): format_hints.Hex(driver.vol.offset), driver_name, MAJOR_FUNCTIONS[i], - format_hints.Hex(address), + format_hints.Hex(irp_handler), renderers.NotAvailableValue(), renderers.NotAvailableValue(), ), diff --git a/volatility3/framework/plugins/windows/drivermodule.py b/volatility3/framework/plugins/windows/drivermodule.py index de827602e..bf8f333f6 100644 --- a/volatility3/framework/plugins/windows/drivermodule.py +++ b/volatility3/framework/plugins/windows/drivermodule.py @@ -1,85 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Iterator, List, Tuple -from volatility3.framework import renderers, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import ssdt, driverscan +import logging +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import drivermodule -# built in Windows-components that trigger false positives -KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] +vollog = logging.getLogger(__name__) -class DriverModule(interfaces.plugins.PluginInterface): - """Determines if any loaded drivers were hidden by a rootkit""" +class DriverModule( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=drivermodule.DriverModule, + removal_date="2026-06-07", +): + """Determines if any loaded drivers were hidden by a rootkit (deprecated).""" _required_framework_version = (2, 0, 0) _version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) - ), - requirements.PluginRequirement( - name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) - ), - ] - - def _generator(self) -> Iterator[Tuple]: - """ - Attempt to match each driver's start code address to a known kernel module - A common rootkit technique is to register drivers from modules that are hidden, - which allows us to detect the disconnect between a malicious driver and its hidden module. - """ - kernel = self.context.modules[self.config["kernel"]] - - collection = ssdt.SSDT.build_module_collection( - self.context, kernel.layer_name, kernel.symbol_table_name - ) - - for driver in driverscan.DriverScan.scan_drivers( - self.context, kernel.layer_name, kernel.symbol_table_name - ): - # we do not care about actual symbol names, we just want to know if the driver points to a known module - module_symbols = list( - collection.get_module_symbols_by_absolute_location(driver.DriverStart) - ) - if not module_symbols: - ( - driver_name, - service_key, - name, - ) = driverscan.DriverScan.get_names_for_driver(driver) - - known_exception = driver_name in KNOWN_DRIVERS - - yield ( - 0, - ( - format_hints.Hex(driver.vol.offset), - known_exception, - driver_name, - service_key, - name, - ), - ) - - def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Known Exception", bool), - ("Driver Name", str), - ("Service Key", str), - ("Alternative Name", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/driverscan.py b/volatility3/framework/plugins/windows/driverscan.py index 24d81c3d5..57d365d00 100644 --- a/volatility3/framework/plugins/windows/driverscan.py +++ b/volatility3/framework/plugins/windows/driverscan.py @@ -2,19 +2,19 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Iterable +from typing import Iterable, Optional, Tuple from volatility3.framework import renderers, interfaces, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import poolscanner +from volatility3.plugins.windows import poolscanner, modules class DriverScan(interfaces.plugins.PluginInterface): """Scans for drivers present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -24,8 +24,11 @@ class DriverScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) ), ] @@ -33,8 +36,7 @@ class DriverScan(interfaces.plugins.PluginInterface): def scan_drivers( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for drivers using the poolscanner module and constraints. @@ -47,24 +49,53 @@ class DriverScan(interfaces.plugins.PluginInterface): A list of Driver objects as found from the `layer_name` layer based on Driver pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Dri\xf6", b"Driv"] + kernel.symbol_table_name, [b"Dri\xf6", b"Driv"] + ) + + driver_start_offset = kernel.get_type("_DRIVER_OBJECT").relative_child_offset( + "DriverStart" + ) + + kernel_space_start = modules.Modules.get_kernel_space_start( + context, kernel_module_name ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result - yield mem_object + + scanned_layer = context.layers[mem_object.vol.layer_name] + + # *Many* _DRIVER_OBJECT instances were found at the end of a page + # leading to member access causing backtraces across several plugins + # when members were accessed as the next page was paged out. + # `DriverStart` is the first member from the beginning of the structure + # of interest to plugins, so if it is not accessible then this instance + # is not useful or usable during analysis + # 8 covers this value 32 and 64 bit systems + if scanned_layer.is_valid(mem_object.vol.offset + driver_start_offset, 8): + # Many/most rootkits zero out their DriverStart member for anti-forensics + # so we accept a driver start that is either 0 or is points into kernel memory (the current layer) + if ( + mem_object.DriverStart == 0 + or mem_object.DriverStart > kernel_space_start + ): + yield mem_object @classmethod - def get_names_for_driver(cls, driver): + def get_names_for_driver( + cls, driver + ) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Convenience method for getting the commonly used names associated with a driver Args: - driver: A Eriver object + driver: A Driver object Returns: A tuple of strings of (driver name, service key, driver alt. name) @@ -72,37 +103,40 @@ class DriverScan(interfaces.plugins.PluginInterface): try: driver_name = driver.get_driver_name() except (ValueError, exceptions.InvalidAddressException): - driver_name = renderers.NotApplicableValue() + driver_name = None try: service_key = driver.DriverExtension.ServiceKeyName.String except exceptions.InvalidAddressException: - service_key = renderers.NotApplicableValue() + service_key = None try: name = driver.DriverName.String except exceptions.InvalidAddressException: - name = renderers.NotApplicableValue() + name = None return driver_name, service_key, name def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - for driver in self.scan_drivers( - self.context, kernel.layer_name, kernel.symbol_table_name + self.context, + self.config["kernel"], ): driver_name, service_key, name = self.get_names_for_driver(driver) + # Prior to #1481, this plugin reported dozens to hundreds of junk drivers per sample + if not driver_name and not service_key and not name: + continue + yield ( 0, ( format_hints.Hex(driver.vol.offset), format_hints.Hex(driver.DriverStart), format_hints.Hex(driver.DriverSize), - service_key, - driver_name, - name, + service_key or renderers.NotAvailableValue(), + driver_name or renderers.NotAvailableValue(), + name or renderers.NotAvailableValue(), ), ) diff --git a/volatility3/framework/plugins/windows/dumpfiles.py b/volatility3/framework/plugins/windows/dumpfiles.py index 33d2d0d41..d069bc9f1 100755 --- a/volatility3/framework/plugins/windows/dumpfiles.py +++ b/volatility3/framework/plugins/windows/dumpfiles.py @@ -5,13 +5,12 @@ import logging import ntpath import re -from typing import List, Tuple, Type, Optional, Generator +from typing import Generator, List, Optional, Tuple, Type -from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, UnreadableValue -from volatility3.plugins.windows import handles -from volatility3.plugins.windows import pslist +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import handles, pslist vollog = logging.getLogger(__name__) @@ -44,14 +43,16 @@ class DumpFiles(interfaces.plugins.PluginInterface): description="Process ID to include (all other processes are excluded)", optional=True, ), - requirements.IntRequirement( + requirements.ListRequirement( name="virtaddr", - description="Dump a single _FILE_OBJECT at this virtual address", + element_type=int, + description="Dump the _FILE_OBJECTs at the given virtual address(es)", optional=True, ), - requirements.IntRequirement( + requirements.ListRequirement( name="physaddr", - description="Dump a single _FILE_OBJECT at this physical address", + element_type=int, + description="Dump a single _FILE_OBJECTs at the given physical address(es)", optional=True, ), requirements.StringRequirement( @@ -66,10 +67,10 @@ class DumpFiles(interfaces.plugins.PluginInterface): optional=True, ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="handles", component=handles.Handles, version=(1, 0, 0) + name="handles", component=handles.Handles, version=(4, 0, 0) ), ] @@ -192,13 +193,7 @@ class DumpFiles(interfaces.plugins.PluginInterface): for memory_object, layer, extension in dump_parameters: cache_name = EXTENSION_CACHE_MAP[extension] - desired_file_name = "file.{0:#x}.{1:#x}.{2}.{3}.{4}".format( - file_obj.vol.offset, - memory_object.vol.offset, - cache_name, - ntpath.basename(obj_name), - extension, - ) + desired_file_name = f"file.{file_obj.vol.offset:#x}.{memory_object.vol.offset:#x}.{cache_name}.{ntpath.basename(obj_name)}.{extension}" file_handle = cls.dump_file_producer( file_obj, memory_object, open_method, layer, desired_file_name @@ -230,18 +225,13 @@ class DumpFiles(interfaces.plugins.PluginInterface): # private variables, so we need an instance (for now, anyway). We _could_ call Handles._generator() # to do some of the other work that is duplicated here, but then we'd need to parse the TreeGrid # results instead of just dealing with them as direct objects here. - handles_plugin = handles.Handles( - context=self.context, config_path=self._config_path - ) - type_map = handles_plugin.get_type_map( + type_map = handles.Handles.get_type_map( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], ) - cookie = handles_plugin.find_cookie( + cookie = handles.Handles.find_cookie( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], ) dumped_files = set() @@ -256,7 +246,11 @@ class DumpFiles(interfaces.plugins.PluginInterface): ) continue - for entry in handles_plugin.handles(object_table): + for entry in handles.Handles.handles( + context=self.context, + kernel_module_name=self.config["kernel"], + handle_table=object_table, + ): try: obj_type = entry.get_object_type(type_map, cookie) if obj_type == "File": @@ -264,7 +258,7 @@ class DumpFiles(interfaces.plugins.PluginInterface): if file_re: name = file_obj.file_name_with_device() - if isinstance(name, UnreadableValue): + if isinstance(name, renderers.UnreadableValue): continue if not file_re.search(name): continue @@ -304,7 +298,7 @@ class DumpFiles(interfaces.plugins.PluginInterface): if file_re: name = file_obj.file_name_with_device() - if isinstance(name, UnreadableValue): + if isinstance(name, renderers.UnreadableValue): continue if not file_re.search(name): continue @@ -324,24 +318,26 @@ class DumpFiles(interfaces.plugins.PluginInterface): ) elif offsets: + virtual_layer_name = kernel.layer_name + + # FIXME - change this after standard access to physical layer + physical_layer_name = self.context.layers[virtual_layer_name].config[ + "memory_layer" + ] + # Now process any offsets explicitly requested by the user. for offset, is_virtual in offsets: try: - layer_name = kernel.layer_name - # switch to a memory layer if the user provided --physaddr instead of --virtaddr - if not is_virtual: - layer_name = self.context.layers[layer_name].config[ - "memory_layer" - ] - file_obj = self.context.object( kernel.symbol_table_name + constants.BANG + "_FILE_OBJECT", - layer_name=layer_name, - native_layer_name=kernel.layer_name, + layer_name=( + virtual_layer_name if is_virtual else physical_layer_name + ), + native_layer_name=virtual_layer_name, offset=offset, ) for result in self.process_file_object( - self.context, kernel.layer_name, self.open, file_obj + self.context, virtual_layer_name, self.open, file_obj ): yield (0, result) except exceptions.InvalidAddressException: @@ -354,25 +350,27 @@ class DumpFiles(interfaces.plugins.PluginInterface): offsets = list() # a list of processes matching the pid filter. all files for these process(es) will be dumped. procs = list() - kernel = self.context.modules[self.config["kernel"]] if self.config["filter"] and ( self.config["virtaddr"] or self.config["physaddr"] ): raise ValueError("Cannot use filter flag with an address flag") - if self.config.get("virtaddr", None) is not None: - offsets.append((self.config["virtaddr"], True)) - elif self.config.get("physaddr", None) is not None: - offsets.append((self.config["physaddr"], False)) - else: + if self.config.get("virtaddr"): + for virtaddr in self.config["virtaddr"]: + offsets.append((virtaddr, True)) + + if self.config.get("physaddr"): + for physaddr in self.config["physaddr"]: + offsets.append((physaddr, False)) + + if not offsets: filter_func = pslist.PsList.create_pid_filter( [self.config.get("pid", None)] ) procs = pslist.PsList.list_processes( - self.context, - kernel.layer_name, - kernel.symbol_table_name, + context=self.context, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) diff --git a/volatility3/framework/plugins/windows/envars.py b/volatility3/framework/plugins/windows/envars.py index 66db03c9c..6c07e797f 100644 --- a/volatility3/framework/plugins/windows/envars.py +++ b/volatility3/framework/plugins/windows/envars.py @@ -39,11 +39,11 @@ class Envars(interfaces.plugins.PluginInterface): description="Suppress common and non-persistent variables", optional=True, ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), ] @@ -58,62 +58,71 @@ class Envars(interfaces.plugins.PluginInterface): """ values = [] - kernel = self.context.modules[self.config["kernel"]] for hive in hivelist.HiveList.list_hives( context=self.context, base_config_path=self.config_path, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], hive_offsets=None, ): - sys = False - ntuser = False - ## The global variables + sys = None try: - key = hive.get_key( + sys = hive.get_key( "CurrentControlSet\\Control\\Session Manager\\Environment" ) - sys = True - except KeyError: - with contextlib.suppress(KeyError): - key = hive.get_key( + except ( + KeyError, + registry.RegistryException, + ): + with contextlib.suppress( + KeyError, + registry.RegistryException, + ): + sys = hive.get_key( "ControlSet001\\Control\\Session Manager\\Environment" ) - sys = True if sys: - with contextlib.suppress(KeyError): - for node in key.get_values(): + with contextlib.suppress( + KeyError, + registry.RegistryException, + ): + for node in sys.get_values(): try: value_node_name = node.get_name() if value_node_name: values.append(value_node_name) except ( exceptions.InvalidAddressException, - registry.RegistryFormatException, - ) as excp: + registry.RegistryException, + ): vollog.log( constants.LOGLEVEL_VVV, "Error while parsing global environment variables keys (some keys might be excluded)", ) continue + ntuser = None ## The user-specific variables - with contextlib.suppress(KeyError): - key = hive.get_key("Environment") - ntuser = True + with contextlib.suppress( + KeyError, + registry.RegistryException, + ): + ntuser = hive.get_key("Environment") if ntuser: - with contextlib.suppress(KeyError): - for node in key.get_values(): + with contextlib.suppress( + KeyError, + registry.RegistryException, + ): + for node in ntuser.get_values(): try: value_node_name = node.get_name() if value_node_name: values.append(value_node_name) except ( exceptions.InvalidAddressException, - registry.RegistryFormatException, - ) as excp: + registry.RegistryException, + ): vollog.log( constants.LOGLEVEL_VVV, "Error while parsing user environment variables keys (some keys might be excluded)", @@ -123,7 +132,10 @@ class Envars(interfaces.plugins.PluginInterface): ## The volatile user variables try: key = hive.get_key("Volatile Environment") - except KeyError: + except ( + KeyError, + registry.RegistryException, + ): continue try: for node in key.get_values(): @@ -133,8 +145,8 @@ class Envars(interfaces.plugins.PluginInterface): values.append(value_node_name) except ( exceptions.InvalidAddressException, - registry.RegistryFormatException, - ) as excp: + registry.RegistryException, + ): vollog.log( constants.LOGLEVEL_VVV, "Error while parsing volatile environment variables keys (some keys might be excluded)", @@ -200,15 +212,13 @@ class Envars(interfaces.plugins.PluginInterface): return values def _generator(self, data): - silent_vars = [] - if self.config.get("SILENT", None): - silent_vars = self._get_silent_vars() + silent_vars = self._get_silent_vars() if self.config.get("SILENT") else [] for task in data: for var, val in task.environment_variables(): - if self.config.get("silent", None): - if var in silent_vars: - continue + if var in silent_vars: + continue + yield ( 0, ( @@ -222,7 +232,6 @@ class Envars(interfaces.plugins.PluginInterface): def run(self): filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - kernel = self.context.modules[self.config["kernel"]] return renderers.TreeGrid( [ @@ -235,8 +244,7 @@ class Envars(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py new file mode 100644 index 000000000..476d0eea7 --- /dev/null +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -0,0 +1,133 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + + +# EtwpEventWriteFull -> https://github.com/SolitudePy/Stealthy-ETW-Patch +# CAPA rule -> https://github.com/mandiant/capa-rules/blob/master/anti-analysis/anti-av/patch-event-tracing-for-windows-function.yml +class EtwPatch(interfaces.plugins.PluginInterface): + """Identifies ETW (Event Tracing for Windows) patching techniques used by malware to evade detection. + + This plugin examines the first opcode of key ETW functions in ntdll.dll and advapi32.dll + to detect common ETW bypass techniques such as return pointer manipulation (RET) or function + redirection (JMP). Attackers often patch these functions to prevent security tools from + receiving telemetry about process execution, API calls, and other system events. + """ + + _version = (1, 0, 0) + _required_framework_version = (2, 26, 0) + + etw_functions = { + "ntdll.dll": { + pe_symbols.wanted_names_identifier: [ + "EtwEventWrite", + "EtwEventWriteFull", + "NtTraceEvent", + "ZwTraceEvent", + "NtTraceControl", + "ZwTraceControl", + "EtwpEventWriteFull", + ], + }, + "advapi32.dll": { + pe_symbols.wanted_names_identifier: ["EventWrite", "TraceEvent"], + }, + } + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + def _generator(self): + # Get all ETW function addresses before looping through processes + found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( + context=self.context, + config_path=self.config_path, + kernel_module_name=self.config["kernel"], + symbols=self.etw_functions, + ) + + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ): + try: + proc_id = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + vollog.debug(f"Unable to create process layer for PID {proc_id}") + continue + + # Map of opcodes to their instruction names + opcode_map = { + 0xC3: "RET", + 0xE9: "JMP", + } + + for dll_name, functions in found_symbols.items(): + for func_name, func_addr in functions: + try: + opcode = self.context.layers[proc_layer_name].read( + func_addr, 1 + )[0] + if opcode in opcode_map: + instruction = opcode_map[opcode] + yield ( + 0, + ( + proc_id, + proc_name, + dll_name, + func_name, + format_hints.Hex(func_addr), + f"{opcode:02x} ({instruction})", + ), + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}" + ) + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("DLL", str), + ("Function", str), + ("Offset", format_hints.Hex), + ("Opcode", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/filescan.py b/volatility3/framework/plugins/windows/filescan.py index 82566361d..f417e3e5e 100644 --- a/volatility3/framework/plugins/windows/filescan.py +++ b/volatility3/framework/plugins/windows/filescan.py @@ -14,7 +14,7 @@ class FileScan(interfaces.plugins.PluginInterface): """Scans for file objects present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -24,8 +24,8 @@ class FileScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), ] @@ -33,36 +33,32 @@ class FileScan(interfaces.plugins.PluginInterface): def scan_files( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for file objects using the poolscanner module and constraints. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel Returns: A list of File objects as found from the `layer_name` layer based on File pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Fil\xe5", b"File"] + kernel.symbol_table_name, [b"Fil\xe5", b"File"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - for fileobj in self.scan_files( - self.context, kernel.layer_name, kernel.symbol_table_name - ): + for fileobj in self.scan_files(self.context, self.config["kernel"]): try: file_name = fileobj.FileName.String except exceptions.InvalidAddressException: diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index 9b20ed2d0..c04472eab 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -10,6 +10,7 @@ from typing import List from volatility3.framework import renderers, interfaces, constants, exceptions from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) @@ -26,7 +27,7 @@ def createservicesid(svc) -> str: ## The use of struct here is OK. It doesn't make much sense ## to leverage obj.Object inside this loop. dec.append(struct.unpack(" List[interfaces.configuration.RequirementInterface]: @@ -43,11 +31,11 @@ class Handles(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="psscan", component=psscan.PsScan, version=(1, 1, 0) + name="psscan", component=psscan.PsScan, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -62,157 +50,83 @@ class Handles(interfaces.plugins.PluginInterface): ), ] - def _decode_pointer(self, value, magic): - """Windows encodes pointers to objects and decodes them on the fly - before using them. - - This function mimics the decoding routine so we can generate the - proper pointer values as well. + @classmethod + def _get_item( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + handle_table_entry: interfaces.objects.ObjectInterface, + handle_value: int, + ) -> Optional[interfaces.objects.ObjectInterface]: + """ + Given a handle table entry (_HANDLE_TABLE_ENTRY) structure from a + process' handle table, determine where the corresponding object's + _OBJECT_HEADER can be found, and construct and return the _OBJECT_HEADER """ - value = value & 0xFFFFFFFFFFFFFFF8 - value = value >> magic - # if (value & (1 << 47)): - # value = value | 0xFFFF000000000000 - - return value - - def _get_item(self, handle_table_entry, handle_value): - """Given a handle table entry (_HANDLE_TABLE_ENTRY) structure from a - process' handle table, determine where the corresponding object's - _OBJECT_HEADER can be found.""" - - kernel = self.context.modules[self.config["kernel"]] + kernel = context.modules[kernel_module_name] virtual = kernel.layer_name try: # before windows 7 - if not self.context.layers[virtual].is_valid(handle_table_entry.Object): + if not context.layers[virtual].is_valid(handle_table_entry.Object): return None fast_ref = handle_table_entry.Object.cast("_EX_FAST_REF") - object_header = fast_ref.dereference().cast("_OBJECT_HEADER") + + try: + object_header = fast_ref.dereference().cast("_OBJECT_HEADER") + except exceptions.InvalidAddressException: + return None + object_header.GrantedAccess = handle_table_entry.GrantedAccess except AttributeError: # starting with windows 8 is_64bit = symbols.symbol_table_is_64bit( - self.context, kernel.symbol_table_name + context=context, symbol_table_name=kernel.symbol_table_name ) if is_64bit: - if handle_table_entry.LowValue == 0: + try: + pointer_bits = handle_table_entry.ObjectPointerBits + except exceptions.InvalidAddressException: return None - magic = self.find_sar_value() + if pointer_bits == 0: + return None - # is this the right thing to raise here? - if magic is None: - if has_capstone: - raise AttributeError( - "Unable to find the SAR value for decoding handle table pointers" - ) - else: - raise exceptions.MissingModuleException( - "capstone", - "Requires capstone to find the SAR value for decoding handle table pointers", - ) + offset = pointer_bits << 4 - offset = self._decode_pointer(handle_table_entry.LowValue, magic) else: - if handle_table_entry.InfoTable == 0: + try: + info_table = handle_table_entry.InfoTable + except exceptions.InvalidAddressException: return None - offset = handle_table_entry.InfoTable & ~7 + if info_table == 0: + return None + + offset = info_table & ~7 # print("LowValue: {0:#x} Magic: {1:#x} Offset: {2:#x}".format(handle_table_entry.InfoTable, magic, offset)) - object_header = self.context.object( + object_header = context.object( kernel.symbol_table_name + constants.BANG + "_OBJECT_HEADER", virtual, offset=offset, ) - object_header.GrantedAccess = handle_table_entry.GrantedAccessBits + try: + object_header.GrantedAccess = handle_table_entry.GrantedAccessBits + except exceptions.InvalidAddressException: + return None object_header.HandleValue = handle_value return object_header - def find_sar_value(self): - """Locate ObpCaptureHandleInformationEx if it exists in the sample. - - Once found, parse it for the SAR value that we need to decode - pointers in the _HANDLE_TABLE_ENTRY which allows us to find the - associated _OBJECT_HEADER. - """ - DEFAULT_SAR_VALUE = 0x10 # to be used only when decoding fails - - if self._sar_value is None: - if not has_capstone: - vollog.debug( - "capstone module is missing, unable to create disassembly of ObpCaptureHandleInformationEx" - ) - return None - kernel = self.context.modules[self.config["kernel"]] - - virtual_layer_name = kernel.layer_name - kvo = self.context.layers[virtual_layer_name].config[ - "kernel_virtual_offset" - ] - ntkrnlmp = self.context.module( - kernel.symbol_table_name, layer_name=virtual_layer_name, offset=kvo - ) - - try: - func_addr = ntkrnlmp.get_symbol("ObpCaptureHandleInformationEx").address - except exceptions.SymbolError: - vollog.debug("Unable to locate ObpCaptureHandleInformationEx symbol") - return None - - try: - func_addr_to_read = kvo + func_addr - num_bytes_to_read = 0x200 - vollog.debug( - f"ObpCaptureHandleInformationEx symbol located at {hex(func_addr_to_read)}" - ) - data = self.context.layers.read( - virtual_layer_name, func_addr_to_read, num_bytes_to_read - ) - except exceptions.InvalidAddressException: - vollog.warning( - f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of {hex(DEFAULT_SAR_VALUE)}" - ) - self._sar_value = DEFAULT_SAR_VALUE - return self._sar_value - - md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) - - instruction_count = 0 - for address, size, mnemonic, op_str in md.disasm_lite( - data, kvo + func_addr - ): - # print("{} {} {} {}".format(address, size, mnemonic, op_str)) - instruction_count += 1 - if mnemonic.startswith("sar"): - # if we don't want to parse op strings, we can disasm the - # single sar instruction again, but we use disasm_lite for speed - self._sar_value = int(op_str.split(",")[1].strip(), 16) - vollog.debug( - f"SAR located at {hex(address)} with value of {hex(self._sar_value)}" - ) - break - - if self._sar_value is None: - vollog.warning( - f"Failed to to locate SAR value having parsed {instruction_count} instructions, failing back to a common value of {hex(DEFAULT_SAR_VALUE)}" - ) - self._sar_value = DEFAULT_SAR_VALUE - - return self._sar_value - @classmethod def get_type_map( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Dict[int, str]: """List the executive object types (_OBJECT_TYPE) using the ObTypeIndexTable or ObpObjectTypes symbol (differs per OS). This method @@ -234,17 +148,16 @@ class Handles(interfaces.plugins.PluginInterface): type_map: Dict[int, str] = {} - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] try: table_addr = ntkrnlmp.get_symbol("ObTypeIndexTable").address except exceptions.SymbolError: table_addr = ntkrnlmp.get_symbol("ObpObjectTypes").address - trans_layer = context.layers[layer_name] + trans_layer = context.layers[ntkrnlmp.layer_name] - if not trans_layer.is_valid(kvo + table_addr): + if not trans_layer.is_valid(ntkrnlmp.offset + table_addr): return type_map ptrs = ntkrnlmp.object( @@ -262,13 +175,13 @@ class Handles(interfaces.plugins.PluginInterface): try: objt = ptr.dereference().cast( - symbol_table + constants.BANG + "_OBJECT_TYPE" + ntkrnlmp.symbol_table_name + constants.BANG + "_OBJECT_TYPE" ) type_name = objt.Name.String except exceptions.InvalidAddressException: vollog.log( constants.LOGLEVEL_VVV, - f"Cannot access _OBJECT_HEADER Name at {objt.vol.offset:#x}", + f"Cannot access _OBJECT_HEADER Name at {ptr.vol.offset:#x}", ) continue @@ -280,49 +193,52 @@ class Handles(interfaces.plugins.PluginInterface): def find_cookie( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Optional[interfaces.objects.ObjectInterface]: """Find the ObHeaderCookie value (if it exists)""" + kernel = context.modules[kernel_module_name] + try: - offset = context.symbol_space.get_symbol( - symbol_table + constants.BANG + "ObHeaderCookie" - ).address + symbol_offset = kernel.get_symbol("ObHeaderCookie").address except exceptions.SymbolError: + vollog.debug('Unable to get symbol information for "ObHeaderCookie"') return None - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - return context.object( - symbol_table + constants.BANG + "unsigned int", - layer_name, - offset=kvo + offset, + return kernel.object( + "unsigned int", + offset=symbol_offset, ) - def _make_handle_array(self, offset, level, depth=0): - """Parse a process' handle table and yield valid handle table entries, - going as deep into the table "levels" as necessary.""" + @classmethod + def _make_handle_array( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + offset: int, + level: int, + depth: int = 0, + ) -> Iterator[interfaces.objects.ObjectInterface]: + """ + Parses a process' handle table by constructing an array of + `_HANDLE_TABLE_ENTRY` structures at the given offset, and yields valid + handle table entries, going as deep into the table "levels" as + necessary. + """ - kernel = self.context.modules[self.config["kernel"]] - - virtual = kernel.layer_name - kvo = self.context.layers[virtual].config["kernel_virtual_offset"] - - ntkrnlmp = self.context.module( - kernel.symbol_table_name, layer_name=virtual, offset=kvo - ) + kernel = context.modules[kernel_module_name] if level > 0: - subtype = ntkrnlmp.get_type("pointer") + subtype = kernel.get_type("pointer") count = 0x1000 / subtype.size else: - subtype = ntkrnlmp.get_type("_HANDLE_TABLE_ENTRY") + subtype = kernel.get_type("_HANDLE_TABLE_ENTRY") count = 0x1000 / subtype.size - if not self.context.layers[virtual].is_valid(offset): + if not context.layers[kernel.layer_name].is_valid(offset): return None - table = ntkrnlmp.object( + table = kernel.object( object_type="array", offset=offset, subtype=subtype, @@ -330,13 +246,27 @@ class Handles(interfaces.plugins.PluginInterface): absolute=True, ) - layer_object = self.context.layers[virtual] + layer_object = context.layers[kernel.layer_name] masked_offset = offset & layer_object.maximum_address - for entry in table: + for i in range(len(table)): + try: + entry = table[i] + except exceptions.InvalidAddressException: + vollog.debug(f"Failed to get handle table entry at index {i}") + continue + # This triggered a backtrace in many testing samples + # in the level == 0 path + # The code above this calls `is_valid` on the `offset` + # It is sent but then does not validate `entry` before + # sending it to `_get_item` + if not context.layers[kernel.layer_name].is_valid(entry.vol.offset): + continue + if level > 0: - for x in self._make_handle_array(entry, level - 1, depth): - yield x + yield from cls._make_handle_array( + context, kernel_module_name, entry, level - 1, depth + ) depth += 1 else: handle_multiplier = 4 @@ -347,7 +277,7 @@ class Handles(interfaces.plugins.PluginInterface): / (subtype.size / handle_multiplier) ) + handle_level_base - item = self._get_item(entry, handle_value) + item = cls._get_item(context, kernel_module_name, entry, handle_value) if item is None: continue @@ -361,10 +291,21 @@ class Handles(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: continue - def handles(self, handle_table): + @classmethod + def handles( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + handle_table: interfaces.objects.ObjectInterface, + ) -> Iterator[interfaces.objects.ObjectInterface]: + """ + Takes a context, kernel module name, and handle table structure + (_HANDLE_TABLE), and yields _HANDLE_TABLE_ENTRY structures from the + handle table. + """ try: - TableCode = handle_table.TableCode & ~self._level_mask - table_levels = handle_table.TableCode & self._level_mask + TableCode = handle_table.TableCode & ~cls.LEVEL_MASK + table_levels = handle_table.TableCode & cls.LEVEL_MASK except exceptions.InvalidAddressException: vollog.log( constants.LOGLEVEL_VVV, @@ -372,22 +313,17 @@ class Handles(interfaces.plugins.PluginInterface): ) return None - for handle_table_entry in self._make_handle_array(TableCode, table_levels): - yield handle_table_entry + yield from cls._make_handle_array( + context, kernel_module_name, TableCode, table_levels + ) def _generator(self, procs): - kernel = self.context.modules[self.config["kernel"]] - type_map = self.get_type_map( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + context=self.context, kernel_module_name=self.config["kernel"] ) cookie = self.find_cookie( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + context=self.context, kernel_module_name=self.config["kernel"] ) for proc in procs: @@ -402,7 +338,9 @@ class Handles(interfaces.plugins.PluginInterface): process_name = utility.array_to_string(proc.ImageFileName) - for entry in self.handles(object_table): + for entry in self.handles( + self.context, self.config["kernel"], object_table + ): try: obj_type = entry.get_object_type(type_map, cookie) if obj_type is None: @@ -425,7 +363,7 @@ class Handles(interfaces.plugins.PluginInterface): try: obj_name = entry.NameInfo.Name.String except (ValueError, exceptions.InvalidAddressException): - obj_name = "" + obj_name = None except exceptions.InvalidAddressException: vollog.log( @@ -443,7 +381,7 @@ class Handles(interfaces.plugins.PluginInterface): format_hints.Hex(entry.HandleValue), obj_type, format_hints.Hex(entry.GrantedAccess), - obj_name, + obj_name or renderers.NotAvailableValue(), ), ) @@ -454,8 +392,7 @@ class Handles(interfaces.plugins.PluginInterface): if self.config["offset"]: procs = psscan.PsScan.scan_processes( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], filter_func=psscan.PsScan.create_offset_filter( self.context, kernel.layer_name, @@ -465,8 +402,7 @@ class Handles(interfaces.plugins.PluginInterface): else: procs = pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) diff --git a/volatility3/framework/plugins/windows/hashdump.py b/volatility3/framework/plugins/windows/hashdump.py index 0c98ab8ca..c4b99d86f 100644 --- a/volatility3/framework/plugins/windows/hashdump.py +++ b/volatility3/framework/plugins/windows/hashdump.py @@ -1,613 +1,21 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import binascii -import hashlib import logging -from struct import pack, unpack -from typing import List, Optional, Tuple -from Crypto.Cipher import AES, ARC4, DES -from Crypto.Hash import MD5 - -from volatility3.framework import interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.windows.extensions import registry -from volatility3.plugins.windows.registry import hivelist +from volatility3.framework import deprecation, interfaces +from volatility3.plugins.windows.registry import hashdump vollog = logging.getLogger(__name__) -class Hashdump(interfaces.plugins.PluginInterface): - """Dumps user hashes from memory""" +class Hashdump( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=hashdump.Hashdump, + removal_date="2026-09-25", +): + """Dumps user hashes from memory (deprecated)""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) - ), - ] - - odd_parity = [ - 1, - 1, - 2, - 2, - 4, - 4, - 7, - 7, - 8, - 8, - 11, - 11, - 13, - 13, - 14, - 14, - 16, - 16, - 19, - 19, - 21, - 21, - 22, - 22, - 25, - 25, - 26, - 26, - 28, - 28, - 31, - 31, - 32, - 32, - 35, - 35, - 37, - 37, - 38, - 38, - 41, - 41, - 42, - 42, - 44, - 44, - 47, - 47, - 49, - 49, - 50, - 50, - 52, - 52, - 55, - 55, - 56, - 56, - 59, - 59, - 61, - 61, - 62, - 62, - 64, - 64, - 67, - 67, - 69, - 69, - 70, - 70, - 73, - 73, - 74, - 74, - 76, - 76, - 79, - 79, - 81, - 81, - 82, - 82, - 84, - 84, - 87, - 87, - 88, - 88, - 91, - 91, - 93, - 93, - 94, - 94, - 97, - 97, - 98, - 98, - 100, - 100, - 103, - 103, - 104, - 104, - 107, - 107, - 109, - 109, - 110, - 110, - 112, - 112, - 115, - 115, - 117, - 117, - 118, - 118, - 121, - 121, - 122, - 122, - 124, - 124, - 127, - 127, - 128, - 128, - 131, - 131, - 133, - 133, - 134, - 134, - 137, - 137, - 138, - 138, - 140, - 140, - 143, - 143, - 145, - 145, - 146, - 146, - 148, - 148, - 151, - 151, - 152, - 152, - 155, - 155, - 157, - 157, - 158, - 158, - 161, - 161, - 162, - 162, - 164, - 164, - 167, - 167, - 168, - 168, - 171, - 171, - 173, - 173, - 174, - 174, - 176, - 176, - 179, - 179, - 181, - 181, - 182, - 182, - 185, - 185, - 186, - 186, - 188, - 188, - 191, - 191, - 193, - 193, - 194, - 194, - 196, - 196, - 199, - 199, - 200, - 200, - 203, - 203, - 205, - 205, - 206, - 206, - 208, - 208, - 211, - 211, - 213, - 213, - 214, - 214, - 217, - 217, - 218, - 218, - 220, - 220, - 223, - 223, - 224, - 224, - 227, - 227, - 229, - 229, - 230, - 230, - 233, - 233, - 234, - 234, - 236, - 236, - 239, - 239, - 241, - 241, - 242, - 242, - 244, - 244, - 247, - 247, - 248, - 248, - 251, - 251, - 253, - 253, - 254, - 254, - ] - - # Permutation matrix for boot key - bootkey_perm_table = [ - 0x8, - 0x5, - 0x4, - 0x2, - 0xB, - 0x9, - 0xD, - 0x3, - 0x0, - 0x6, - 0x1, - 0xC, - 0xE, - 0xA, - 0xF, - 0x7, - ] - - # Constants for SAM decrypt algorithm - aqwerty = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" - anum = b"0123456789012345678901234567890123456789\0" - antpassword = b"NTPASSWORD\0" - almpassword = b"LMPASSWORD\0" - lmkey = b"KGS!@#$%" - - empty_lm = b"\xaa\xd3\xb4\x35\xb5\x14\x04\xee\xaa\xd3\xb4\x35\xb5\x14\x04\xee" - empty_nt = b"\x31\xd6\xcf\xe0\xd1\x6a\xe9\x31\xb7\x3c\x59\xd7\xe0\xc0\x89\xc0" - - @classmethod - def get_hive_key(cls, hive: registry.RegistryHive, key: str): - result = None - try: - if hive: - result = hive.get_key(key) - except KeyError: - vollog.info( - f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image" - ) - return result - - @classmethod - def get_user_keys( - cls, samhive: registry.RegistryHive - ) -> List[interfaces.objects.ObjectInterface]: - user_key_path = "SAM\\Domains\\Account\\Users" - - user_key = cls.get_hive_key(samhive, user_key_path) - - if not user_key: - return [] - return [k for k in user_key.get_subkeys() if k.Name != "Names"] - - @classmethod - def get_bootkey(cls, syshive: registry.RegistryHive) -> Optional[bytes]: - cs = 1 - lsa_base = f"ControlSet{cs:03}" + "\\Control\\Lsa" - lsa_keys = ["JD", "Skew1", "GBG", "Data"] - - lsa = cls.get_hive_key(syshive, lsa_base) - - if not lsa: - return None - - bootkey = "" - - for lk in lsa_keys: - key = cls.get_hive_key(syshive, lsa_base + "\\" + lk) - class_data = None - if key: - class_data = syshive.read(key.Class + 4, key.ClassLength) - - if class_data is None: - return None - bootkey += class_data.decode("utf-16-le") - - bootkey_str = binascii.unhexlify(bootkey) - bootkey_scrambled = bytes( - [bootkey_str[cls.bootkey_perm_table[i]] for i in range(len(bootkey_str))] - ) - return bootkey_scrambled - - @classmethod - def get_hbootkey( - cls, samhive: registry.RegistryHive, bootkey: bytes - ) -> Optional[bytes]: - sam_account_path = "SAM\\Domains\\Account" - - if not bootkey: - return None - - sam_account_key = cls.get_hive_key(samhive, sam_account_path) - if not sam_account_key: - return None - - sam_data = None - for v in sam_account_key.get_values(): - if v.get_name() == "F": - sam_data = samhive.read(v.Data + 4, v.DataLength) - if not sam_data: - return None - - revision = sam_data[0x00] - if revision == 2: - md5 = hashlib.md5() - - md5.update(sam_data[0x70:0x80] + cls.aqwerty + bootkey + cls.anum) - rc4_key = md5.digest() - - rc4 = ARC4.new(rc4_key) - hbootkey = rc4.encrypt( - sam_data[0x80:0xA0] - ) # lgtm [py/weak-cryptographic-algorithm] - return hbootkey - elif revision == 3: - # AES encrypted - iv = sam_data[0x78:0x88] - encryptedHBootKey = sam_data[0x88:0xA8] - cipher = AES.new(bootkey, AES.MODE_CBC, iv) - hbootkey = cipher.decrypt(encryptedHBootKey) - return hbootkey[:16] - return None - - @classmethod - def decrypt_single_salted_hash( - cls, rid, hbootkey: bytes, enc_hash: bytes, _lmntstr, salt: bytes - ) -> Optional[bytes]: - (des_k1, des_k2) = cls.sid_to_key(rid) - des1 = DES.new(des_k1, DES.MODE_ECB) - des2 = DES.new(des_k2, DES.MODE_ECB) - cipher = AES.new(hbootkey[:16], AES.MODE_CBC, salt) - obfkey = cipher.decrypt(enc_hash) - return des1.decrypt(obfkey[:8]) + des2.decrypt( - obfkey[8:16] - ) # lgtm [py/weak-cryptographic-algorithm] - - @classmethod - def get_user_hashes( - cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive, hbootkey: bytes - ) -> Optional[Tuple[bytes, bytes]]: - ## Will sometimes find extra user with rid = NAMES, returns empty strings right now - try: - rid = int(str(user.get_name()), 16) - except ValueError: - return None - sam_data = None - for v in user.get_values(): - if v.get_name() == "V": - sam_data = samhive.read(v.Data + 4, v.DataLength) - if not sam_data: - return None - - lm_offset = unpack(" Tuple[bytes, bytes]: - """Takes rid of a user and converts it to a key to be used by the DES cipher""" - bytestr1 = [ - sid & 0xFF, - (sid >> 8) & 0xFF, - (sid >> 16) & 0xFF, - (sid >> 24) & 0xFF, - ] - bytestr1 += bytestr1[0:3] - bytestr2 = [bytestr1[3]] + bytestr1[0:3] - bytestr2 += bytestr2[0:3] - return cls.sidbytes_to_key(bytes(bytestr1)), cls.sidbytes_to_key( - bytes(bytestr2) - ) - - @classmethod - def sidbytes_to_key(cls, s: bytes) -> bytes: - """Builds final DES key from the strings generated in sid_to_key""" - key = [ - s[0] >> 1, - ((s[0] & 0x01) << 6) | (s[1] >> 2), - ((s[1] & 0x03) << 5) | (s[2] >> 3), - ((s[2] & 0x07) << 4) | (s[3] >> 4), - ((s[3] & 0x0F) << 3) | (s[4] >> 5), - ((s[4] & 0x1F) << 2) | (s[5] >> 6), - ((s[5] & 0x3F) << 1) | (s[6] >> 7), - s[6] & 0x7F, - ] - for i in range(8): - key[i] = key[i] << 1 - key[i] = cls.odd_parity[key[i]] - return bytes(key) - - @classmethod - def decrypt_single_hash( - cls, rid: int, hbootkey: bytes, enc_hash: bytes, lmntstr: bytes - ): - (des_k1, des_k2) = cls.sid_to_key(rid) - des1 = DES.new(des_k1, DES.MODE_ECB) - des2 = DES.new(des_k2, DES.MODE_ECB) - md5 = MD5.new() - - md5.update(hbootkey[:0x10] + pack(" Optional[bytes]: - value = None - for v in user.get_values(): - if v.get_name() == "V": - value = samhive.read(v.Data + 4, v.DataLength) - if not value: - return None - - name_offset = unpack(" len(value): - return None - - username = value[name_offset : name_offset + name_length] - return username - - # replaces the dump_hashes method in vol2 - def _generator( - self, syshive: registry.RegistryHive, samhive: registry.RegistryHive - ): - if syshive is None: - vollog.debug("SYSTEM address is None: No system hive found") - if samhive is None: - vollog.debug("SAM address is None: No SAM hive found") - bootkey = self.get_bootkey(syshive) - hbootkey = self.get_hbootkey(samhive, bootkey) - if hbootkey: - for user in self.get_user_keys(samhive): - ret = self.get_user_hashes(user, samhive, hbootkey) - if ret: - lmhash, nthash = ret - - ## temporary fix to prevent UnicodeDecodeError backtraces - ## however this can cause truncated user names as a result - name = self.get_user_name(user, samhive) - if name is None: - name = renderers.NotAvailableValue() - else: - name = str(name, "utf-16-le", errors="ignore") - - lmout = str(binascii.hexlify(lmhash or self.empty_lm), "latin-1") - ntout = str(binascii.hexlify(nthash or self.empty_nt), "latin-1") - rid = int(str(user.get_name()), 16) - yield (0, (name, rid, lmout, ntout)) - else: - vollog.warning("Hbootkey is not valid") - - def run(self): - offset = self.config.get("offset", None) - syshive = None - samhive = None - kernel = self.context.modules[self.config["kernel"]] - for hive in hivelist.HiveList.list_hives( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - hive_offsets=None if offset is None else [offset], - ): - if hive.get_name().split("\\")[-1].upper() == "SYSTEM": - syshive = hive - if hive.get_name().split("\\")[-1].upper() == "SAM": - samhive = hive - - return renderers.TreeGrid( - [("User", str), ("rid", int), ("lmhash", str), ("nthash", str)], - self._generator(syshive, samhive), - ) + _version = (1, 1, 1) diff --git a/volatility3/framework/plugins/windows/hollowprocesses.py b/volatility3/framework/plugins/windows/hollowprocesses.py index 69fa94f06..9949a223c 100644 --- a/volatility3/framework/plugins/windows/hollowprocesses.py +++ b/volatility3/framework/plugins/windows/hollowprocesses.py @@ -1,237 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import NamedTuple, Dict, Generator - -from volatility3.framework import interfaces, exceptions, constants -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import hollowprocesses vollog = logging.getLogger(__name__) -VadData = NamedTuple( - "VadData", - [ - ("protection", str), - ("path", str), - ], -) -DLLData = NamedTuple( - "DLLData", - [ - ("path", str), - ], -) - -### Useful references on process hollowing -# https://cysinfo.com/detecting-deceptive-hollowing-techniques/ -# https://github.com/m0n0ph1/Process-Hollowing - - -class HollowProcesses(interfaces.plugins.PluginInterface): - """Lists hollowed processes""" +class HollowProcesses( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=hollowprocesses.HollowProcesses, + removal_date="2026-06-07", +): + """Lists hollowed processes (deprecated)""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - element_type=int, - description="Process IDs to include (all other processes are excluded)", - optional=True, - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - def _get_vads_data( - self, proc: interfaces.objects.ObjectInterface - ) -> Dict[int, VadData]: - """ - Returns a dictionary of: - base address -> (protection string, file name) - For each mapped VAD in the process. This is used - for quick lookups of data and matching the DLL - at the same base address as the VAD - """ - vads = {} - - kernel = self.context.modules[self.config["kernel"]] - - for vad in proc.get_vad_root().traverse(): - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, kernel.layer_name, kernel.symbol_table_name - ), - vadinfo.winnt_protections, - ) - - fn = vad.get_file_name() - if not fn or not isinstance(fn, str): - fn = "" - - vads[vad.get_start()] = VadData(protection_string, fn) - - return vads - - def _get_dlls_map( - self, proc: interfaces.objects.ObjectInterface - ) -> Dict[int, DLLData]: - """ - Returns a dictionary of: - base address -> path - for each DLL loaded in the process - - This is used to cross compare with - the corresponding VAD and to have a - backup path source in case of smear - in the VAD - """ - dlls = {} - - for entry in proc.load_order_modules(): - try: - base = entry.DllBase - except exceptions.InvalidAddressException: - continue - - try: - FullDllName = entry.FullDllName.get_string() - except exceptions.InvalidAddressException: - FullDllName = renderers.UnreadableValue() - - dlls[base] = DLLData(FullDllName) - - return dlls - - def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int: - """ - Uses the PEB to get the image base of the process - """ - kernel = self.context.modules[self.config["kernel"]] - - try: - proc_layer_name = proc.add_process_layer() - peb = self.context.object( - kernel.symbol_table_name + constants.BANG + "_PEB", - layer_name=proc_layer_name, - offset=proc.Peb, - ) - return peb.ImageBaseAddress - except exceptions.InvalidAddressException: - return None - - def _check_load_address(self, proc, _, __) -> Generator[str, None, None]: - """ - Detects when the image base in the PEB, which is writable by process malware, - does not match the section base address - whose value lives in kernel memory. - Many malware samples will manipulate their image base to fool AVs/EDRs and - as a necessary part of certain hollowing techniques - """ - image_base = self._get_image_base(proc) - if image_base is not None and image_base != proc.SectionBaseAddress: - yield "The ImageBaseAddress reported from the PEB ({:#x}) does not match the process SectionBaseAddress ({:#x})".format( - image_base, proc.SectionBaseAddress - ) - - def _check_exe_protection( - self, proc, vads: Dict[int, VadData], __ - ) -> Generator[str, None, None]: - """ - Legitimately mapped application executables and DLLs - will have a VAD present and its initial protection will be - PAGE_EXECUTE_WRITECOPY. - Many process hollowing and code injection techniques will - unmap the real executable and/or map in executables with - incorrect permissions. - This check verifies the VAD for the application exe. - `_check_dlls_protection` checks for DLLs mapped in the process. - """ - base = proc.SectionBaseAddress - - if base not in vads: - yield "There is no VAD starting at the base address of the process executable ({:#x})".format( - base - ) - elif vads[base].protection != "PAGE_EXECUTE_WRITECOPY": - yield "Unexpected protection ({}) for VAD hosting the process executable ({:#x}) with path {}".format( - vads[base].protection, base, vads[base].path - ) - - def _check_dlls_protection( - self, _, vads: Dict[int, VadData], dlls: Dict[int, DLLData] - ) -> Generator[str, None, None]: - for dll_base in dlls: - # could be malicious but triggers too many FPs from smear - if dll_base not in vads: - continue - - # PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files - if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY": - yield "Unexpected protection ({}) for DLL in the PEB's load order list ({:#x}) with path {}".format( - vads[dll_base].protection, dll_base, dlls[dll_base].path - ) - - def _generator(self, procs): - checks = [ - self._check_load_address, - self._check_exe_protection, - self._check_dlls_protection, - ] - - for proc in procs: - # smear and/or terminated process - dlls = self._get_dlls_map(proc) - if len(dlls) < 3: - continue - - vads = self._get_vads_data(proc) - if len(vads) < 5: - continue - - proc_name = utility.array_to_string(proc.ImageFileName) - pid = proc.UniqueProcessId - - for check in checks: - for note in check(proc, vads, dlls): - yield 0, ( - pid, - proc_name, - note, - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - kernel = self.context.modules[self.config["kernel"]] - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Notes", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=filter_func, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/iat.py b/volatility3/framework/plugins/windows/iat.py index d2fdc0ad8..f2ba8e576 100644 --- a/volatility3/framework/plugins/windows/iat.py +++ b/volatility3/framework/plugins/windows/iat.py @@ -1,7 +1,9 @@ # 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 -import logging, io, pefile +import logging +import io +import pefile from volatility3.framework.symbols import intermed from volatility3.framework import renderers, interfaces, exceptions, constants from volatility3.framework.configuration import requirements @@ -26,7 +28,7 @@ class IAT(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -67,11 +69,23 @@ class IAT(interfaces.plugins.PluginInterface): layer_name=proc_layer_name, ) - for offset, data in dos_header.reconstruct(): - pe_data.seek(offset) - pe_data.write(data) + try: + for offset, data in dos_header.reconstruct(): + pe_data.seek(offset) + pe_data.write(data) + except (exceptions.InvalidAddressException, ValueError) as excp: + vollog.warning( + f"Exception triggered when reconstructing PE file for process {proc.UniqueProcessId} at address {peb.ImageBaseAddress:#x} due to {excp}. Output file may be corrupt and/or truncated." + ) + + try: + pe_obj = pefile.PE(data=pe_data.getvalue(), fast_load=True) + except pefile.PEFormatError as excp: + vollog.debug( + f"Exception triggered when creating PE file object for process {proc.UniqueProcessId} at address {peb.ImageBaseAddress:#x} due to {excp}. Unable to extract file." + ) + continue - pe_obj = pefile.PE(data=pe_data.getvalue(), fast_load=True) pe_obj.parse_data_directories( [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_IMPORT"]] ) @@ -119,15 +133,11 @@ class IAT(interfaces.plugins.PluginInterface): ) except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) continue def run(self): - kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid( [ ("PID", int), @@ -140,8 +150,7 @@ class IAT(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=pslist.PsList.create_pid_filter( self.config.get("pid", None) ), diff --git a/volatility3/framework/plugins/windows/indirect_system_calls.py b/volatility3/framework/plugins/windows/indirect_system_calls.py new file mode 100644 index 000000000..65f5f8734 --- /dev/null +++ b/volatility3/framework/plugins/windows/indirect_system_calls.py @@ -0,0 +1,21 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from volatility3.framework import deprecation +from volatility3.plugins.windows.malware import indirect_system_calls +from volatility3.plugins.windows.malware import direct_system_calls + +vollog = logging.getLogger(__name__) + + +class IndirectSystemCalls( + direct_system_calls.DirectSystemCalls, + deprecation.PluginRenameClass, + replacement_class=indirect_system_calls.IndirectSystemCalls, + removal_date="2026-06-07", +): + """Detects the Indirect System Call technique used to bypass EDRs (deprecated).""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/info.py b/volatility3/framework/plugins/windows/info.py index 137d29c22..3ff224c68 100644 --- a/volatility3/framework/plugins/windows/info.py +++ b/volatility3/framework/plugins/windows/info.py @@ -3,12 +3,11 @@ # import time -from typing import List, Tuple, Iterable +from typing import Iterable, List, Tuple -from volatility3.framework import constants, interfaces, layers, symbols +from volatility3.framework import constants, interfaces, layers, renderers, symbols from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import TreeGrid from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import kdbg, pe @@ -17,7 +16,7 @@ class Info(plugins.PluginInterface): """Show OS & kernel details of the memory sample being analyzed.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -56,6 +55,9 @@ class Info(plugins.PluginInterface): # FileLayer won't have dependencies pass + # FIXME - this needs to be deprecated. This is exactly the same + # as getting it from context.modules + # Deprecation warning will go once the API is overhauled @classmethod def get_kernel_module( cls, @@ -68,7 +70,9 @@ class Info(plugins.PluginInterface): if not isinstance(virtual_layer, layers.intel.Intel): raise TypeError("Virtual Layer is not an intel layer") - kvo = virtual_layer.config["kernel_virtual_offset"] + kvo = virtual_layer.config.get("kernel_virtual_offset", None) + if not kvo: + raise ValueError("Intel layer has no kernel virtual offset defined") ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) return ntkrnlmp @@ -78,13 +82,12 @@ class Info(plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, config_path: str, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> interfaces.objects.ObjectInterface: """Returns the KDDEBUGGER_DATA64 structure for a kernel""" - ntkrnlmp = cls.get_kernel_module(context, layer_name, symbol_table) + ntkrnlmp = context.modules[kernel_module_name] - native_types = context.symbol_space[symbol_table].natives + native_types = context.symbol_space[ntkrnlmp.symbol_table_name].natives kdbg_offset = ntkrnlmp.get_symbol("KdDebuggerDataBlock").address @@ -100,7 +103,7 @@ class Info(plugins.PluginInterface): kdbg_obj = context.object( kdbg_table_name + constants.BANG + "_KDDEBUGGER_DATA64", offset=ntkrnlmp.offset + kdbg_offset, - layer_name=layer_name, + layer_name=ntkrnlmp.layer_name, ) return kdbg_obj @@ -109,16 +112,15 @@ class Info(plugins.PluginInterface): def get_kuser_structure( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> interfaces.objects.ObjectInterface: """Returns the _KUSER_SHARED_DATA structure for a kernel""" - virtual_layer = context.layers[layer_name] + ntkrnlmp = context.modules[kernel_module_name] + + virtual_layer = context.layers[ntkrnlmp.layer_name] if not isinstance(virtual_layer, layers.intel.Intel): raise TypeError("Virtual Layer is not an intel layer") - ntkrnlmp = cls.get_kernel_module(context, layer_name, symbol_table) - # this is a hard-coded address in the Windows OS if virtual_layer.bits_per_register == 32: kuser_addr = 0xFFDF0000 @@ -127,7 +129,6 @@ class Info(plugins.PluginInterface): kuser = ntkrnlmp.object( object_type="_KUSER_SHARED_DATA", - layer_name=layer_name, offset=kuser_addr, absolute=True, ) @@ -138,17 +139,15 @@ class Info(plugins.PluginInterface): def get_version_structure( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> interfaces.objects.ObjectInterface: """Returns the KdVersionBlock information from a kernel""" - ntkrnlmp = cls.get_kernel_module(context, layer_name, symbol_table) + ntkrnlmp = context.modules[kernel_module_name] vers_offset = ntkrnlmp.get_symbol("KdVersionBlock").address vers = ntkrnlmp.object( object_type="_DBGKD_GET_VERSION64", - layer_name=layer_name, offset=vers_offset, ) @@ -166,7 +165,9 @@ class Info(plugins.PluginInterface): if not isinstance(virtual_layer, layers.intel.Intel): raise TypeError("Virtual Layer is not an intel layer") - kvo = virtual_layer.config["kernel_virtual_offset"] + kvo = virtual_layer.config.get("kernel_virtual_offset", None) + if not kvo: + raise ValueError("Intel layer has no kernel virtual offset defined") pe_table_name = intermed.IntermediateSymbolTable.create( context, @@ -189,28 +190,38 @@ class Info(plugins.PluginInterface): def _generator(self): kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name - symbol_table = kernel.symbol_table_name - layer = self.context.layers[layer_name] - table = self.context.symbol_space[symbol_table] + kernel_layer = self.context.layers[kernel.layer_name] + symbol_table = self.context.symbol_space[kernel.symbol_table_name] kdbg = self.get_kdbg_structure( - self.context, self.config_path, layer_name, symbol_table + self.context, + self.config_path, + self.config["kernel"], ) - yield (0, ("Kernel Base", hex(layer.config["kernel_virtual_offset"]))) - yield (0, ("DTB", hex(layer.config["page_map_offset"]))) - yield (0, ("Symbols", table.config["isf_url"])) + yield (0, ("Kernel Base", hex(kernel_layer.config["kernel_virtual_offset"]))) + yield (0, ("DTB", hex(kernel_layer.config["page_map_offset"]))) + yield (0, ("Symbols", symbol_table.config["isf_url"])) yield ( 0, - ("Is64Bit", str(symbols.symbol_table_is_64bit(self.context, symbol_table))), + ( + "Is64Bit", + str( + symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ) + ), + ), ) yield ( 0, - ("IsPAE", str(self.context.layers[layer_name].metadata.get("pae", False))), + ( + "IsPAE", + str(self.context.layers[kernel.layer_name].metadata.get("pae", False)), + ), ) - for i, layer in self.get_depends(self.context, layer_name): + for i, layer in self.get_depends(self.context, kernel.layer_name): yield (0, (layer.name, f"{i} {layer.__class__.__name__}")) if kdbg.Header.OwnerTag == 0x4742444B: @@ -218,23 +229,22 @@ class Info(plugins.PluginInterface): yield (0, ("NTBuildLab", kdbg.get_build_lab())) yield (0, ("CSDVersion", str(kdbg.get_csdversion()))) - vers = self.get_version_structure(self.context, layer_name, symbol_table) + vers = self.get_version_structure(self.context, self.config["kernel"]) yield (0, ("KdVersionBlock", hex(vers.vol.offset))) yield (0, ("Major/Minor", f"{vers.MajorVersion}.{vers.MinorVersion}")) yield (0, ("MachineType", str(vers.MachineType))) - ntkrnlmp = self.get_kernel_module(self.context, layer_name, symbol_table) + cpu_count_offset = kernel.get_symbol("KeNumberProcessors").address - cpu_count_offset = ntkrnlmp.get_symbol("KeNumberProcessors").address - - cpu_count = ntkrnlmp.object( - object_type="unsigned int", layer_name=layer_name, offset=cpu_count_offset + cpu_count = kernel.object( + object_type="unsigned int", + offset=cpu_count_offset, ) yield (0, ("KeNumberProcessors", str(cpu_count))) - kuser = self.get_kuser_structure(self.context, layer_name, symbol_table) + kuser = self.get_kuser_structure(self.context, self.config["kernel"]) yield (0, ("SystemTime", str(kuser.SystemTime.get_time()))) yield ( @@ -255,7 +265,7 @@ class Info(plugins.PluginInterface): # yield (0, ("SafeBootMode", "True" if kuser.SafeBootMode else "False")) nt_header = self.get_ntheader_structure( - self.context, self.config_path, layer_name + self.context, self.config_path, kernel.layer_name ) yield ( @@ -283,4 +293,6 @@ class Info(plugins.PluginInterface): ) def run(self): - return TreeGrid([("Variable", str), ("Value", str)], self._generator()) + return renderers.TreeGrid( + [("Variable", str), ("Value", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/windows/joblinks.py b/volatility3/framework/plugins/windows/joblinks.py index d84c133c0..a7fa4e709 100644 --- a/volatility3/framework/plugins/windows/joblinks.py +++ b/volatility3/framework/plugins/windows/joblinks.py @@ -36,16 +36,18 @@ class JobLinks(interfaces.plugins.PluginInterface): optional=True, ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), ] def _generator(self) -> Iterator[Tuple]: kernel = self.context.modules[self.config["kernel"]] + memory = self.context.layers[kernel.layer_name] for proc in pslist.PsList.list_processes( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], ): try: if not self.config["physical"]: diff --git a/volatility3/framework/plugins/windows/kpcrs.py b/volatility3/framework/plugins/windows/kpcrs.py index 558ea844c..d544fd498 100644 --- a/volatility3/framework/plugins/windows/kpcrs.py +++ b/volatility3/framework/plugins/windows/kpcrs.py @@ -4,7 +4,7 @@ import logging -from typing import Iterator, List, Tuple +from typing import Iterator, Generator, List, Tuple from volatility3.framework import ( renderers, @@ -22,7 +22,7 @@ class KPCRs(interfaces.plugins.PluginInterface): """Print KPCR structure for each processor""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -39,60 +39,69 @@ class KPCRs(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - layer_name: str, - symbol_table: str, - ) -> interfaces.objects.ObjectInterface: + ) -> Generator[Tuple[interfaces.objects.ObjectInterface, int], None, None]: """Returns the KPCR structure for each processor Args: context: The context to retrieve required elements (layers, symbol tables) from kernel_module_name: The name of the kernel module on which to operate - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols Returns: The _KPCR structure for each processor """ kernel = context.modules[kernel_module_name] + kernel_layer = context.layers[kernel.layer_name] + + kpcr_type = kernel.get_type("_KPCR") + + reloff = kpcr_type.relative_child_offset("Prcb") + + if kpcr_type.has_member("CurrentPrcb"): + kpcr_member = "CurrentPrcb" + else: + kpcr_member = "Prcb" + cpu_count_offset = kernel.get_symbol("KeNumberProcessors").address + cpu_count = kernel.object( - object_type="unsigned int", layer_name=layer_name, offset=cpu_count_offset + object_type="unsigned int", + layer_name=kernel_layer.name, + offset=cpu_count_offset, ) + processor_block = kernel.object( object_type="pointer", - layer_name=layer_name, + layer_name=kernel_layer.name, offset=kernel.get_symbol("KiProcessorBlock").address, ) + processor_pointers = utility.array_of_pointers( context=context, array=processor_block, count=cpu_count, - subtype=symbol_table + constants.BANG + "_KPRCB", + subtype=kernel.symbol_table_name + constants.BANG + "_KPRCB", ) + for pointer in processor_pointers: kprcb = pointer.dereference() - reloff = kernel.get_type("_KPCR").relative_child_offset("Prcb") - kpcr = context.object( - symbol_table + constants.BANG + "_KPCR", - offset=kprcb.vol.offset - reloff, - layer_name=layer_name, - ) - yield kpcr + + object_address = kprcb.vol.offset - reloff + + if not kernel_layer.is_valid(kprcb.vol.offset): + continue + + kpcr = kernel.object("_KPCR", offset=object_address, absolute=True) + + yield kpcr, kpcr.member(kpcr_member) def _generator(self) -> Iterator[Tuple]: - kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name - symbol_table = kernel.symbol_table_name - - for kpcr in self.list_kpcrs( - self.context, self.config["kernel"], layer_name, symbol_table - ): + for kpcr, current_prcb in self.list_kpcrs(self.context, self.config["kernel"]): yield ( 0, ( format_hints.Hex(kpcr.vol.offset), - format_hints.Hex(kpcr.CurrentPrcb), + format_hints.Hex(current_prcb), ), ) diff --git a/volatility3/framework/plugins/windows/ldrmodules.py b/volatility3/framework/plugins/windows/ldrmodules.py index a888f22e1..efb62f8f6 100644 --- a/volatility3/framework/plugins/windows/ldrmodules.py +++ b/volatility3/framework/plugins/windows/ldrmodules.py @@ -1,130 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging - -from volatility3.framework import constants, exceptions, interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import ldrmodules vollog = logging.getLogger(__name__) -class LdrModules(interfaces.plugins.PluginInterface): +class LdrModules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=ldrmodules.LdrModules, + removal_date="2026-06-07", +): """Lists the loaded modules in a particular windows memory image.""" _required_framework_version = (2, 0, 0) _version = (1, 0, 1) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - requirements.ListRequirement( - name="pid", - element_type=int, - description="Process IDs to include (all other processes are excluded)", - optional=True, - ), - ] - - def _generator(self, procs): - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - for proc in procs: - proc_layer_name = proc.add_process_layer() - - # Build dictionaries from different module lists, where the DllBase address is the key and value is the module object - load_order_mod = dict( - (mod.DllBase, mod) for mod in proc.load_order_modules() - ) - init_order_mod = dict( - (mod.DllBase, mod) for mod in proc.init_order_modules() - ) - mem_order_mod = dict((mod.DllBase, mod) for mod in proc.mem_order_modules()) - - # Build dictionary of mapped files, where the VAD start address is the key and value is the file name of the mapped file - mapped_files = {} - for vad in vadinfo.VadInfo.list_vads(proc): - dos_header = self.context.object( - pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset=vad.get_start(), - layer_name=proc_layer_name, - ) - try: - # Filter out VADs that do not start with a MZ header - if dos_header.e_magic != 0x5A4D: - continue - except exceptions.InvalidAddressException: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping vad at {hex(dos_header.vol.offset)} due to InvalidAddressException", - ) - continue - - mapped_files[vad.get_start()] = vad.get_file_name() - - for base in mapped_files.keys(): - # Does the base address exist in the PEB DLL lists? - load_mod = load_order_mod.get(base, None) - init_mod = init_order_mod.get(base, None) - mem_mod = mem_order_mod.get(base, None) - - yield ( - 0, - [ - int(proc.UniqueProcessId), - str( - proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors="replace", - ) - ), - format_hints.Hex(base), - load_mod is not None, - init_mod is not None, - mem_mod is not None, - mapped_files[base], - ], - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - kernel = self.context.modules[self.config["kernel"]] - - return renderers.TreeGrid( - [ - ("Pid", int), - ("Process", str), - ("Base", format_hints.Hex), - ("InLoad", bool), - ("InInit", bool), - ("InMem", bool), - ("MappedPath", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=filter_func, - ) - ), - ) diff --git a/volatility3/framework/plugins/windows/lsadump.py b/volatility3/framework/plugins/windows/lsadump.py index da8dee325..ab81f18df 100644 --- a/volatility3/framework/plugins/windows/lsadump.py +++ b/volatility3/framework/plugins/windows/lsadump.py @@ -1,226 +1,21 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from struct import unpack -from typing import Optional -from Crypto.Cipher import ARC4, DES, AES -from Crypto.Hash import MD5, SHA256 - -from volatility3.framework import interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import registry -from volatility3.framework.symbols.windows import versions -from volatility3.plugins.windows import hashdump -from volatility3.plugins.windows.registry import hivelist +from volatility3.framework import deprecation, interfaces +from volatility3.plugins.windows.registry import lsadump vollog = logging.getLogger(__name__) -class Lsadump(interfaces.plugins.PluginInterface): - """Dumps lsa secrets from memory""" +class Lsadump( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=lsadump.Lsadump, + removal_date="2026-09-25", +): + """Dumps lsa secrets from memory (deprecated)""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="hashdump", component=hashdump.Hashdump, version=(1, 1, 0) - ), - requirements.VersionRequirement( - name="hivelist", component=hivelist.HiveList, version=(1, 0, 0) - ), - ] - - @classmethod - def decrypt_aes(cls, secret: bytes, key: bytes) -> bytes: - """ - Based on code from http://lab.mediaservice.net/code/cachedump.rb - """ - sha = SHA256.new() - sha.update(key) - for _i in range(1, 1000 + 1): - sha.update(secret[28:60]) - aeskey = sha.digest() - - data = b"" - for i in range(60, len(secret), 16): - aes = AES.new(aeskey, AES.MODE_CBC, b"\x00" * 16) - buf = secret[i : i + 16] - if len(buf) < 16: - buf += (16 - len(buf)) * "\00" - data += aes.decrypt(buf) - - return data - - @classmethod - def get_lsa_key( - cls, sechive: registry.RegistryHive, bootkey: bytes, vista_or_later: bool - ) -> Optional[bytes]: - if not bootkey: - return None - - if vista_or_later: - policy_key = "PolEKList" - else: - policy_key = "PolSecretEncryptionKey" - - enc_reg_key = hashdump.Hashdump.get_hive_key(sechive, "Policy\\" + policy_key) - if not enc_reg_key: - return None - enc_reg_value = next(enc_reg_key.get_values()) - - if not enc_reg_value: - return None - - obf_lsa_key = sechive.read(enc_reg_value.Data + 4, enc_reg_value.DataLength) - - if not obf_lsa_key: - return None - if not vista_or_later: - md5 = MD5.new() - md5.update(bootkey) - for _i in range(1000): - md5.update(obf_lsa_key[60:76]) - rc4key = md5.digest() - - rc4 = ARC4.new(rc4key) - lsa_key = rc4.decrypt( - obf_lsa_key[12:60] - ) # lgtm [py/weak-cryptographic-algorithm] - lsa_key = lsa_key[0x10:0x20] - else: - lsa_key = cls.decrypt_aes(obf_lsa_key, bootkey) - lsa_key = lsa_key[68:100] - return lsa_key - - @classmethod - def get_secret_by_name( - cls, - sechive: registry.RegistryHive, - name: str, - lsakey: bytes, - is_vista_or_later: bool, - ): - enc_secret_key = hashdump.Hashdump.get_hive_key( - sechive, "Policy\\Secrets\\" + name + "\\CurrVal" - ) - - secret = None - if enc_secret_key: - enc_secret_value = next(enc_secret_key.get_values()) - if enc_secret_value: - enc_secret = sechive.read( - enc_secret_value.Data + 4, enc_secret_value.DataLength - ) - if enc_secret: - if not is_vista_or_later: - secret = cls.decrypt_secret(enc_secret[0xC:], lsakey) - else: - secret = cls.decrypt_aes(enc_secret, lsakey) - - return secret - - @classmethod - def decrypt_secret(cls, secret: bytes, key: bytes): - """Python implementation of SystemFunction005. - - Decrypts a block of data with DES using given key. - Note that key can be longer than 7 bytes.""" - decrypted_data = b"" - j = 0 # key index - - for i in range(0, len(secret), 8): - enc_block = secret[i : i + 8] - block_key = key[j : j + 7] - des_key = hashdump.Hashdump.sidbytes_to_key(block_key) - des = DES.new(des_key, DES.MODE_ECB) - enc_block = enc_block + b"\x00" * int(abs(8 - len(enc_block)) % 8) - decrypted_data += des.decrypt( - enc_block - ) # lgtm [py/weak-cryptographic-algorithm] - j += 7 - if len(key[j : j + 7]) < 7: - j = len(key[j : j + 7]) - - (dec_data_len,) = unpack(" Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: - """Generate memory regions for a process that may contain injected - code. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - kernel_layer_name: The name of the kernel layer from which to read the VAD protections - symbol_table: The name of the table containing the kernel symbols - proc: an _EPROCESS instance - - Returns: - An iterable of VAD instances and the first 64 bytes of data containing in that region - """ - proc_id = "Unknown" - try: - proc_id = proc.UniqueProcessId - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException as excp: - vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) - ) - return None - - proc_layer = context.layers[proc_layer_name] - - for vad in proc.get_vad_root().traverse(): - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - context, kernel_layer_name, symbol_table - ), - vadinfo.winnt_protections, - ) - write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string - dirty_page_check = False - - if not write_exec: - """ - # Inspect "PAGE_EXECUTE_READ" VAD pages to detect - # non writable memory regions having been injected - # using elevated WriteProcessMemory(). - """ - if "EXECUTE" in protection_string: - for page in range( - vad.get_start(), vad.get_end(), proc_layer.page_size - ): - try: - # If we have a dirty page in a non writable "EXECUTE" region, it is suspicious. - if proc_layer.is_dirty(page): - dirty_page_check = True - break - except exceptions.InvalidAddressException: - # Abort as it is likely that other addresses in the same range will also fail. - break - if not dirty_page_check: - continue - else: - continue - - if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( - vad.get_private_memory() == 0 - and protection_string != "PAGE_EXECUTE_WRITECOPY" - ): - if cls.is_vad_empty(proc_layer, vad): - continue - - if dirty_page_check: - # Useful information to investigate the page content with volshell afterwards. - vollog.warning( - f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(page)}", - ) - data = proc_layer.read(vad.get_start(), 64, pad=True) - yield vad, data - - def _generator(self, procs): - # determine if we're on a 32 or 64 bit kernel - kernel = self.context.modules[self.config["kernel"]] - - # set refined criteria to know when to add to "Notes" column - refined_criteria = { - b"MZ": "MZ header", - b"\x55\x8b": "PE header", - b"\x55\x48": "Function prologue", - b"\x55\x89": "Function prologue", - } - - is_32bit_arch = not symbols.symbol_table_is_64bit( - self.context, kernel.symbol_table_name - ) - - for proc in procs: - # by default, "Notes" column will be set to N/A - process_name = utility.array_to_string(proc.ImageFileName) - - for vad, data in self.list_injections( - self.context, kernel.layer_name, kernel.symbol_table_name, proc - ): - notes = renderers.NotApplicableValue() - # Check for unique headers and update "Notes" column if criteria is met - if data[0:2] in refined_criteria: - notes = refined_criteria[data[0:2]] - - # if we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 - if is_32bit_arch or proc.get_is_wow64(): - architecture = "intel" - else: - architecture = "intel64" - - disasm = interfaces.renderers.Disassembly( - data, vad.get_start(), architecture - ) - - file_output = "Disabled" - if self.config["dump"]: - file_output = "Error outputting to file" - try: - file_handle = vadinfo.VadInfo.vad_dump( - self.context, proc, vad, self.open - ) - file_handle.close() - file_output = file_handle.preferred_filename - except (exceptions.InvalidAddressException, OverflowError) as excp: - vollog.debug( - "Unable to dump PE with pid {0}.{1:#x}: {2}".format( - proc.UniqueProcessId, vad.get_start(), excp - ) - ) - - yield ( - 0, - ( - proc.UniqueProcessId, - process_name, - format_hints.Hex(vad.get_start()), - format_hints.Hex(vad.get_end()), - vad.get_tag(), - vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, - kernel.layer_name, - kernel.symbol_table_name, - ), - vadinfo.winnt_protections, - ), - vad.get_commit_charge(), - vad.get_private_memory(), - file_output, - notes, - format_hints.HexBytes(data), - disasm, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - kernel = self.context.modules[self.config["kernel"]] - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Start VPN", format_hints.Hex), - ("End VPN", format_hints.Hex), - ("Tag", str), - ("Protection", str), - ("CommitCharge", int), - ("PrivateMemory", int), - ("File output", str), - ("Notes", str), - ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=filter_func, - ) - ), - ) + _required_framework_version = (2, 22, 0) + _version = (1, 1, 0) diff --git a/volatility3/framework/plugins/windows/malware/__init__.py b/volatility3/framework/plugins/windows/malware/__init__.py new file mode 100644 index 000000000..2e2fec739 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/__init__.py @@ -0,0 +1,8 @@ +# 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 +# +"""All core windows malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/windows/malware/direct_system_calls.py b/volatility3/framework/plugins/windows/malware/direct_system_calls.py new file mode 100644 index 000000000..f6b9e53bb --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/direct_system_calls.py @@ -0,0 +1,475 @@ +# 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 +# + +import logging + +from collections import namedtuple +from typing import List, Tuple, Optional, Generator, Callable + +from volatility3.framework.objects import utility +from volatility3.framework import interfaces, renderers, symbols, exceptions +from volatility3.framework.configuration import requirements +from volatility3.plugins import yarascan +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +syscall_finder_type = namedtuple( + "syscall_finder_type", + [ + "get_syscall_target_address", + "wants_syscall_inst", + "rule_str", + "invalid_ops", + "termination_ops", + ], +) + +syscall_finder_type.__doc__ = """ +This type is used to specify how malicious system call invocations should be found. + +`get_syscall_target_address` is optionally used to extract the address containing the malicious 'syscall' instruction +`wants_syscall_inst` whether or not this method expects the 'syscall' instruction directly within the malicious code block +`rule` the opcode string to search for the malicious syscall instructions +`invalid_ops` instructions that only appear in invalid code blocks. Stops processing of the code block when encountered. +`termination_ops` instructions that are expected to be present in the code block and that stop processing +""" + + +class DirectSystemCalls(interfaces.plugins.PluginInterface): + """Detects the Direct System Call technique used to bypass EDRs""" + + _required_framework_version = (2, 4, 0) + + # 2.0.0 - changes signature of `get_tasks_to_scan` + _version = (2, 0, 0) + + # DLLs that are expected to host system call invocations + valid_syscall_handlers = ("ntdll.dll", "win32u.dll") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.syscall_finder = syscall_finder_type( + # for direct system calls, we find the `syscall` instruction directly, so we already know the address + None, + # yes, we want the syscall instruction present as it is what this technique looks for + True, + # regex to find "\x0f\x05" (syscall) followed later by "\xc3" (ret) + # we allow spacing in between to break naive anti-analysis forms (e.g., TarTarus Gate) + # Standard techniques, such as HellsGate, look like: + # mov r10, rcx + # mov eax, + # syscall + # ret + "/\\x0f\\x05[^\\xc3]{,24}\\xc3/", + # any of these will not be in a workable, malicious direct system call block + ["jmp", "call", "leave", "int3"], + # the expected form is to end with a "ret" back to the calling code + ["ret"], + ) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # create a list of requirements for vadyarascan + vadyarascan_requirements = [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), + ] + + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vadyarascan_requirements + + @staticmethod + def _is_syscall_block( + disasm_func: Callable, + syscall_finder: syscall_finder_type, + data: bytes, + address: int, + ) -> Optional[Tuple[str, "capstone._cs_insn"]]: + """ + Determines if the bytes starting at `data` represent a valid syscall instruction invocation block + + To maliciously invoke the system call instruction, malware must do each of the following: + + 1) update RAX to the system call number + 2) update R10 to the first parameter + 3) hit the 'termination' instruction set in `syscall_finder_type` + + We also track whether the 'syscall' instruction was encountered while parsing + + This function is reusable for every technique we found and studied during the DEFCON research timeframe + + Args: + disasm_func: capstone disassembly function gathered from `get_disasm_function` + syscall_finder: the method and constraints on the malicious system call blocks that the calling plugin knows how to find + data: the bytes from memory to search for malicious syscall invocations + address: the address from where `data` came from in the particular process + Returns: + Optional[Tuple[str, capstone._cs_insn]]: For valid blocks, the disassembled bytes in string from and the last (termination) instruction + """ + found_movr10 = False + found_movreax = False + found_syscall = False + found_end = False + end_inst = None + + disasm_bytes = "" + + for inst in disasm_func(data, address): + disasm_bytes += f"{inst.address:#x}: {inst.mnemonic} {inst.op_str}; " + + # an instruction of all 0x00 opcodes + if inst.opcode.count(0) == len(inst.opcode): + break + + op = inst.mnemonic + + # invalid op, bail + if op in syscall_finder.invalid_ops: + break + + # found the end instruction wanted by the caller + elif op in syscall_finder.termination_ops: + found_end = True + end_inst = inst + break + + # track this no matter what to make code more re-usable + elif op == "syscall": + found_syscall = True + + # if we hit a 'syscall' but RAX or R10 haven't been touched + # then we are in an invalid path, so bail + if not syscall_finder.wants_syscall_inst or ( + not (found_movr10 and found_movreax) + ): + break + + else: + # attempt to see if any other instruction type wrote to registers + try: + _, regs_written = inst.regs_access() + except capstone.CsError: + continue + + if regs_written: + for r in regs_written: + # track writes to eax/rax or R10 + reg = inst.reg_name(r) + if reg in ["eax", "rax"]: + found_movreax = True + + elif reg == "r10": + found_movr10 = True + + # if any of these are missing, the block is invalid regardless of + # the technique we are trying to detect now or in the future + if not (found_movr10 and found_movreax and found_end): + return None + + # if the finder requires a 'syscall' instruction then bail now if we didn't find one + if syscall_finder.wants_syscall_inst and not found_syscall: + return None + + return disasm_bytes, end_inst + + @classmethod + def get_disasm_function(cls, architecture: str) -> Callable: + """ + Returns the disassembly handler for the given architecture + .detail is used to get full instruction information + + Args: + architecture: the name of the architecture for the process being disassembled + Returns: + The disasm function from capstone for the given architecture + """ + disasm_types = { + "intel": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32), + "intel64": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64), + } + + disasm_type = disasm_types[architecture] + disasm_type.detail = True + return disasm_type.disasm + + @classmethod + def _is_valid_syscall( + cls, + syscall_finder: syscall_finder_type, + proc_layer: interfaces.layers.DataLayerInterface, + architecture: str, + vads: List[Tuple[int, int, str]], + address: int, + ) -> Optional[Tuple[int, str]]: + """ + Args: + syscall_finder: + proc_layer: the memory layer of the process being scanned + architecture: the name of the architecture for the process being disassembled + vads: the ranges of this process under 10MB + address: the starting address to check for malicious syscall code blocks + + Returns: + Optional[Tuple[int, str]]: For valid code blocks, the starting address of the block and the disassembly string + """ + # the number bytes behind the yara rule hit to scan + behind = 32 + + address = address - behind + + try: + data = proc_layer.read(address, behind * 2) + except exceptions.InvalidAddressException: + return None + + disasm_func = cls.get_disasm_function(architecture) + + # since Intel does not have fixed-size instructions, we have to scan + # each byte offset and re-disassemble the remaining block + for offset in range(behind): + # if this looks like a system call back (r10, rax, ret/jmp) + syscall_info = cls._is_syscall_block( + disasm_func, syscall_finder, data[offset:], address + offset + ) + if syscall_info: + disasm_bytes, end_inst = syscall_info + + # if we can recover (and require) a target address for this malware technique + if syscall_finder.get_syscall_target_address: + target_address = syscall_finder.get_syscall_target_address( + proc_layer, end_inst + ) + + # could not determine the address -> invalid basic block + if not target_address: + continue + + # we only care about calls to system call DLLs + path = cls.get_range_path(vads, target_address) + if not isinstance(path, str) or not path.lower().endswith( + cls.valid_syscall_handlers + ): + continue + + # return the address and disassembly string if all checks pass + return address + offset, disasm_bytes + + return None + + @classmethod + def get_vad_maps( + cls, + task: interfaces.objects.ObjectInterface, + ) -> List[Tuple[int, int, str]]: + """Creates a map of start/end addresses within a virtual address + descriptor tree. + + Args: + task: The EPROCESS object of which to traverse the vad tree + + Returns: + An iterable of tuples containing start and end addresses for each descriptor + """ + vads: List[Tuple[int, int, str]] = [] + + # scan regions under 10MB + scan_max = 10 * 1000 * 1000 + + vad_root = task.get_vad_root() + + for vad in vad_root.traverse(): + if vad.get_size() < scan_max: + vads.append((vad.get_start(), vad.get_size(), vad.get_file_name())) + + return vads + + @classmethod + def get_range_path( + cls, ranges: List[Tuple[int, int, str]], address: int + ) -> Optional[str]: + """ + Returns the path for the range holding `address`, if found + + Args: + ranges: VADs collected from `get_vad_maps` + address: the address to find + Returns: + The path holding the address, if any + """ + for start, size, path in ranges: + if start <= address < start + size: + return path + + return None + + @classmethod + def get_tasks_to_scan( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ) -> Generator[ + Tuple[interfaces.objects.ObjectInterface, str, str, str], None, None + ]: + """ + Gathers active processes with the extra information needed + to detect malicious syscall instructions + + Returns: + Generator of the process object, name, memory layer, and architecture + """ + + # gather active processes + filter_func = pslist.PsList.create_active_process_filter() + + kernel = context.modules[kernel_module_name] + + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) + + for proc in pslist.PsList.list_processes( + context=context, + kernel_module_name=kernel_module_name, + filter_func=filter_func, + ): + proc_name = utility.array_to_string(proc.ImageFileName) + + # skip Defender + if proc_name in ["MsMpEng.exe"]: + continue + + try: + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + if is_32bit_arch or proc.get_is_wow64(): + architecture = "intel" + else: + architecture = "intel64" + + yield proc, proc_name, proc_layer_name, architecture + + @classmethod + def _get_rule_hits( + cls, + context: interfaces.objects.ObjectInterface, + proc_layer: interfaces.layers.DataLayerInterface, + vads: List[Tuple[int, int, str]], + pattern: str, + ) -> Generator[Tuple[int, Optional[str]], None, None]: + """ + Runs the given opcode rule through Yara and returns the address and file path of hits + + Args: + context: + proc_layer: the layer to scan + vads: the ranges inside of the process being scanned + pattern: the opcodes rule from the plugin to detect a particular EDR-bypass technique + + Returns: + Generator of the address and file path of hits + """ + sections = [(vad[0], vad[1]) for vad in vads] + + rule = yarascan.YaraScanner.get_rule(pattern) + + for hit in proc_layer.scan( + context=context, + scanner=yarascan.YaraScanner(rules=rule), + sections=sections, + ): + address = hit[0] + + path = cls.get_range_path(vads, address) + + # ignore hits in the system call DLLs + if isinstance(path, str) and path.lower().endswith( + cls.valid_syscall_handlers + ): + continue + + yield address, path + + def _generator( + self, + ) -> Generator[Tuple[int, Tuple[str, int, Optional[str], int, str]], None, None]: + if not has_capstone: + vollog.warning( + "capstone is not installed. This plugin requires capstone to operate." + ) + return + + for proc, proc_name, proc_layer_name, architecture in self.get_tasks_to_scan( + self.context, self.config["kernel"] + ): + proc_layer = self.context.layers[proc_layer_name] + + vads = self.get_vad_maps(proc) + if not vads: + continue + + # for each valid process, look for malicious syscall invocations + for address, vad_path in self._get_rule_hits( + self.context, proc_layer, vads, self.syscall_finder.rule_str + ): + syscall_info = self._is_valid_syscall( + self.syscall_finder, proc_layer, architecture, vads, address + ) + if not syscall_info: + continue + + address, disasm_bytes = syscall_info + + yield ( + 0, + ( + proc_name, + proc.UniqueProcessId, + vad_path, + format_hints.Hex(address), + disasm_bytes, + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("Range", str), + ("Address", format_hints.Hex), + ("Disasm", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/drivermodule.py b/volatility3/framework/plugins/windows/malware/drivermodule.py new file mode 100644 index 000000000..c31fe2500 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/drivermodule.py @@ -0,0 +1,101 @@ +# 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 +# +from typing import Iterator, List, Tuple +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import ssdt, driverscan, modules + +# built in Windows-components that trigger false positives +KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] + + +class DriverModule(interfaces.plugins.PluginInterface): + """Determines if any loaded drivers were hidden by a rootkit""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + ] + + def _generator(self) -> Iterator[Tuple]: + """ + Attempt to match each driver's start code address to a known kernel module + A common rootkit technique is to register drivers from modules that are hidden, + which allows us to detect the disconnect between a malicious driver and its hidden module. + """ + collection = ssdt.SSDT.build_module_collection( + context=self.context, + kernel_module_name=self.config["kernel"], + ) + + kernel_space_start = modules.Modules.get_kernel_space_start( + self.context, self.config["kernel"] + ) + + for driver in driverscan.DriverScan.scan_drivers( + self.context, + self.config["kernel"], + ): + # We want starts of 0 as rootkits often set this value + # greater than 0 but less than the kernel space start is smear/terminated though + if 0 < driver.DriverStart < kernel_space_start: + continue + + # we do not care about actual symbol names, we just want to know if the driver points to a known module + module_symbols = list( + collection.get_module_symbols_by_absolute_location(driver.DriverStart) + ) + if not module_symbols: + ( + driver_name, + service_key, + name, + ) = driverscan.DriverScan.get_names_for_driver(driver) + + # drivers without any names will not produce useful output + if not driver_name and not service_key and not name: + continue + + known_exception = driver_name in KNOWN_DRIVERS + + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + known_exception, + driver_name or renderers.NotAvailableValue(), + service_key or renderers.NotAvailableValue(), + name or renderers.NotAvailableValue(), + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Known Exception", bool), + ("Driver Name", str), + ("Service Key", str), + ("Alternative Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/hollowprocesses.py b/volatility3/framework/plugins/windows/malware/hollowprocesses.py new file mode 100644 index 000000000..1ea46b540 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/hollowprocesses.py @@ -0,0 +1,226 @@ +# 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 +# +import logging +from typing import NamedTuple, Dict, Generator + +from volatility3.framework import interfaces, exceptions, constants +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class VadData(NamedTuple): + protection: str + path: str + + +class DLLData(NamedTuple): + path: str + + +### Useful references on process hollowing +# https://cysinfo.com/detecting-deceptive-hollowing-techniques/ +# https://github.com/m0n0ph1/Process-Hollowing + + +class HollowProcesses(interfaces.plugins.PluginInterface): + """Lists hollowed processes""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + ] + + def _get_vads_data( + self, proc: interfaces.objects.ObjectInterface + ) -> Dict[int, VadData]: + """ + Returns a dictionary of: + base address -> (protection string, file name) + For each mapped VAD in the process. This is used + for quick lookups of data and matching the DLL + at the same base address as the VAD + """ + vads = {} + + kernel = self.context.modules[self.config["kernel"]] + + for vad in proc.get_vad_root().traverse(): + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, kernel.layer_name, kernel.symbol_table_name + ), + vadinfo.winnt_protections, + ) + + fn = vad.get_file_name() + if not fn or not isinstance(fn, str): + fn = "" + + vads[vad.get_start()] = VadData(protection_string, fn) + + return vads + + def _get_dlls_map( + self, proc: interfaces.objects.ObjectInterface + ) -> Dict[int, DLLData]: + """ + Returns a dictionary of: + base address -> path + for each DLL loaded in the process + + This is used to cross compare with + the corresponding VAD and to have a + backup path source in case of smear + in the VAD + """ + dlls = {} + + for entry in proc.load_order_modules(): + try: + base = entry.DllBase + except exceptions.InvalidAddressException: + continue + + try: + FullDllName = entry.FullDllName.get_string() + except exceptions.InvalidAddressException: + FullDllName = renderers.UnreadableValue() + + dlls[base] = DLLData(FullDllName) + + return dlls + + def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int: + """ + Uses the PEB to get the image base of the process + """ + kernel = self.context.modules[self.config["kernel"]] + + try: + proc_layer_name = proc.add_process_layer() + peb = self.context.object( + kernel.symbol_table_name + constants.BANG + "_PEB", + layer_name=proc_layer_name, + offset=proc.Peb, + ) + return peb.ImageBaseAddress + except exceptions.InvalidAddressException: + return None + + def _check_load_address(self, proc, _, __) -> Generator[str, None, None]: + """ + Detects when the image base in the PEB, which is writable by process malware, + does not match the section base address - whose value lives in kernel memory. + Many malware samples will manipulate their image base to fool AVs/EDRs and + as a necessary part of certain hollowing techniques + """ + image_base = self._get_image_base(proc) + if image_base is not None and image_base != proc.SectionBaseAddress: + yield f"The ImageBaseAddress reported from the PEB ({image_base:#x}) does not match the process SectionBaseAddress ({proc.SectionBaseAddress:#x})" + + def _check_exe_protection( + self, proc, vads: Dict[int, VadData], __ + ) -> Generator[str, None, None]: + """ + Legitimately mapped application executables and DLLs + will have a VAD present and its initial protection will be + PAGE_EXECUTE_WRITECOPY. + Many process hollowing and code injection techniques will + unmap the real executable and/or map in executables with + incorrect permissions. + This check verifies the VAD for the application exe. + `_check_dlls_protection` checks for DLLs mapped in the process. + """ + base = proc.SectionBaseAddress + + if base not in vads: + yield f"There is no VAD starting at the base address of the process executable ({base:#x})" + elif vads[base].protection != "PAGE_EXECUTE_WRITECOPY": + yield f"Unexpected protection ({vads[base].protection}) for VAD hosting the process executable ({base:#x}) with path {vads[base].path}" + + def _check_dlls_protection( + self, _, vads: Dict[int, VadData], dlls: Dict[int, DLLData] + ) -> Generator[str, None, None]: + for dll_base in dlls: + # could be malicious but triggers too many FPs from smear + if dll_base not in vads: + continue + + # PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files + if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY": + yield f"Unexpected protection ({vads[dll_base].protection}) for DLL in the PEB's load order list ({dll_base:#x}) with path {dlls[dll_base].path}" + + def _generator(self, procs): + checks = [ + self._check_load_address, + self._check_exe_protection, + self._check_dlls_protection, + ] + + for proc in procs: + # smear and/or terminated process + dlls = self._get_dlls_map(proc) + if len(dlls) < 3: + continue + + vads = self._get_vads_data(proc) + if len(vads) < 5: + continue + + proc_name = utility.array_to_string(proc.ImageFileName) + pid = proc.UniqueProcessId + + for check in checks: + for note in check(proc, vads, dlls): + yield ( + 0, + ( + pid, + proc_name, + note, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Notes", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/indirect_system_calls.py b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py new file mode 100644 index 000000000..ba34eb110 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py @@ -0,0 +1,121 @@ +# 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 +# + +import struct +import logging +from typing import List, Optional + +from volatility3.framework import interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.plugins import yarascan +from volatility3.plugins.windows.malware import direct_system_calls + +vollog = logging.getLogger(__name__) + + +class IndirectSystemCalls(direct_system_calls.DirectSystemCalls): + """Detects the Indirect System Call technique used to bypass EDRs.""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.syscall_finder = direct_system_calls.syscall_finder_type( + # gets the target address of a indirect jmp + self._indirect_syscall_block_target, + # we are looking for indirect system calls, so we don't want 'syscall' instructions in our code block + False, + # jmp [address]; ret + "/\\xff\\x25[^\\xc3]{,24}\\xc3/", + # any of these mean we aren't in a malicious indirect call + ["call", "leave", "int3", "ret"], + # stop at jmp, this should reference the system call instruction + ["jmp"], + ) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # create a list of requirements for vadyarascan + vadyarascan_requirements = [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="direct_system_calls", + component=direct_system_calls.DirectSystemCalls, + version=(2, 0, 0), + ), + ] + + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vadyarascan_requirements + + @staticmethod + def _indirect_syscall_block_target( + proc_layer: interfaces.layers.DataLayerInterface, inst + ) -> Optional[int]: + """ + This function determines the address of a jmp in the following form: + + jmp [address] + + To determine this, we must: + 1) Pull the 4 byte relative offset of 'address' inside the instruction + 2) Compute the full address of this relative offset + 3) Read from the address as it is being dereferenced + 4) Ensure the target address points to a 'syscall' instruction + + Args: + proc_layer: the layer of the potential syscall block + inst: the terminating instruction of the syscall block check + Returns: + The target address of the jump if it can be computed + """ + + try: + jmp_address_str = proc_layer.read(inst.address, 6) + except exceptions.InvalidAddressException: + return None + + # Should be an jmp... + if jmp_address_str[0:2] != b"\xff\x25": + return None + + # get the address of the 'jmp [address]' instruction + relative_offset = struct.unpack(" Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: + 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 + ), + ) + + @classmethod + def list_injection_sites( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + symbol_table: str, + proc: interfaces.objects.ObjectInterface, + ) -> Generator[ + Tuple[interfaces.objects.ObjectInterface, renderers.LayerData], + None, + None, + ]: + """Generate memory regions for a process that may contain injected + code. + + Args: + context: The context from which to retrieve required elements (layers, symbol tables) + kernel_layer_name: The name of the kernel layer from which to read the VAD protections + symbol_table: The name of the table containing the kernel symbols + proc: an _EPROCESS instance + + Returns: + An iterable of VAD instances and the first 64 bytes of data contained in that region + """ + proc_id = "Unknown" + try: + proc_id = proc.UniqueProcessId + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException as excp: + vollog.debug( + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" + ) + return None + + proc_layer = context.layers[proc_layer_name] + + for vad in proc.get_vad_root().traverse(): + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + context, kernel_layer_name, symbol_table + ), + vadinfo.winnt_protections, + ) + write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string + dirty_page = None + if not write_exec: + """ + # Inspect "PAGE_EXECUTE_READ" VAD pages to detect + # non-writable memory regions having been injected + # using elevated WriteProcessMemory(). + """ + if "EXECUTE" in protection_string: + for page in range( + vad.get_start(), vad.get_end(), proc_layer.page_size + ): + try: + # If we have a dirty page in a non-writable "EXECUTE" region, it is suspicious. + if proc_layer.is_dirty(page): + dirty_page = page + break + except exceptions.InvalidAddressException: + # Abort as it is likely that other addresses in the same range will also fail. + break + if dirty_page is None: + continue + else: + continue + + if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( + vad.get_private_memory() == 0 + and protection_string != "PAGE_EXECUTE_WRITECOPY" + ): + if cls.is_vad_empty(proc_layer, vad): + continue + + if dirty_page is not None: + # Useful information to investigate the page content with volshell afterwards. + vollog.debug( + f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}", + ) + start = vad.get_start() + length = 64 + data = renderers.LayerData( + context=context, + layer_name=proc_layer_name, + offset=start, + length=length, + no_surrounding=True, + ) + yield (vad, data) + + def _generator(self, procs): + # Determine if we're on a 32 or 64 bit kernel + kernel = self.context.modules[self.config["kernel"]] + + # Set refined criteria to know when to add to "Notes" column + refined_criteria = { + b"MZ": "MZ header", + b"\x55\x8b": "PE header", + b"\x55\x48": "Function prologue", + b"\x55\x89": "Function prologue", + } + + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ) + + for proc in procs: + # By default, "Notes" column will be set to N/A + process_name = utility.array_to_string(proc.ImageFileName) + + for vad, data_object in self.list_injection_sites( + self.context, kernel.layer_name, kernel.symbol_table_name, proc + ): + notes = renderers.NotApplicableValue() + # Check for unique headers and update "Notes" column if criteria is met + data = data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length, True + ) + if data[:2] in refined_criteria: + notes = refined_criteria[data[:2]] + + # If we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 + if is_32bit_arch or proc.get_is_wow64(): + architecture = "intel" + else: + architecture = "intel64" + + disasm = renderers.Disassembly(data, vad.get_start(), architecture) + + file_output = "Disabled" + if self.config["dump"]: + file_output = "Error outputting to file" + try: + file_handle = vadinfo.VadInfo.vad_dump( + self.context, proc, vad, self.open + ) + file_handle.close() + file_output = file_handle.preferred_filename + except (exceptions.InvalidAddressException, OverflowError) as excp: + vollog.debug( + f"Unable to dump PE with pid {proc.UniqueProcessId}.{vad.get_start():#x}: {excp}" + ) + + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(vad.get_start()), + format_hints.Hex(vad.get_end()), + vad.get_tag(), + vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + ), + vadinfo.winnt_protections, + ), + vad.get_commit_charge(), + vad.get_private_memory(), + file_output, + notes, + data_object, + disasm, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start VPN", format_hints.Hex), + ("End VPN", format_hints.Hex), + ("Tag", str), + ("Protection", str), + ("CommitCharge", int), + ("PrivateMemory", int), + ("File output", str), + ("Notes", str), + ("Hexdump", renderers.LayerData), + ("Disasm", renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) 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 new file mode 100644 index 000000000..fb8ec527d --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/processghosting.py @@ -0,0 +1,226 @@ +# 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 +# +import logging + +from typing import Optional, Tuple, Generator, Dict + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class ProcessGhosting(interfaces.plugins.PluginInterface): + """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose""" + + _version = (1, 0, 0) + _required_framework_version = (2, 4, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 1) + ), + ] + + @classmethod + def _process_checks( + cls, + proc: interfaces.objects.ObjectInterface, + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], + ) -> Generator[ + Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None + ]: + """ + Checks the EPROCESS for signs of ghosting + """ + if not proc.has_member("ImageFilePointer"): + return + + delete_pending = None + + # if it is 0 then its a side effect of process ghosting + if proc.ImageFilePointer.vol.offset != 0: + try: + file_object = proc.ImageFilePointer + delete_pending = file_object.DeletePending + file_object = file_object.dereference().vol.offset + except exceptions.InvalidAddressException: + file_object = 0 + + # ImageFilePointer equal to 0 means process ghosting or similar techniques were used + else: + file_object = 0 + + # delete_pending besides 0 or 1 = smear + if isinstance(delete_pending, int) and delete_pending not in [0, 1]: + vollog.debug( + f"Invalid delete_pending value {delete_pending} found for process {proc.UniqueProcessId}" + ) + delete_pending = None + + if file_object == 0 or delete_pending == 1: + yield file_object, delete_pending, None, proc.SectionBaseAddress + + @classmethod + def _vad_checks( + cls, control_area: interfaces.objects.ObjectInterface, vad_path: str + ) -> Generator[Tuple[int, Optional[int], Optional[int]], None, None]: + """ + Checks the control area for delete on close or delete pending being set + """ + try: + file_object = control_area.FilePointer.dereference().cast("_FILE_OBJECT") + except exceptions.InvalidAddressException: + return + + try: + delete_on_close = control_area.u.Flags.DeleteOnClose + except exceptions.InvalidAddressException: + delete_on_close = None + + if delete_on_close and vad_path.lower().endswith((".exe", ".dll")): + yield file_object.vol.offset, None, delete_on_close + + try: + delete_pending = file_object.DeletePending + except exceptions.InvalidAddressException: + delete_pending = None + + if delete_pending == 1: + yield file_object.vol.offset, delete_pending, None + + @classmethod + def check_for_ghosting( + cls, + proc: interfaces.objects.ObjectInterface, + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], + ) -> Generator[ + Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None + ]: + """ + Returns process or vad info for ghosting files + + Args: + proc: + mapped_files: A dictionary mapping vad base addresses to the path and vad instance for the process + + Return: + A Generator of tuples of the file object address, the delete pending state, delete on close state, base address of the VAD, and the path + """ + # check the direct file object of the process + yield from cls._process_checks(proc, mapped_files) + + # walk each vad, check if it is pending delete or has its delete on close bit set + for vad_base, (path, vad) in mapped_files.items(): + # these checks have no meaning for private memory areas + if vad.get_private_memory() == 1: + continue + + try: + if vad.has_member("ControlArea"): + control_area = vad.ControlArea + elif vad.has_member("Subsection"): + control_area = vad.Subsection.ControlArea + # We got here from a short vad, likely smear + else: + continue + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to get control area for vad at base {vad_base:#x} for process with pid {proc.UniqueProcessId}" + ) + continue + + 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, + ) + + def _generator(self, procs): + kernel = self.context.modules[self.config["kernel"]] + + has_imagefilepointer = kernel.get_type("_EPROCESS").has_member( + "ImageFilePointer" + ) + if not has_imagefilepointer: + vollog.warning( + "ImageFilePointer checks are only supported on Windows 10+ builds when the ImageFilePointer member of _EPROCESS is present" + ) + + for proc in procs: + process_name = utility.array_to_string(proc.ImageFileName) + pid = proc.UniqueProcessId + + # base address -> (file path, VAD instance) + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]] = {} + for vad in vadinfo.VadInfo.list_vads(proc): + path = vad.get_file_name() + if isinstance(path, str): + mapped_files[vad.get_start()] = (path, vad) + + for ( + file_object_address, + delete_pending, + delete_on_close, + base_address, + ) in self.check_for_ghosting(proc, mapped_files): + vad_info = mapped_files.get(base_address) + if vad_info: + path = vad_info[0] + 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, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_active_process_filter() + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Base", format_hints.Hex), + ("FILE_OBJECT", format_hints.Hex), + ("DeletePending", int), + ("DeleteOnClose", int), + ("Path", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/psxview.py b/volatility3/framework/plugins/windows/malware/psxview.py new file mode 100644 index 000000000..51616a22c --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/psxview.py @@ -0,0 +1,241 @@ +import datetime +import logging +import string +from itertools import chain +from typing import Dict, Iterable, List + +from volatility3.framework import constants, exceptions, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows import extensions +from volatility3.plugins.windows import handles, pslist, psscan, thrdscan + +vollog = logging.getLogger(__name__) + + +class PsXView(plugins.PluginInterface): + """Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \ +identify processes that are trying to hide themselves. + +We recommend using -r pretty if you are looking at this plugin's output in a terminal.""" + + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality + # which the original plugin used to do it. + + # The sessions method is omitted because it begins with the list of processes found by Pslist anyway. + + # Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the + # code I do have from it, and will happily share it if anyone else wants to add it. + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + valid_proc_name_chars = set( + string.ascii_lowercase + string.ascii_uppercase + "." + " " + ) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="psscan", component=psscan.PsScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(4, 0, 0) + ), + requirements.BooleanRequirement( + name="physical-offsets", + description="List processes with physical offsets instead of virtual offsets.", + optional=True, + ), + ] + + def _proc_name_to_string(self, proc): + return proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ) + + def _is_valid_proc_name(self, string: str) -> bool: + return all(c in self.valid_proc_name_chars for c in string) + + def _filter_garbage_procs( + self, proc_list: Iterable[extensions.EPROCESS] + ) -> List[extensions.EPROCESS]: + return [ + p + for p in proc_list + if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) + ] + + def _translate_offset(self, offset: int) -> int: + if not self.config["physical-offsets"]: + return offset + + kernel = self.context.modules[self.config["kernel"]] + layer_name = kernel.layer_name + + try: + _original_offset, _original_length, offset, _length, _layer_name = list( + self.context.layers[layer_name].mapping(offset=offset, length=0) + )[0] + except exceptions.PagedInvalidAddressException: + vollog.debug(f"Page fault: unable to translate {offset:0x}") + + return offset + + def _proc_list_to_dict( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + tasks = self._filter_garbage_procs(tasks) + return {self._translate_offset(proc.vol.offset): proc for proc in tasks} + + def _check_pslist(self, tasks): + return self._proc_list_to_dict(tasks) + + def _check_psscan( + self, + ) -> Dict[int, extensions.EPROCESS]: + res = psscan.PsScan.scan_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + return self._proc_list_to_dict(res) + + def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: + ret = [] + + for ethread in thrdscan.ThrdScan.scan_threads( + self.context, module_name="kernel" + ): + process = None + try: + process = ethread.owning_process() + if not process.is_valid(): + continue + + ret.append(process) + except AttributeError: + vollog.log( + constants.LOGLEVEL_VVV, + "Unable to find the owning process of ethread", + ) + + return self._proc_list_to_dict(ret) + + def _check_csrss_handles( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + ret: List[extensions.EPROCESS] = [] + + type_map = handles.Handles.get_type_map( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + cookie = handles.Handles.find_cookie( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + for p in tasks: + name = self._proc_name_to_string(p) + if name != "csrss.exe": + continue + + try: + ret += [ + handle.Body.cast("_EPROCESS") + for handle in handles.Handles.handles( + context=self.context, + kernel_module_name=self.config["kernel"], + handle_table=p.ObjectTable, + ) + if handle.get_object_type(type_map, cookie) == "Process" + ] + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVV, "Cannot access eprocess object table" + ) + + return self._proc_list_to_dict(ret) + + def _generator(self): + kdbg_list_processes = list( + pslist.PsList.list_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + ) + + # get processes from each source + processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} + + processes["pslist"] = self._check_pslist(kdbg_list_processes) + processes["psscan"] = self._check_psscan() + processes["thrdscan"] = self._check_thrdscan() + processes["csrss"] = self._check_csrss_handles(kdbg_list_processes) + + # Unique set of all offsets from all sources + offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) + + for offset in offsets: + # We know there will be at least one process mapped to each offset + proc: extensions.EPROCESS = next( + mapping[offset] for mapping in processes.values() if offset in mapping + ) + + in_sources = {src: False for src in processes} + + for source, process_mapping in processes.items(): + if offset in process_mapping: + in_sources[source] = True + + pid = proc.UniqueProcessId + name = self._proc_name_to_string(proc) + + exit_time = proc.get_exit_time() + if type(exit_time) is not datetime.datetime: + exit_time = "" + else: + exit_time = str(exit_time) + + yield ( + 0, + ( + format_hints.Hex(offset), + name, + pid, + in_sources["pslist"], + in_sources["psscan"], + in_sources["thrdscan"], + in_sources["csrss"], + exit_time, + ), + ) + + def run(self): + offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" + offset_str = "Offset" + offset_type + + return renderers.TreeGrid( + [ + (offset_str, format_hints.Hex), + ("Name", str), + ("PID", int), + ("pslist", bool), + ("psscan", bool), + ("thrdscan", bool), + ("csrss", bool), + ("Exit Time", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py new file mode 100644 index 000000000..10c6222bc --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py @@ -0,0 +1,689 @@ +# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# This module attempts to locate skeleton-key like function hooks. +# It does this by locating the CSystems array through a variety of methods, +# and then validating the entry for RC4 HMAC (0x17 / 23) +# +# For a thorough walkthrough on how the R&D was performed to develop this plugin, +# please see our blogpost here: +# +# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html + +import logging +from typing import Iterable, Tuple, List, Optional + +import pefile + +from volatility3.framework import interfaces, symbols, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import scanners +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows import pdbutil +from volatility3.framework.symbols.windows.extensions import pe +from volatility3.plugins.windows import pslist, vadinfo, pe_symbols + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +vollog = logging.getLogger(__name__) + + +class Skeleton_Key_Check(interfaces.plugins.PluginInterface): + """Looks for signs of Skeleton Key malware""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), + ] + + def _check_for_skeleton_key_vad( + self, + csystem: interfaces.objects.ObjectInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> bool: + """ + Checks if Initialize and/or Decrypt is hooked by determining if + these function pointers reference addresses inside of the cryptdll VAD + + Args: + csystem: The RC4HMAC KERB_ECRYPT instance + cryptdll_base: Base address of the cryptdll.dll VAD + cryptdll_size: Size of the VAD + Returns: + bool: if a skeleton key hook is present + """ + return not ( + (cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) + and (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size) + ) + + def _check_for_skeleton_key_symbols( + self, + csystem: interfaces.objects.ObjectInterface, + rc4HmacInitialize: int, + rc4HmacDecrypt: int, + ) -> bool: + """ + Uses the PDB information to specifically check if the csystem for RC4HMAC + has an initialization pointer to rc4HmacInitialize and a decryption pointer + to rc4HmacDecrypt. + + Args: + csystem: The RC4HMAC KERB_ECRYPT instance + rc4HmacInitialize: The expected address of csystem Initialization function + rc4HmacDecrypt: The expected address of the csystem Decryption function + + Returns: + bool: if a skeleton key hook was found + """ + return ( + csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt + ) + + def _construct_ecrypt_array( + self, + array_start: int, + count: int, + cryptdll_types: interfaces.context.ModuleInterface, + ) -> interfaces.context.ModuleInterface: + """ + Attempts to construct an array of _KERB_ECRYPT structures + + Args: + array_start: starting virtual address of the array + count: how many elements are in the array + cryptdll_types: the reverse engineered types + + Returns: + The instantiated array + """ + + try: + array = cryptdll_types.object( + object_type="array", + offset=array_start, + subtype=cryptdll_types.get_type("_KERB_ECRYPT"), + count=count, + absolute=True, + ) + + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to construct cSystems array at given offset: {array_start:x}" + ) + array = None + + return array + + def _find_array_with_pdb_symbols( + self, + cryptdll_symbols: str, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + cryptdll_base: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: + """ + Finds the CSystems array through use of PDB symbols + + Args: + cryptdll_symbols: The symbols table from the PDB file + cryptdll_types: The types from cryptdll binary analysis + proc_layer_name: The lsass.exe process layer name + cryptdll_base: Base address of cryptdll.dll inside of lsass.exe + + Returns: + Tuple of: + array: The cSystems array + rc4HmacInitialize: The runtime address of the expected initialization function + rc4HmacDecrypt: The runtime address of the expected decryption function + """ + cryptdll_module = self.context.module( + cryptdll_symbols, layer_name=proc_layer_name, offset=cryptdll_base + ) + + rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address( + "rc4HmacInitialize" + ) + + rc4HmacDecrypt = cryptdll_module.get_absolute_symbol_address("rc4HmacDecrypt") + + count_address = cryptdll_module.get_symbol("cCSystems").address + + # we do not want to fail just because the count is not in memory + # 16 was the size on samples I tested, so I chose it as the default + try: + count = cryptdll_types.object( + object_type="unsigned long", offset=count_address + ) + except exceptions.InvalidAddressException: + count = 16 + + array_start = cryptdll_module.get_absolute_symbol_address("CSystems") + + array = self._construct_ecrypt_array(array_start, count, cryptdll_types) + + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB based analysis." + ) + + return array, rc4HmacInitialize, rc4HmacDecrypt + + def _get_cryptdll_types( + self, + context: interfaces.context.ContextInterface, + config, + config_path: str, + proc_layer_name: str, + cryptdll_base: int, + ): + """ + Builds a symbol table from the cryptdll types generated after binary analysis + + Args: + context: the context to operate upon + config: + config_path: + proc_layer_name: name of the lsass.exe process layer + cryptdll_base: base address of cryptdll.dll inside of lsass.exe + """ + kernel = self.context.modules[self.config["kernel"]] + table_mapping = {"nt_symbols": kernel.symbol_table_name} + + cryptdll_symbol_table = intermed.IntermediateSymbolTable.create( + context=context, + config_path=config_path, + sub_path="windows", + filename="kerb_ecrypt", + table_mapping=table_mapping, + ) + + return context.module( + cryptdll_symbol_table, proc_layer_name, offset=cryptdll_base + ) + + def _find_lsass_proc( + self, proc_list: Iterable + ) -> Tuple[interfaces.context.ContextInterface, str]: + """ + Walks the process list and returns the first valid lsass instances. + There should be only one lsass process, but malware will often use the + process name to try and blend in. + + Args: + proc_list: The process list generator + + Return: + The process object for lsass + """ + + for proc in proc_list: + try: + proc_layer_name = proc.add_process_layer() + + return proc, proc_layer_name + + except exceptions.InvalidAddressException as excp: + vollog.debug( + f"Invalid address {excp.invalid_address} in layer {excp.layer_name}" + ) + + return None, None + + def _find_cryptdll( + self, lsass_proc: interfaces.context.ContextInterface + ) -> Tuple[int, int]: + """ + Finds the base address of cryptdll.dll inside of lsass.exe + + Args: + lsass_proc: the process object for lsass.exe + + Returns: + A tuple of: + cryptdll_base: the base address of cryptdll.dll + crytpdll_size: the size of the VAD for cryptdll.dll + """ + for vad in lsass_proc.get_vad_root().traverse(): + filename = vad.get_file_name() + + if isinstance(filename, str) and filename.lower().endswith("cryptdll.dll"): + base = vad.get_start() + return base, vad.get_size() + + return None, None + + def _find_csystems_with_symbols( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int]: + """ + Attempts to find CSystems and the expected address of the handlers. + Relies on downloading and parsing of the cryptdll PDB file. + + Args: + proc_layer_name: the name of the lsass.exe process layer + cryptdll_types: The types from cryptdll binary analysis + cryptdll_base: the base address of cryptdll.dll + crytpdll_size: the size of the VAD for cryptdll.dll + + Returns: + A tuple of: + array: An initialized Volatility array of _KERB_ECRYPT structures + rc4HmacInitialize: The expected address of csystem Initialization function + rc4HmacDecrypt: The expected address of the csystem Decryption function + """ + try: + cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( + self.context, + interfaces.configuration.path_join(self.config_path, "cryptdll"), + proc_layer_name, + "cryptdll.pdb", + cryptdll_base, + cryptdll_size, + ) + except exceptions.VolatilityException: + vollog.debug( + "Unable to use the cryptdll PDB. Stopping PDB symbols based analysis." + ) + return None, None, None + + array, rc4HmacInitialize, rc4HmacDecrypt = self._find_array_with_pdb_symbols( + cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base + ) + + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB symbols based analysis." + ) + + return array, rc4HmacInitialize, rc4HmacDecrypt + + def _get_rip_relative_target(self, inst) -> int: + """ + Returns the target address of a RIP-relative instruction. + + These instructions contain the offset of a target address + relative to the current instruction pointer. + + Args: + inst: A capstone instruction instance + + Returns: + None or the target address of the instruction + """ + try: + opnd = inst.operands[1] + except capstone.CsError: + return None + + if opnd.type != capstone.x86.X86_OP_MEM: + return None + + if inst.reg_name(opnd.mem.base) != "rip": + return None + + return inst.address + inst.size + opnd.mem.disp + + def _analyze_cdlocatecsystem( + self, + function_bytes: bytes, + function_start: int, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + ) -> Optional[interfaces.objects.ObjectInterface]: + """ + Performs static analysis on CDLocateCSystem to find the instructions that + reference CSystems as well as cCsystems + + Args: + function_bytes: the instruction bytes of CDLocateCSystem + function_start: the address of CDLocateCSystem + proc_layer_name: the name of the lsass.exe process layer + + Return: + The cSystems array of ecrypt instances + """ + found_count = False + array_start = None + count = None + + ## we only support 64bit disassembly analysis + md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) + md.detail = True + + for inst in md.disasm(function_bytes, function_start): + # we should not reach debug traps + if inst.mnemonic == "int3": + break + + # cCsystems is referenced by a mov instruction + elif inst.mnemonic == "mov": + if not found_count: + target_address = self._get_rip_relative_target(inst) + + # we do not want to fail just because the count is not in memory + # 16 was the size on samples I tested, so I chose it as the default + count = 16 + + if target_address: + try: + count = int.from_bytes( + self.context.layers[proc_layer_name].read( + target_address, 4 + ), + "little", + ) + except exceptions.InvalidAddressException: + vollog.debug( + "Unable to read `cCsystems`. Defaulting to 16." + ) + + found_count = True + + elif inst.mnemonic == "lea": + target_address = self._get_rip_relative_target(inst) + + if target_address: + array_start = target_address + + # we find the count before, so we can terminate the static analysis here + break + + if array_start and count: + array = self._construct_ecrypt_array(array_start, count, cryptdll_types) + else: + array = None + + return array + + def _find_csystems_with_export( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + _, + ) -> Optional[interfaces.objects.ObjectInterface]: + """ + Uses export table analysis to locate CDLocateCsystem + This function references CSystems and cCsystems + + Args: + proc_layer_name: The lsass.exe process layer name + cryptdll_types: The types from cryptdll binary analysis + cryptdll_base: Base address of cryptdll.dll inside of lsass.exe + _: unused in this source + Returns: + The cSystems array + """ + + if not has_capstone: + vollog.debug( + "capstone is not installed so cannot fall back to export table analysis." + ) + return None + + vollog.debug( + "Unable to perform analysis using PDB symbols, falling back to export table analysis." + ) + + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + + cryptdll = pe_symbols.PESymbols.get_pefile_obj( + self.context, pe_table_name, proc_layer_name, cryptdll_base + ) + if not cryptdll: + return None + + cryptdll.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + if not hasattr(cryptdll, "DIRECTORY_ENTRY_EXPORT"): + return None + + # find the location of CDLocateCSystem and then perform static analysis + for export in cryptdll.DIRECTORY_ENTRY_EXPORT.symbols: + if export.name != b"CDLocateCSystem": + continue + + function_start = cryptdll_base + export.address + + try: + function_bytes = self.context.layers[proc_layer_name].read( + function_start, 0x50 + ) + except exceptions.InvalidAddressException: + vollog.debug( + "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis." + ) + break + + array = self._analyze_cdlocatecsystem( + function_bytes, function_start, cryptdll_types, proc_layer_name + ) + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping export based analysis." + ) + + return array + + return None + + def _find_csystems_with_scanning( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> List[interfaces.context.ModuleInterface]: + """ + Performs scanning to find potential RC4 HMAC csystem instances + + This function may return several values as it cannot validate which is the active one + + Args: + proc_layer_name: the lsass.exe process layer name + cryptdll_types: the types from cryptdll binary analysis + cryptdll_base: base address of cryptdll.dll inside of lsass.exe + cryptdll_size: size of the VAD + Returns: + A list of csystem instances + """ + + csystems = [] + + cryptdll_end = cryptdll_base + cryptdll_size + + proc_layer = self.context.layers[proc_layer_name] + + ecrypt_size = cryptdll_types.get_type("_KERB_ECRYPT").size + + # scan for potential instances of RC4 HMAC + # the signature is based on the type being 0x17 + # and the block size member being 1 in all test samples + for address in proc_layer.scan( + self.context, + scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), + sections=[(cryptdll_base, cryptdll_size)], + ): + # this occurs across page boundaries + if not proc_layer.is_valid(address, ecrypt_size): + continue + + kerb = cryptdll_types.object("_KERB_ECRYPT", offset=address, absolute=True) + + # ensure the Encrypt and Finish pointers are inside the VAD + # these are not manipulated in the attack + if (cryptdll_base < kerb.Encrypt < cryptdll_end) and ( + cryptdll_base < kerb.Finish < cryptdll_end + ): + csystems.append(kerb) + + return csystems + + def _generator(self, procs): + """ + Finds instances of the RC4 HMAC CSystem structure + + Returns whether the instances are hooked as well as the function handler addresses + + Args: + procs: the process list filtered to lsass.exe instances + """ + kernel = self.context.modules[self.config["kernel"]] + + if not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ): + vollog.info("This plugin only supports 64bit Windows memory samples") + return None + + lsass_proc, proc_layer_name = self._find_lsass_proc(procs) + if not lsass_proc: + vollog.info( + "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed." + ) + return None + + cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc) + if not cryptdll_base: + vollog.info( + "Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed." + ) + return None + + # the custom type information from binary analysis + cryptdll_types = self._get_cryptdll_types( + self.context, self.config, self.config_path, proc_layer_name, cryptdll_base + ) + + # attempt to find the array and symbols directly from the PDB + csystems, rc4HmacInitialize, rc4HmacDecrypt = self._find_csystems_with_symbols( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) + + # if we can't find cSystems through the PDB then + # we fall back to export analysis and scanning + # we keep the address of the rc4 functions from the PDB + # though as its our only source to get them + if csystems is None: + fallback_sources = [ + self._find_csystems_with_export, + self._find_csystems_with_scanning, + ] + + for source in fallback_sources: + csystems = source( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) + + if csystems is not None: + break + + if csystems is None: + vollog.info( + "Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed." + ) + return None + + for csystem in csystems: + if not self.context.layers[proc_layer_name].is_valid( + csystem.vol.offset, csystem.vol.size + ): + continue + + # filter for RC4 HMAC + if csystem.EncryptionType != 0x17: + continue + + # use the specific symbols if present, otherwise use the vad start and size + if rc4HmacInitialize and rc4HmacDecrypt: + skeleton_key_present = self._check_for_skeleton_key_symbols( + csystem, rc4HmacInitialize, rc4HmacDecrypt + ) + else: + skeleton_key_present = self._check_for_skeleton_key_vad( + 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), + ), + ) + + def _lsass_proc_filter(self, proc): + """ + Used to filter to only lsass.exe processes + + There should only be one of these, but malware can/does make lsass.exe + named processes to blend in or uses lsass.exe as a process hollowing target + """ + process_name = utility.array_to_string(proc.ImageFileName) + + return process_name != "lsass.exe" + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Skeleton Key Found", bool), + ("rc4HmacInitialize", format_hints.Hex), + ("rc4HmacDecrypt", format_hints.Hex), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=self._lsass_proc_filter, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/suspicious_threads.py b/volatility3/framework/plugins/windows/malware/suspicious_threads.py new file mode 100644 index 000000000..803a0b04b --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/suspicious_threads.py @@ -0,0 +1,224 @@ +# 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 +# + +import logging +from typing import List, Dict, Tuple, Generator +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan + +vollog = logging.getLogger(__name__) + + +class SuspiciousThreads(interfaces.plugins.PluginInterface): + """Lists suspicious userland process threads""" + + _required_framework_version = (2, 4, 0) + _version = (2, 0, 1) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="threads", component=threads.Threads, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + ] + + def _get_ranges( + self, + kernel: interfaces.context.ModuleInterface, + all_ranges: Dict[int, List[Tuple[int, int, str, str]]], + proc, + ) -> Tuple[int, int, str, str]: + """ + Maintains a hash table so each process' VADs + are only enumerated once per plugin run + """ + key = proc.vol.offset + + if key not in all_ranges: + all_ranges[key] = [] + + for vad in proc.get_vad_root().traverse(): + fn = vad.get_file_name() + if not isinstance(fn, str) or not fn: + fn = None + + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, kernel.layer_name, kernel.symbol_table_name + ), + vadinfo.winnt_protections, + ) + + all_ranges[key].append( + (vad.get_start(), vad.get_end(), protection_string, fn) + ) + + return all_ranges[key] + + def _get_range( + self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int + ) -> Tuple[int, str, str]: + """ + Walks a process' VADs looking for the one + containing `address` + + Returns its base address, protection string, and mapped file, if any + """ + for start, end, protection_string, fn in ranges: + if start <= address < end: + return start, protection_string, fn + + return None, None, None + + def _check_thread_address( + self, exe_path: str, ranges, thread_address: int + ) -> Generator[Tuple[str, str], None, None]: + vad_base, prot, vad_path = self._get_range(ranges, thread_address) + + # threads outside of a VAD means either smear from this thread or this process' VAD tree + if vad_base is None: + return + + if vad_path is None: + # set this so checks after report the non file backed region in the path column + vad_path = "" + + yield ( + vad_path, + f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", + ) + + # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions + if prot != "PAGE_EXECUTE_WRITECOPY": + yield ( + vad_path, + f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", + ) + + # check for process hollowing type techniques that mapped in a second, malicious exe file + if ( + exe_path + and vad_path.lower().endswith(".exe") + and (vad_path.lower() != exe_path.lower()) + ): + yield ( + vad_path, + "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process executable", + ) + + def _enumerate_processes( + self, kernel: interfaces.context.ModuleInterface, all_ranges + ): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ): + ranges = self._get_ranges(kernel, all_ranges, proc) + + # smeared vads or process is terminating + if len(all_ranges[proc.vol.offset]) < 5: + continue + + pid = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + + _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) + if not isinstance(exe_path, str): + exe_path = None + + yield proc, pid, proc_name, exe_path, ranges + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + all_ranges = {} + + for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( + kernel, all_ranges + ): + # processes often create multiple threads at the same address + # there is no benefit to checking the same address more than once per process + checked = set() + + for thread in threads.Threads.list_threads( + self.context, self.config["kernel"], proc + ): + # do not process if a thread is exited or terminated (4 = Terminated) + if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: + continue + + # bail if accessing the threads members causes a page fault + info = thrdscan.ThrdScan.gather_thread_info(thread) + if not info: + continue + + _, _, tid, start_address, _, win32_start_address, _, _, _ = info + + addresses = [ + (start_address, "Start"), + (win32_start_address, "Win32Start"), + ] + + for address, context in addresses: + if address in checked: + continue + checked.add(address) + + 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, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("TID", int), + ("Context", str), + ("Address", format_hints.Hex), + ("VAD Path", str), + ("Note", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/svcdiff.py b/volatility3/framework/plugins/windows/malware/svcdiff.py new file mode 100644 index 000000000..78b61eb67 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/svcdiff.py @@ -0,0 +1,102 @@ +# 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 +# +# This module compares services found through list walking versus scanning, +# with the aim of finding hidden services. +# +# For background of hidden services and a real-world example of the use of this plugin, +# please see our blogpost: +# +# https://volatilityfoundation.org/memory-forensics-rd-illustrated-detecting-hidden-windows-services/ + +import logging + +from volatility3.framework import symbols, interfaces +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import svclist, svcscan +from volatility3.framework.symbols.windows import versions + +vollog = logging.getLogger(__name__) + + +class SvcDiff(svcscan.SvcScan): + """Compares services found through list walking versus scanning to find rootkits""" + + _required_framework_version = (2, 4, 0) + + _version = (2, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._enumeration_method = self.service_diff + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="svclist", component=svclist.SvcList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="svcscan", component=svcscan.SvcScan, version=(4, 0, 0) + ), + ] + + @classmethod + def service_diff( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + service_table_name: str, + service_binary_dll_map, + filter_func, + ): + """ + On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list + and scan for services then report differences + """ + kernel = context.modules[kernel_module_name] + + if not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) or not versions.is_win10_15063_or_later( + context=context, symbol_table=kernel.symbol_table_name + ): + vollog.warning( + "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" + ) + return + + from_scan = set() + from_list = set() + records = {} + + # collect unique service names from scanning + for service in svcscan.SvcScan.service_scan( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_scan.add(service[6]) + records[service[6]] = service + + # collect services from listing walking + for service in svclist.SvcList.service_list( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_list.add(service[6]) + + # report services found from scanning but not list walking + for hidden_service in from_scan - from_list: + yield records[hidden_service] diff --git a/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py new file mode 100644 index 000000000..71dc20021 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py @@ -0,0 +1,202 @@ +# 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 + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +import logging + +from typing import Dict, Tuple, List, Generator + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + + +class UnhookedSystemCalls(interfaces.plugins.PluginInterface): + """Detects hooked ntdll.dll stub functions in Windows processes.""" + + _required_framework_version = (2, 4, 0) + _version = (2, 0, 0) + + system_calls = { + "ntdll.dll": { + pe_symbols.wanted_names_identifier: [ + "NtCreateThread", + "NtProtectVirtualMemory", + "NtReadVirtualMemory", + "NtOpenProcess", + "NtWriteFile", + "NtQueryVirtualMemory", + "NtAllocateVirtualMemory", + "NtWorkerFactoryWorkerReady", + "NtAcceptConnectPort", + "NtAddDriverEntry", + "NtAdjustPrivilegesToken", + "NtAlpcCreatePort", + "NtClose", + "NtCreateFile", + "NtCreateMutant", + "NtOpenFile", + "NtOpenIoCompletion", + "NtOpenJobObject", + "NtOpenKey", + "NtOpenKeyEx", + "NtOpenThread", + "NtOpenThreadToken", + "NtOpenThreadTokenEx", + "NtWriteVirtualMemory", + "NtTraceEvent", + "NtTranslateFilePath", + "NtUmsThreadYield", + "NtUnloadDriver", + "NtUnloadKey", + "NtUnloadKey2", + "NtUnloadKeyEx", + "NtCreateKey", + "NtCreateSection", + "NtDeleteKey", + "NtDeleteValueKey", + "NtDuplicateObject", + "NtQueryValueKey", + "NtReplaceKey", + "NtRequestWaitReplyPort", + "NtRestoreKey", + "NtSetContextThread", + "NtSetSecurityObject", + "NtSetValueKey", + "NtSystemDebugControl", + "NtTerminateProcess", + ] + } + } + + # This data structure is used to track unique implementations of functions across processes + # The outer dictionary holds the module name (e.g., ntdll.dll) + # The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module + # The innermost dictionary holds the unique implementation (bytes) of a function across processes + # Each implementation is tracked along with the process(es) that host it + # For systems without malware, all functions should have the same implementation + # When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations + _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] + + @classmethod + def get_requirements(cls) -> List: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + ] + + def _gather_code_bytes( + self, + kernel_module_name: str, + found_symbols: pe_symbols.found_symbols_type, + ) -> _code_bytes_type: + """ + Enumerates the desired DLLs and function implementations in each process + Groups based on unique implementations of each DLLs' functions + The purpose is to detect when a function has different implementations (code) + in different processes. + This very effectively detects code injection. + """ + code_bytes: UnhookedSystemCalls._code_bytes_type = {} + + procs = pslist.PsList.list_processes(self.context, kernel_module_name) + + for proc in procs: + try: + proc_id = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + for dll_name, functions in found_symbols.items(): + for func_name, func_addr in functions: + try: + fbytes = self.context.layers[proc_layer_name].read( + func_addr, 0x20 + ) + except exceptions.InvalidAddressException: + continue + + # see the definition of _code_bytes_type for details of this data structure + if dll_name not in code_bytes: + code_bytes[dll_name] = {} + + if func_name not in code_bytes[dll_name]: + code_bytes[dll_name][func_name] = {} + + if fbytes not in code_bytes[dll_name][func_name]: + code_bytes[dll_name][func_name][fbytes] = [] + + code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name)) + + return code_bytes + + def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: + found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( + context=self.context, + config_path=self.config_path, + kernel_module_name=self.config["kernel"], + symbols=UnhookedSystemCalls.system_calls, + ) + + # code_bytes[dll_name][func_name][func_bytes] + code_bytes = self._gather_code_bytes(self.config["kernel"], found_symbols) + + # walk the functions that were evaluated + for functions in code_bytes.values(): + # cbb is the distinct groups of bytes (instructions) + # for this function across processes + for func_name, cbb in functions.items(): + # the dict key here is the raw instructions, which is not helpful to look at + # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) + cb = list(cbb.values()) + + # if all processes map to the same implementation, then no malware is present + if len(cb) == 1: + yield 0, (func_name, "", len(cb[0])) + else: + # if there are differing implementations then it means + # that malware has overwritten system call(s) in infected processes + # max_idx and small_idx find which implementation of a system call has the least processes + # as all observed malware and open source projects only infected a few targets, leaving the + # rest with the original EDR hooks in place + max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 + small_idx = (~max_idx) & 1 + + ps = [] + + # gather processes on small_idx since these are the malware infected ones + for pid, pname in cb[small_idx]: + ps.append(f"{pid:d}:{pname}") + + proc_names = ", ".join(ps) + + yield 0, (func_name, proc_names, len(cb[max_idx])) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Function", str), + ("Distinct Implementations", str), + ("Total Implementations", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index 272120b3a..86b78ecf5 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -20,8 +20,8 @@ vollog = logging.getLogger(__name__) class MBRScan(interfaces.plugins.PluginInterface): """Scans for and parses potential Master Boot Records (MBRs)""" - _required_framework_version = (2, 0, 1) - _version = (1, 0, 0) + _required_framework_version = (2, 22, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -37,6 +37,11 @@ class MBRScan(interfaces.plugins.PluginInterface): default=False, optional=True, ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] @classmethod @@ -53,7 +58,9 @@ class MBRScan(interfaces.plugins.PluginInterface): layer = self.context.layers[physical_layer_name] architecture = ( "intel" - if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + if not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ) else "intel64" ) @@ -118,9 +125,7 @@ class MBRScan(interfaces.plugins.PluginInterface): renderers.NotApplicableValue(), renderers.NotApplicableValue(), renderers.NotApplicableValue(), - interfaces.renderers.Disassembly( - bootcode, 0, architecture - ), + renderers.Disassembly(bootcode, 0, architecture), ), ) else: @@ -144,10 +149,14 @@ class MBRScan(interfaces.plugins.PluginInterface): renderers.NotApplicableValue(), renderers.NotApplicableValue(), renderers.NotApplicableValue(), - interfaces.renderers.Disassembly( - bootcode, 0, architecture + renderers.Disassembly(bootcode, 0, architecture), + renderers.LayerData( + context=self.context, + layer_name=layer.name, + offset=mbr_start_offset, + length=bootcode_length, + no_surrounding=True, ), - format_hints.HexBytes(bootcode), ), ) @@ -230,7 +239,7 @@ class MBRScan(interfaces.plugins.PluginInterface): ("Bootable", bool), ("PartitionType", str), ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly), + ("Disasm", renderers.Disassembly), ], self._generator(), ) @@ -254,8 +263,8 @@ class MBRScan(interfaces.plugins.PluginInterface): ("EndingCHS", int), ("EndingSector", int), ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly), - ("Bootcode", format_hints.HexBytes), + ("Disasm", renderers.Disassembly), + ("Bootcode", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/windows/memmap.py b/volatility3/framework/plugins/windows/memmap.py index b5c9a211e..af4564259 100644 --- a/volatility3/framework/plugins/windows/memmap.py +++ b/volatility3/framework/plugins/windows/memmap.py @@ -27,8 +27,8 @@ class Memmap(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.IntRequirement( name="pid", @@ -53,9 +53,7 @@ class Memmap(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - pid, excp.invalid_address, excp.layer_name - ) + f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) continue @@ -80,11 +78,7 @@ class Memmap(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: file_output = "Error outputting to file" vollog.debug( - "Unable to write {}'s address {} to {}".format( - proc_layer_name, - offset, - file_handle.preferred_filename, - ) + f"Unable to write {proc_layer_name}'s address {offset} to {file_handle.preferred_filename}" ) yield ( @@ -103,7 +97,6 @@ class Memmap(interfaces.plugins.PluginInterface): def run(self): filter_func = pslist.PsList.create_pid_filter([self.config.get("pid", None)]) - kernel = self.context.modules[self.config["kernel"]] return renderers.TreeGrid( [ @@ -116,8 +109,7 @@ class Memmap(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 9e6585345..2dfecb93b 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -1,11 +1,11 @@ # This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import contextlib import datetime import logging +from typing import Iterator, NamedTuple, Optional, Tuple, Union -from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework import constants, exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed @@ -18,7 +18,23 @@ vollog = logging.getLogger(__name__) class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Scans for MFT FILE objects present in a particular windows memory image.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 26, 0) + + _version = (3, 0, 0) + + class MFTScanResult(NamedTuple): + offset: format_hints.Hex + record_type: str + record_number: objects.Integer + link_count: objects.Integer + mft_type: str + permissions: Union[str, interfaces.renderers.BaseAbsentValue] + attribute_type: str + created: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] + modified: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] + updated: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] + accessed: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] + filename: Union[interfaces.renderers.BaseAbsentValue, objects.String] @classmethod def get_requirements(cls): @@ -28,13 +44,43 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): description="Memory layer for the kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), requirements.VersionRequirement( name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), ] - def _generator(self): - layer = self.context.layers[self.config["primary"]] + @classmethod + def enumerate_mft_records( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + primary_layer_name: str, + ) -> Iterator[mft.MFTEntry]: + try: + primary = context.layers[primary_layer_name] + except KeyError: + vollog.error( + "Unable to obtain primary layer for scanning. Please file a bug on GitHub about this issue." + ) + return + + try: + memory_layer_name = primary.config["memory_layer"] + except KeyError: + vollog.error( + "Unable to obtain memory layer from primary layer. Please file a bug on GitHub about this issue." + ) + return + + layer = context.layers[memory_layer_name] # Yara Rule to scan for MFT Header Signatures rules = yarascan.YaraScan.process_yara_options( @@ -42,117 +88,178 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) # Read in the Symbol File - symbol_table = intermed.IntermediateSymbolTable.create( - context=self.context, - config_path=self.config_path, + symbol_table_name = intermed.IntermediateSymbolTable.create( + context=context, + config_path=config_path, sub_path="windows", filename="mft", - class_types={"FILE_NAME_ENTRY": mft.MFTFileName, "MFT_ENTRY": mft.MFTEntry}, + class_types={ + "FILE_NAME_ENTRY": mft.MFTFileName, + "MFT_ENTRY": mft.MFTEntry, + "ATTRIBUTE": mft.MFTAttribute, + }, ) # get each of the individual Field Sets - mft_object = symbol_table + constants.BANG + "MFT_ENTRY" - attribute_object = symbol_table + constants.BANG + "ATTRIBUTE" - si_object = symbol_table + constants.BANG + "STANDARD_INFORMATION_ENTRY" - fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY" + + mft_object_type_name = symbol_table_name + constants.BANG + "MFT_ENTRY" # Scan the layer for Raw MFT records and parse the fields for offset, _rule_name, _name, _value in layer.scan( - context=self.context, scanner=yarascan.YaraScanner(rules=rules) + context=context, scanner=yarascan.YaraScanner(rules=rules) ): - with contextlib.suppress(exceptions.PagedInvalidAddressException): - mft_record = self.context.object( - mft_object, offset=offset, layer_name=layer.name + mft_record: mft.MFTEntry = context.object( + mft_object_type_name, + offset=offset, + layer_name=layer.name, + ) + + yield mft_record + + @classmethod + def parse_standard_information_records( + cls, mft_record: mft.MFTEntry + ) -> Iterator[Tuple[int, MFTScanResult]]: + # MFT Flags determine the file type or dir + # If we don't have a valid enum, coerce to hex so we can keep the record + try: + mft_flag = mft_record.Flags.lookup() + except ValueError: + mft_flag = hex(mft_record.Flags) + + # Standard Information Attribute + try: + # 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(), + ), ) - # We will update this on each pass in the next loop and use it as the new offset. - attr_base_offset = mft_record.FirstAttrOffset - attr = self.context.object( - attribute_object, - offset=offset + attr_base_offset, - layer_name=layer.name, + except exceptions.InvalidAddressException: + pass + + @classmethod + def parse_filename_records( + cls, mft_record: mft.MFTEntry + ) -> Iterator[Tuple[int, MFTScanResult]]: + # MFT Flags determine the file type or dir + # If we don't have a valid enum, coerce to hex so we can keep the record + try: + mft_flag = mft_record.Flags.lookup() + except ValueError: + mft_flag = hex(mft_record.Flags) + + # 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(), + ), ) + except exceptions.InvalidAddressException: + return - # There is no field that has a count of Attributes - # Keep Attempting to read attributes until we get an invalid attr_header.AttrType + @classmethod + def parse_mft_records( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + primary_layer_name: str, + ) -> Iterator[Tuple[int, MFTScanResult]]: + for mft_record in cls.enumerate_mft_records( + context=context, + config_path=config_path, + primary_layer_name=primary_layer_name, + ): + yield from cls.parse_standard_information_records(mft_record) + yield from cls.parse_filename_records(mft_record) - while attr.Attr_Header.AttrType.is_valid_choice: - vollog.debug(f"Attr Type: {attr.Attr_Header.AttrType.lookup()}") - - # MFT Flags determine the file type or dir - # If we don't have a valid enum, coerce to hex so we can keep the record - try: - mft_flag = mft_record.Flags.lookup() - except ValueError: - mft_flag = hex(mft_record.Flags) - - # Standard Information Attribute - if attr.Attr_Header.AttrType.lookup() == "STANDARD_INFORMATION": - attr_data = attr.Attr_Data.cast(si_object) - yield 0, ( - format_hints.Hex(attr_data.vol.offset), - mft_record.get_signature(), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - renderers.NotApplicableValue(), - attr.Attr_Header.AttrType.lookup(), - conversion.wintime_to_datetime(attr_data.CreationTime), - conversion.wintime_to_datetime(attr_data.ModifiedTime), - conversion.wintime_to_datetime(attr_data.UpdatedTime), - conversion.wintime_to_datetime(attr_data.AccessedTime), - renderers.NotApplicableValue(), - ) - - # File Name Attribute - if attr.Attr_Header.AttrType.lookup() == "FILE_NAME": - attr_data = attr.Attr_Data.cast(fn_object) - file_name = attr_data.get_full_name() - - # If we don't have a valid enum, coerce to hex so we can keep the record - try: - permissions = attr_data.Flags.lookup() - except ValueError: - permissions = hex(attr_data.Flags) - - yield 1, ( - format_hints.Hex(attr_data.vol.offset), - mft_record.get_signature(), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - permissions, - attr.Attr_Header.AttrType.lookup(), - conversion.wintime_to_datetime(attr_data.CreationTime), - conversion.wintime_to_datetime(attr_data.ModifiedTime), - conversion.wintime_to_datetime(attr_data.UpdatedTime), - conversion.wintime_to_datetime(attr_data.AccessedTime), - file_name, - ) - - # If there's no advancement the loop will never end, so break it now - if attr.Attr_Header.Length == 0: - break - - # Update the base offset to point to the next attribute - attr_base_offset += attr.Attr_Header.Length - attr = self.context.object( - attribute_object, - offset=offset + attr_base_offset, - layer_name=layer.name, - ) + def _generator(self): + for level, record in self.parse_mft_records( + self.context, + self.config_path, + self.config["primary"], + ): + # Convert all `objects.PrimitiveObject` to their simpler Python + # types. This is normally not something we would do, since it's + # lossy and prevents users from getting back to the data source, + # 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, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), + ), + ) def generate_timeline(self): - for row in self._generator(): - _depth, row_data = row + for record in self.enumerate_mft_records( + self.context, self.config_path, self.config["primary"] + ): + fname = record.longest_filename() - # Only Output FN Records - if row_data[6] == "FILE_NAME": - filename = row_data[-1] - description = f"MFT FILE_NAME entry for {filename}" - yield (description, timeliner.TimeLinerType.CREATED, row_data[7]) - yield (description, timeliner.TimeLinerType.MODIFIED, row_data[8]) - yield (description, timeliner.TimeLinerType.CHANGED, row_data[9]) - yield (description, timeliner.TimeLinerType.ACCESSED, row_data[10]) + for _, item in self.parse_standard_information_records(record): + description = f"MFT {item.attribute_type} entry for {fname}" + yield (description, timeliner.TimeLinerType.CREATED, item.created) + yield (description, timeliner.TimeLinerType.MODIFIED, item.modified) + yield (description, timeliner.TimeLinerType.CHANGED, item.updated) + yield (description, timeliner.TimeLinerType.ACCESSED, item.accessed) + + for _, item in self.parse_filename_records(record): + description = f"MFT {item.attribute_type} entry for {item.filename}" + yield (description, timeliner.TimeLinerType.CREATED, item.created) + yield (description, timeliner.TimeLinerType.MODIFIED, item.modified) + yield (description, timeliner.TimeLinerType.CHANGED, item.updated) + yield (description, timeliner.TimeLinerType.ACCESSED, item.accessed) def run(self): return renderers.TreeGrid( @@ -177,11 +284,25 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): class ADS(interfaces.plugins.PluginInterface): """Scans for Alternate Data Stream""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 26, 0) + + _version = (2, 0, 0) + + class ADSResult(NamedTuple): + offset: format_hints.Hex + signature: objects.String + record_number: objects.Integer + attribute_type: str + filename: Union[objects.String, interfaces.renderers.BaseAbsentValue] + stream_name: Union[objects.String, interfaces.renderers.BaseAbsentValue] + content: Union[renderers.LayerData, interfaces.renderers.BaseAbsentValue] @classmethod def get_requirements(cls): return [ + requirements.VersionRequirement( + name="MFTScan", component=MFTScan, version=(3, 0, 0) + ), requirements.TranslationLayerRequirement( name="primary", description="Memory layer for the kernel", @@ -192,108 +313,65 @@ class ADS(interfaces.plugins.PluginInterface): ), ] + @classmethod + def parse_ads_data_records(cls, mft_record: mft.MFTEntry) -> Iterator[ADSResult]: + for data_attr in mft_record.alternate_data_streams(): + record_filename = ( + mft_record.longest_filename() or renderers.NotAvailableValue() + ) + content_obj = data_attr.get_resident_filecontent() + content = ( + renderers.LayerData.from_object(content_obj) + if content_obj + else renderers.NotAvailableValue() + ) + ads_filename = ( + data_attr.get_resident_filename() or renderers.NotAvailableValue() + ) + + yield cls.ADSResult( + format_hints.Hex(data_attr.Attr_Data.vol.offset), + mft_record.get_signature(), + mft_record.RecordNumber, + data_attr.Attr_Header.AttrType.lookup(), + record_filename, + ads_filename, + content, + ) + def _generator(self): - layer = self.context.layers[self.config["primary"]] - - # Yara Rule to scan for MFT Header Signatures - rules = yarascan.YaraScan.process_yara_options( - {"yara_string": "/FILE0|FILE\\*|BAAD/"} - ) - - # Read in the Symbol File - symbol_table = intermed.IntermediateSymbolTable.create( - context=self.context, - config_path=self.config_path, - sub_path="windows", - filename="mft", - class_types={ - "MFT_ENTRY": mft.MFTEntry, - "FILE_NAME_ENTRY": mft.MFTFileName, - "ATTRIBUTE": mft.MFTAttribute, - }, - ) - - # get each of the individual Field Sets - mft_object = symbol_table + constants.BANG + "MFT_ENTRY" - attribute_object = symbol_table + constants.BANG + "ATTRIBUTE" - fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY" - - # Scan the layer for Raw MFT records and parse the fields - for offset, _rule_name, _name, _value in layer.scan( - context=self.context, scanner=yarascan.YaraScanner(rules=rules) + for mft_entry in MFTScan.enumerate_mft_records( + self.context, + self.config_path, + self.config["primary"], ): - with contextlib.suppress(exceptions.PagedInvalidAddressException): - mft_record = self.context.object( - mft_object, offset=offset, layer_name=layer.name + for record in self.parse_ads_data_records(mft_entry): + # Convert all `objects.PrimitiveObject` to their simpler Python + # types. This is normally not something we would do, since it's + # lossy and prevents users from getting back to the data source, + # 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, + ( + 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, + ), ) - # We will update this on each pass in the next loop and use it as the new offset. - attr_base_offset = mft_record.FirstAttrOffset - - attr = self.context.object( - attribute_object, - offset=offset + attr_base_offset, - layer_name=layer.name, - ) - - # There is no field that has a count of Attributes - # Keep Attempting to read attributes until we get an invalid attr.AttrType - is_ads = False - file_name = renderers.NotAvailableValue - # The First $DATA Attr is the 'principal' file itself not the ADS - while attr.Attr_Header.AttrType.is_valid_choice: - if attr.Attr_Header.AttrType.lookup() == "FILE_NAME": - attr_data = attr.Attr_Data.cast(fn_object) - file_name = attr_data.get_full_name() - if attr.Attr_Header.AttrType.lookup() == "DATA": - if is_ads: - if not attr.Attr_Header.NonResidentFlag: - # Resident files are the most interesting. - if attr.Attr_Header.NameLength > 0: - ads_name = attr.get_resident_filename() - if not ads_name: - ads_name = renderers.NotAvailableValue - - content = attr.get_resident_filecontent() - if content: - # Preparing for Disassembly - disasm = interfaces.renderers.BaseAbsentValue - architecture = layer.metadata.get( - "architecture", None - ) - if architecture: - disasm = interfaces.renderers.Disassembly( - content, 0, architecture.lower() - ) - content = format_hints.HexBytes(content) - else: - content = renderers.NotAvailableValue() - disasm = interfaces.renderers.BaseAbsentValue() - - yield 0, ( - format_hints.Hex(attr_data.vol.offset), - mft_record.get_signature(), - mft_record.RecordNumber, - attr.Attr_Header.AttrType.lookup(), - file_name, - ads_name, - content, - disasm, - ) - else: - is_ads = True - - # If there's no advancement the loop will never end, so break it now - if attr.Attr_Header.Length == 0: - break - - # Update the base offset to point to the next attribute - attr_base_offset += attr.Attr_Header.Length - # Get the next attribute - attr = self.context.object( - attribute_object, - offset=offset + attr_base_offset, - layer_name=layer.name, - ) def run(self): return renderers.TreeGrid( @@ -304,8 +382,110 @@ class ADS(interfaces.plugins.PluginInterface): ("MFT Type", str), ("Filename", str), ("ADS Filename", str), - ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), + ("Hexdump", renderers.LayerData), + ], + self._generator(), + ) + + +class ResidentData(interfaces.plugins.PluginInterface): + """Scans for MFT Records with Resident Data""" + + _required_framework_version = (2, 26, 0) + + _version = (2, 0, 0) + + class ResidentDataResult(NamedTuple): + offset: format_hints.Hex + signature: objects.String + record_number: int + attribute_type: str + filename: Union[objects.String, interfaces.renderers.BaseAbsentValue] + content: Union[renderers.LayerData, interfaces.renderers.BaseAbsentValue] + + @classmethod + def get_requirements(cls): + return [ + requirements.VersionRequirement( + name="MFTScan", component=MFTScan, version=(3, 0, 0) + ), + requirements.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), + ] + + @classmethod + def parse_resident_data( + cls, + mft_record: mft.MFTEntry, + ) -> Optional[ResidentDataResult]: + """ + Returns the parsed data from a MFT record + """ + + try: + attr = next(mft_record.resident_data_attributes()) + except StopIteration: + return None + + content = attr.get_resident_filecontent() + if content: + content = renderers.LayerData.from_object(content) + else: + content = renderers.NotAvailableValue() + + # Choose the longest of the two, since it often includes a DOS 8.3 name + filename = mft_record.longest_filename() or renderers.NotAvailableValue() + + return cls.ResidentDataResult( + format_hints.Hex(attr.Attr_Data.vol.offset), + mft_record.get_signature(), + mft_record.RecordNumber, + attr.Attr_Header.AttrType.lookup(), + filename, + content, + ) + + def _generator(self): + for mft_record in MFTScan.enumerate_mft_records( + self.context, + self.config_path, + self.config["primary"], + ): + resident_data_entry = self.parse_resident_data(mft_record) + if resident_data_entry: + # Convert all `objects.PrimitiveObject` to their simpler Python + # types. This is normally not something we would do, since it's + # lossy and prevents users from getting back to the data source, + # 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, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Record Type", str), + ("Record Number", int), + ("MFT Type", str), + ("Filename", str), + ("Hexdump", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/windows/modscan.py b/volatility3/framework/plugins/windows/modscan.py index fc45e6913..667fadd11 100644 --- a/volatility3/framework/plugins/windows/modscan.py +++ b/volatility3/framework/plugins/windows/modscan.py @@ -15,7 +15,9 @@ class ModScan(modules.Modules): """Scans for modules present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + + # 3.0.0 changed the signature of enumeration methods (scan_modules) + _version = (3, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -30,10 +32,10 @@ class ModScan(modules.Modules): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="poolscanner", component=poolscanner.PoolScanner, version=(1, 0, 0) + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(2, 0, 0) + name="modules", component=modules.Modules, version=(3, 0, 0) ), requirements.BooleanRequirement( name="dump", @@ -53,7 +55,7 @@ class ModScan(modules.Modules): default=None, ), requirements.VersionRequirement( - name="pedump", component=pedump.PEDump, version=(1, 0, 0) + name="pedump", component=pedump.PEDump, version=(2, 0, 0) ), ] @@ -61,26 +63,25 @@ class ModScan(modules.Modules): def scan_modules( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for modules using the poolscanner module and constraints. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols - + kernel_module_name: Name of the module for the kernel Returns: - A list of Driver objects as found from the `layer_name` layer based on Driver pool signatures + A list of kernel module objects as found from the primary (kernel) layer based on module pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"MmLd"] + kernel.symbol_table_name, [b"MmLd"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result yield mem_object diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index ba45834d5..18a7b5919 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -2,14 +2,14 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Iterable, Generator +from typing import Generator, Iterable, List, Optional, Dict, Tuple -from volatility3.framework import exceptions, interfaces, constants, renderers +from volatility3.framework import symbols, constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, pedump +from volatility3.plugins.windows import pedump, pslist vollog = logging.getLogger(__name__) @@ -18,7 +18,9 @@ class Modules(interfaces.plugins.PluginInterface): """Lists the loaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + + # 3.0.0 - changed signature of get_session_layers, added get_session_layers_map + _version = (3, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -33,7 +35,10 @@ class Modules(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pedump", component=pedump.PEDump, version=(2, 0, 0) ), requirements.BooleanRequirement( name="dump", @@ -52,9 +57,6 @@ class Modules(interfaces.plugins.PluginInterface): optional=True, default=None, ), - requirements.VersionRequirement( - name="pedump", component=pedump.PEDump, version=(1, 0, 0) - ), ] def dump_module(self, session_layers, pe_table_name, mod): @@ -76,8 +78,6 @@ class Modules(interfaces.plugins.PluginInterface): return file_output def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - pe_table_name = None session_layers = None @@ -92,24 +92,24 @@ class Modules(interfaces.plugins.PluginInterface): session_layers = list( self.get_session_layers( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], ) ) for mod in self._enumeration_method( - self.context, kernel.layer_name, kernel.symbol_table_name + self.context, kernel_module_name=self.config["kernel"] ): if self.config["base"] and self.config["base"] != mod.DllBase: continue try: BaseDllName = mod.BaseDllName.get_string() + if self.config["name"] and self.config["name"] not in BaseDllName: + continue except exceptions.InvalidAddressException: BaseDllName = interfaces.renderers.BaseAbsentValue() - if self.config["name"] and self.config["name"] not in BaseDllName: - continue - try: FullDllName = mod.FullDllName.get_string() except exceptions.InvalidAddressException: @@ -119,43 +119,82 @@ 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 - def get_session_layers( + def get_kernel_space_start(cls, context, module_name: str) -> int: + """ + Returns the starting address of the kernel address space + + This method allows plugins that analyze kernel data structures to quickly detect + smeared or otherwise invalid data as many pointers must point into the kernel or + access during runtime would crash the system + """ + module = context.modules[module_name] + + # default is used if/when MmSystemRangeStart is paged out + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=module.symbol_table_name + ): + object_type = "unsigned long long" + default_start = 0xFFFF800000000000 + else: + object_type = "unsigned long" + default_start = 0x80000000 + + range_start_offset = module.get_symbol("MmSystemRangeStart").address + + try: + kernel_space_start = module.object( + object_type=object_type, offset=range_start_offset + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read MmSystemRangeStart. Defaulting to {default_start:#x} for the kernel space start." + ) + kernel_space_start = default_start + + layer = context.layers[module.layer_name] + + return kernel_space_start & layer.address_mask + + @classmethod + def _do_get_session_layers( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - pids: List[int] = None, - ) -> Generator[str, None, None]: + kernel_module_name: str, + pids: Optional[List[int]] = None, + ) -> Generator[Tuple[int, str], None, None]: """Build a cache of possible virtual layers, in priority starting with the primary/kernel layer. Then keep one layer per session by cycling through the process list. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel pids: A list of process identifiers to include exclusively or None for no filter Returns: - A list of session layer names + A generator of session layer names """ seen_ids: List[interfaces.objects.ObjectInterface] = [] filter_func = pslist.PsList.create_pid_filter(pids or []) + kernel = context.modules[kernel_module_name] + for proc in pslist.PsList.list_processes( context=context, - layer_name=layer_name, - symbol_table=symbol_table, + kernel_module_name=kernel_module_name, filter_func=filter_func, ): proc_id = "Unknown" @@ -165,28 +204,84 @@ class Modules(interfaces.plugins.PluginInterface): # create the session space object in the process' own layer. # not all processes have a valid session pointer. - session_space = context.object( - symbol_table + constants.BANG + "_MM_SESSION_SPACE", - layer_name=layer_name, - offset=proc.Session, - ) + try: + session_space = context.object( + kernel.symbol_table_name + constants.BANG + "_MM_SESSION_SPACE", + layer_name=kernel.layer_name, + offset=proc.Session, + ) + session_id = session_space.SessionId - if session_space.SessionId in seen_ids: + except exceptions.SymbolError: + # In Windows 11 24H2, the _MM_SESSION_SPACE type was + # replaced with _PSP_SESSION_SPACE, and the kernel PDB + # doesn't contain information about its members (otherwise, + # we would just fall back to the new type). However, it + # appears to be, for our purposes, functionally identical + # to the _MM_SESSION_SPACE. Because _MM_SESSION_SPACE + # stores its session ID at offset 8 as an unsigned long, we + # create an unsigned long at that offset and use that + # instead. + session_id = context.object( + layer_name=kernel.layer_name, + object_type=kernel.symbol_table_name + + constants.BANG + + "unsigned long", + offset=proc.Session + 8, + ) + + if session_id in seen_ids: continue except exceptions.InvalidAddressException: vollog.log( constants.LOGLEVEL_VVV, - "Process {} does not have a valid Session or a layer could not be constructed for it".format( - proc_id - ), + f"Process {proc_id} does not have a valid Session or a layer could not be constructed for it", ) continue # save the layer if we haven't seen the session yet - seen_ids.append(session_space.SessionId) + seen_ids.append(session_id) + yield session_id, proc_layer_name + + @classmethod + def get_session_layers( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + pids: Optional[List[int]] = None, + ) -> Generator[str, None, None]: + """ + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the module for the kernel + pids: A list of process identifiers to include exclusively or None for no filter + + Yields the names of the unique memory layers that map sessions + """ + for _session_id, proc_layer_name in cls._do_get_session_layers( + context, kernel_module_name, pids + ): yield proc_layer_name + @classmethod + def get_session_layers_map( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + pids: Optional[List[int]] = None, + ) -> Dict[int, str]: + """ + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the module for the kernel + pids: A list of process identifiers to include exclusively or None for no filter + + Wraps `_do_get_session_layers` to produce a dictionary where each key is a session_id + and the value is the name of the layer for that session + """ + return dict(cls._do_get_session_layers(context, kernel_module_name, pids)) + @classmethod def find_session_layer( cls, @@ -218,40 +313,39 @@ class Modules(interfaces.plugins.PluginInterface): def list_modules( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the modules in the primary layer. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols - + kernel_module_name: The name of the module for the kernel Returns: A list of Modules as retrieved from PsLoadedModuleList """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + kernel = context.modules[kernel_module_name] + if not kernel.offset: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) try: # use this type if its available (starting with windows 10) - ldr_entry_type = ntkrnlmp.get_type("_KLDR_DATA_TABLE_ENTRY") + ldr_entry_type = kernel.get_type("_KLDR_DATA_TABLE_ENTRY") except exceptions.SymbolError: - ldr_entry_type = ntkrnlmp.get_type("_LDR_DATA_TABLE_ENTRY") + ldr_entry_type = kernel.get_type("_LDR_DATA_TABLE_ENTRY") type_name = ldr_entry_type.type_name.split(constants.BANG)[1] - list_head = ntkrnlmp.get_symbol("PsLoadedModuleList").address - list_entry = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=list_head) + list_head = kernel.get_symbol("PsLoadedModuleList").address + list_entry = kernel.object(object_type="_LIST_ENTRY", offset=list_head) reloff = ldr_entry_type.relative_child_offset("InLoadOrderLinks") - module = ntkrnlmp.object( + module = kernel.object( object_type=type_name, offset=list_entry.vol.offset - reloff, absolute=True ) - for mod in module.InLoadOrderLinks: - yield mod + yield from module.InLoadOrderLinks def run(self): return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/windows/mutantscan.py b/volatility3/framework/plugins/windows/mutantscan.py index 64d3b5470..ba2824bfc 100644 --- a/volatility3/framework/plugins/windows/mutantscan.py +++ b/volatility3/framework/plugins/windows/mutantscan.py @@ -14,6 +14,7 @@ class MutantScan(interfaces.plugins.PluginInterface): """Scans for mutexes present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -23,8 +24,8 @@ class MutantScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), ] @@ -32,36 +33,32 @@ class MutantScan(interfaces.plugins.PluginInterface): def scan_mutants( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for mutants using the poolscanner module and constraints. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel Returns: A list of Mutant objects found by scanning memory for the Mutant pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Mut\xe1", b"Muta"] + kernel.symbol_table_name, [b"Mut\xe1", b"Muta"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - for mutant in self.scan_mutants( - self.context, kernel.layer_name, kernel.symbol_table_name - ): + for mutant in self.scan_mutants(self.context, self.config["kernel"]): try: name = mutant.get_name() except (ValueError, exceptions.InvalidAddressException): diff --git a/volatility3/framework/plugins/windows/netscan.py b/volatility3/framework/plugins/windows/netscan.py index 66a24da5a..fa422e103 100644 --- a/volatility3/framework/plugins/windows/netscan.py +++ b/volatility3/framework/plugins/windows/netscan.py @@ -23,7 +23,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Scans for network objects present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -34,10 +34,15 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="poolscanner", component=poolscanner.PoolScanner, version=(1, 0, 0) + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), requirements.VersionRequirement( - name="info", component=info.Info, version=(1, 0, 0) + name="info", component=info.Info, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.VersionRequirement( name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) @@ -50,9 +55,9 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ), ] - @staticmethod + @classmethod def create_netscan_constraints( - context: interfaces.context.ContextInterface, symbol_table: str + cls, context: interfaces.context.ContextInterface, symbol_table: str ) -> List[poolscanner.PoolConstraint]: """Creates a list of Pool Tag Constraints for network objects. @@ -117,15 +122,13 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def determine_tcpip_version( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, ) -> Tuple[str, Type]: """Tries to determine which symbol filename to use for the image's tcpip driver. The logic is partially taken from the info plugin. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols + kernel_module_name: Name of the module for the kernel Returns: The filename of the symbol table to use. @@ -137,10 +140,14 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # therefore we determine the version based on the kernel version as testing # with several windows versions has showed this to work out correctly. - is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) + kernel = context.modules[kernel_module_name] + + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) is_18363_or_later = versions.is_win10_18363_or_later( - context=context, symbol_table=nt_symbol_table + context=context, symbol_table=kernel.symbol_table_name ) if is_64bit: @@ -148,9 +155,9 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: arch = "x86" - vers = info.Info.get_version_structure(context, layer_name, nt_symbol_table) + vers = info.Info.get_version_structure(context, kernel_module_name) - kuser = info.Info.get_kuser_structure(context, layer_name, nt_symbol_table) + kuser = info.Info.get_kuser_structure(context, kernel_module_name) try: vers_minor_version = int(vers.MinorVersion) @@ -161,20 +168,15 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): raise NotImplementedError( "Kernel Debug Structure version format not supported!" ) - except: - # unsure what to raise here. Also, it might be useful to add some kind of fallback, + except Exception: + # FIXME: unsure what to raise here. Also, it might be useful to add some kind of fallback, # either to a user-provided version or to another method to determine tcpip.sys's version raise exceptions.VolatilityException( "Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!" ) vollog.debug( - "Determined OS Version: {}.{} {}.{}".format( - kuser.NtMajorVersion, - kuser.NtMinorVersion, - vers.MajorVersion, - vers.MinorVersion, - ) + f"Determined OS Version: {kuser.NtMajorVersion}.{kuser.NtMinorVersion} {vers.MajorVersion}.{vers.MinorVersion}" ) if nt_major_version == 10 and arch == "x64": @@ -262,7 +264,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): "Requiring further version inspection due to OS version by checking tcpip.sys's FileVersion header" ) # the following is IntelLayer specific and might need to be adapted to other architectures. - physical_layer_name = context.layers[layer_name].config.get( + physical_layer_name = context.layers[kernel.layer_name].config.get( "memory_layer", None ) if physical_layer_name: @@ -272,9 +274,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if ver: tcpip_mod_version = ver[3] vollog.debug( - "Determined tcpip.sys's FileVersion: {}".format( - tcpip_mod_version - ) + f"Determined tcpip.sys's FileVersion: {tcpip_mod_version}" ) else: vollog.debug("Could not determine tcpip.sys's FileVersion.") @@ -316,12 +316,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): else: raise NotImplementedError( - "This version of Windows is not supported: {}.{} {}.{}!".format( - nt_major_version, - nt_minor_version, - vers.MajorVersion, - vers_minor_version, - ) + f"This version of Windows is not supported: {nt_major_version}.{nt_minor_version} {vers.MajorVersion}.{vers_minor_version}!" ) vollog.debug(f"Determined symbol filename: {filename}") @@ -332,27 +327,26 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def create_netscan_symbol_table( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, config_path: str, ) -> str: """Creates a symbol table for TCP Listeners and TCP/UDP Endpoints. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols + kernel_module_name: Name of the module for the kernel config_path: The config path where to find symbol files Returns: The name of the constructed symbol table """ - table_mapping = {"nt_symbols": nt_symbol_table} + kernel = context.modules[kernel_module_name] + + table_mapping = {"nt_symbols": kernel.symbol_table_name} symbol_filename, class_types = cls.determine_tcpip_version( context, - layer_name, - nt_symbol_table, + kernel_module_name, ) return intermed.IntermediateSymbolTable.create( @@ -368,16 +362,14 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def scan( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, netscan_symbol_table: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for network objects using the poolscanner module and constraints. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel netscan_symbol_table: The name of the table containing the network object symbols (_TCP_LISTENER etc.) Returns: @@ -387,7 +379,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): constraints = cls.create_netscan_constraints(context, netscan_symbol_table) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, nt_symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result yield mem_object @@ -395,16 +387,13 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def _generator(self, show_corrupt_results: Optional[bool] = None): """Generates the network objects for use in rendering.""" - kernel = self.context.modules[self.config["kernel"]] - netscan_symbol_table = self.create_netscan_symbol_table( - self.context, kernel.layer_name, kernel.symbol_table_name, self.config_path + self.context, self.config["kernel"], self.config_path ) for netw_obj in self.scan( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], netscan_symbol_table, ): vollog.debug( @@ -510,17 +499,8 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): for i in row_data ] description = ( - "Network connection: Process {} {} Local Address {}:{} " - "Remote Address {}:{} State {} Protocol {} ".format( - row_data[7], - row_data[8], - row_data[2], - row_data[3], - row_data[4], - row_data[5], - row_data[6], - row_data[1], - ) + f"Network connection: Process {row_data[7]} {row_data[8]} Local Address {row_data[2]}:{row_data[3]} " + f"Remote Address {row_data[4]}:{row_data[5]} State {row_data[6]} Protocol {row_data[1]} " ) yield (description, timeliner.TimeLinerType.CREATED, row_data[9]) diff --git a/volatility3/framework/plugins/windows/netstat.py b/volatility3/framework/plugins/windows/netstat.py index 0908767fc..cf7f5272a 100644 --- a/volatility3/framework/plugins/windows/netstat.py +++ b/volatility3/framework/plugins/windows/netstat.py @@ -21,7 +21,9 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Traverses network tracking structures present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + + # 2.0.0 changed the signature of `get_tcpip_module` + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -32,16 +34,21 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="netscan", component=netscan.NetScan, version=(1, 0, 0) + name="netscan", component=netscan.NetScan, version=(2, 0, 0) ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(2, 0, 0) + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.VersionRequirement( name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) ), requirements.VersionRequirement( - name="info", component=info.Info, version=(1, 0, 0) + name="info", component=info.Info, version=(2, 0, 0) ), requirements.VersionRequirement( name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) @@ -111,8 +118,21 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): The list of indices at which a 1 was found. """ ret = [] + # This value is broken in many samples and was causing essentially infinite loops + # Testing showed that 8192 is the current size across all Windows versions + # We give some leeway in case it increases in later versions, while still keeping it sane + # The problematic samples had values that looked like addresses, so in the billions + if bitmap_size_in_byte > 8192 * 10: + return ret + for idx in range(bitmap_size_in_byte): - current_byte = context.layers[layer_name].read(bitmap_offset + idx, 1)[0] + try: + current_byte = context.layers[layer_name].read(bitmap_offset + idx, 1)[ + 0 + ] + except exceptions.InvalidAddressException: + continue + current_offs = idx * 8 for bit in range(8): if current_byte & (1 << bit) != 0: @@ -154,32 +174,37 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) else: # invalid argument. - return None + return vollog.debug(f"Current Port: {port}") # the given port serves as a shifted index into the port pool lists list_index = port >> 8 truncated_port = port & 0xFF - # constructing port_pool object here so callers don't have to - port_pool = context.object( - net_symbol_table + constants.BANG + "_INET_PORT_POOL", - layer_name=layer_name, - offset=port_pool_addr, - ) + try: + # constructing port_pool object here so callers don't have to + port_pool = context.object( + net_symbol_table + constants.BANG + "_INET_PORT_POOL", + layer_name=layer_name, + offset=port_pool_addr, + ) + # first, grab the given port's PortAssignment (`_PORT_ASSIGNMENT`) + inpa = port_pool.PortAssignments[list_index] - # first, grab the given port's PortAssignment (`_PORT_ASSIGNMENT`) - inpa = port_pool.PortAssignments[list_index] - - # then parse the port assignment list (`_PORT_ASSIGNMENT_LIST`) and grab the correct entry - assignment = inpa.InPaBigPoolBase.Assignments[truncated_port] + # then parse the port assignment list (`_PORT_ASSIGNMENT_LIST`) and grab the correct entry + assignment = inpa.InPaBigPoolBase.Assignments[truncated_port] + except exceptions.InvalidAddressException: + return if not assignment: - return None + return # the value within assignment.Entry is a) masked and b) points inside of the network object # first decode the pointer - netw_inside = cls._decode_pointer(assignment.Entry) + try: + netw_inside = cls._decode_pointer(assignment.Entry) + except exceptions.InvalidAddressException: + return if netw_inside: # if the value is valid, calculate the actual object address by subtracting the offset @@ -188,34 +213,46 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) yield curr_obj + try: + next_obj_address = cls._decode_pointer(curr_obj.Next) + except exceptions.InvalidAddressException: + return + # if the same port is used on different interfaces multiple objects are created # those can be found by following the pointer within the object's `Next` field until it is empty - while curr_obj.Next: - curr_obj = context.object( - obj_name, - layer_name=layer_name, - offset=cls._decode_pointer(curr_obj.Next) - ptr_offset, - ) + while next_obj_address: + try: + curr_obj = context.object( + obj_name, + layer_name=layer_name, + offset=next_obj_address - ptr_offset, + ) + except exceptions.InvalidAddressException: + return + yield curr_obj + try: + next_obj_address = cls._decode_pointer(curr_obj.Next) + except exceptions.InvalidAddressException: + return + @classmethod def get_tcpip_module( cls, context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbols: str, + kernel_module_name: str, ) -> Optional[interfaces.objects.ObjectInterface]: """Uses `windows.modules` to find tcpip.sys in memory. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - nt_symbols: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel Returns: The constructed tcpip.sys module object. """ - for mod in modules.Modules.list_modules(context, layer_name, nt_symbols): + for mod in modules.Modules.list_modules(context, kernel_module_name): if mod.BaseDllName.get_string() == "tcpip.sys": vollog.debug(f"Found tcpip.sys image base @ 0x{mod.DllBase:x}") return mod @@ -243,16 +280,25 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): The hash table entries which are _not_ empty """ # we are looking for entries whose values are not their own address + # smear sanity check from mass testing + if ht_length > 4096: + return + for index in range(ht_length): current_addr = ht_offset + index * alignment - current_pointer = context.object( - net_symbol_table + constants.BANG + "pointer", - layer_name=layer_name, - offset=current_addr, - ) + try: + current_pointer = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=current_addr, + ) + except exceptions.InvalidAddressException: + continue + # check if addr of pointer is equal to the value pointed to if current_pointer.vol.offset == current_pointer: continue + yield current_pointer @classmethod @@ -278,7 +324,9 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: The list of TCP endpoint objects from the `layer_name` layer's `PartitionTable` """ - if symbols.symbol_table_is_64bit(context, net_symbol_table): + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=net_symbol_table + ): alignment = 0x10 else: alignment = 8 @@ -292,11 +340,15 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): tcpip_symbol_table + constants.BANG + "PartitionCount" ).address - part_table_addr = context.object( - net_symbol_table + constants.BANG + "pointer", - layer_name=layer_name, - offset=tcpip_module_offset + part_table_symbol, - ) + try: + part_table_addr = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + part_table_symbol, + ) + except exceptions.InvalidAddressException: + vollog.debug("`PartitionTable` not present in memory.") + return # part_table is the actual partition table offset and consists out of a dynamic amount of _PARTITION objects part_table = context.object( @@ -304,23 +356,41 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): layer_name=layer_name, offset=part_table_addr, ) - part_count = int.from_bytes( - context.layers[layer_name].read(tcpip_module_offset + part_count_symbol, 1), - "little", - ) + + try: + part_count = int.from_bytes( + context.layers[layer_name].read( + tcpip_module_offset + part_count_symbol, 1 + ), + "little", + ) + except exceptions.InvalidAddressException: + vollog.debug("`PartitionCount` not present in memory.") + return + part_table.Partitions.count = part_count vollog.debug( - "Found TCP connection PartitionTable @ 0x{:x} (partition count: {})".format( - part_table_addr, part_count - ) + f"Found TCP connection PartitionTable @ 0x{part_table_addr:x} (partition count: {part_count})" ) entry_offset = context.symbol_space.get_type(obj_name).relative_child_offset( "ListEntry" ) - for ctr, partition in enumerate(part_table.Partitions): + + try: + partitions = part_table.Partitions + except exceptions.InvalidAddressException: + vollog.debug("Partitions member not present in memory") + return + + for ctr, partition in enumerate(partitions): vollog.debug(f"Parsing partition {ctr}") - if partition.Endpoints.NumEntries > 0: + try: + num_entries = partition.Endpoints.NumEntries + except exceptions.InvalidAddressException: + continue + + if num_entries > 0: for endpoint_entry in cls.parse_hashtable( context, layer_name, @@ -404,6 +474,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): upp_symbol = context.symbol_space.get_symbol( tcpip_symbol_table + constants.BANG + "UdpPortPool" ).address + upp_addr = context.object( net_symbol_table + constants.BANG + "pointer", layer_name=layer_name, @@ -490,18 +561,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ # first, TCP endpoints by parsing the partition table - for endpoint in cls.parse_partitions( - context, - layer_name, - net_symbol_table, - tcpip_symbol_table, - tcpip_module_offset, - ): - yield endpoint - - # then, towards the UDP and TCP port pools - # first, find their addresses - upp_addr, tpp_addr = cls.find_port_pools( + yield from cls.parse_partitions( context, layer_name, net_symbol_table, @@ -509,6 +569,19 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): tcpip_module_offset, ) + # then, towards the UDP and TCP port pools + # first, find their addresses + try: + upp_addr, tpp_addr = cls.find_port_pools( + context, + layer_name, + net_symbol_table, + tcpip_symbol_table, + tcpip_module_offset, + ) + except (exceptions.SymbolError, exceptions.InvalidAddressException): + vollog.debug("Unable to reconstruct port pools") + # create port pool objects at the detected address and parse the port bitmap upp_obj = context.object( net_symbol_table + constants.BANG + "_INET_PORT_POOL", @@ -561,12 +634,10 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): kernel = self.context.modules[self.config["kernel"]] netscan_symbol_table = netscan.NetScan.create_netscan_symbol_table( - self.context, kernel.layer_name, kernel.symbol_table_name, self.config_path + self.context, self.config["kernel"], self.config_path ) - tcpip_module = self.get_tcpip_module( - self.context, kernel.layer_name, kernel.symbol_table_name - ) + tcpip_module = self.get_tcpip_module(self.context, self.config["kernel"]) if not tcpip_module: vollog.error("Unable to locate symbols for the memory image's tcpip module") @@ -581,6 +652,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) except exceptions.VolatilityException: vollog.error("Unable to locate symbols for the memory image's tcpip module") + return for netw_obj in self.list_sockets( self.context, @@ -624,9 +696,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): proto = "TCPv6" else: vollog.debug( - "TCP Endpoint @ 0x{:2x} has unknown address family 0x{:x}".format( - netw_obj.vol.offset, netw_obj.get_address_family() - ) + f"TCP Endpoint @ 0x{netw_obj.vol.offset:2x} has unknown address family 0x{netw_obj.get_address_family():x}" ) proto = "TCPv?" diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index f4901dc8c..16a25503e 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -5,9 +5,9 @@ import logging from typing import List, Generator -from volatility3.framework import interfaces, symbols +from volatility3.framework import interfaces, exceptions from volatility3.framework.configuration import requirements -from volatility3.plugins.windows import thrdscan, ssdt +from volatility3.plugins.windows import thrdscan, ssdt, modules vollog = logging.getLogger(__name__) @@ -16,7 +16,9 @@ class Threads(thrdscan.ThrdScan): """Lists process threads""" _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + + # 2.0.0 - changed the signature of `list_orphan_kernel_threads` + _version = (2, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -31,11 +33,14 @@ class Threads(thrdscan.ThrdScan): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="thrdscan", plugin=thrdscan.ThrdScan, version=(1, 1, 0) + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) ), ] @@ -43,7 +48,7 @@ class Threads(thrdscan.ThrdScan): def list_orphan_kernel_threads( cls, context: interfaces.context.ContextInterface, - module_name: str, + kernel_module_name: str, ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Yields thread objects of kernel threads that do not map to a module @@ -54,41 +59,46 @@ class Threads(thrdscan.ThrdScan): Returns: A generator of thread objects of orphaned threads """ - module = context.modules[module_name] - layer_name = module.layer_name - symbol_table = module.symbol_table_name - collection = ssdt.SSDT.build_module_collection( - context, layer_name, symbol_table + context=context, + kernel_module_name=kernel_module_name, ) - # FIXME - use a proper constant once established - # used to filter out smeared pointers - if symbols.symbol_table_is_64bit(context, symbol_table): - kernel_start = 0xFFFFF80000000000 - else: - kernel_start = 0x80000000 + kernel_space_start = modules.Modules.get_kernel_space_start( + context, kernel_module_name + ) - for thread in thrdscan.ThrdScan.scan_threads(context, module_name): - # we don't want smeared or terminated threads + for thread in thrdscan.ThrdScan.scan_threads(context, kernel_module_name): + # We don't want smeared or terminated threads + # So we access the owning process (which could also be terminated or smeared) + # Plus check the start address holding page try: proc = thread.owning_process() - except AttributeError: + pid = proc.UniqueProcessId + ppid = proc.InheritedFromUniqueProcessId + + thread_start = thread.StartAddress + except (AttributeError, exceptions.InvalidAddressException): continue # we only care about kernel threads, 4 = System # previous methods for determining if a thread was a kernel thread # such as bit fields and flags are not stable in Win10+ # so we check if the thread is from the kernel itself or one its child - # kernel processes (MemCompression, Regsitry, ...) - if proc.UniqueProcessId != 4 and proc.InheritedFromUniqueProcessId != 4: + # kernel processes (MemCompression, Registry, ...) + if pid != 4 and ppid != 4: continue - if thread.StartAddress < kernel_start: + # if the thread has an exit time or terminated (4) state, then skip it + if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: + continue + + # threads pointing into userland, which is from smeared or terminated threads + if thread_start < kernel_space_start: continue module_symbols = list( - collection.get_module_symbols_by_absolute_location(thread.StartAddress) + collection.get_module_symbols_by_absolute_location(thread_start) ) # alert on threads that do not map to a module diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 955098d6b..b21e39a8c 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -1,7 +1,6 @@ # 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 -import copy import io import logging import ntpath @@ -11,14 +10,14 @@ from typing import Dict, Tuple, Optional, List, Generator, Union, Callable import pefile from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers, constants +from volatility3.framework import renderers, constants, objects from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins.windows import pslist, modules -from volatility3.framework.constants.windows import KERNEL_MODULE_NAMES +from volatility3.framework.constants import windows vollog = logging.getLogger(__name__) @@ -38,7 +37,7 @@ filter_modules_type = Dict[str, filter_module_info] found_symbols_module = List[Tuple[str, int]] found_symbols_type = Dict[str, found_symbols_module] -# used to hold informatin about a range (VAD or kernel module) +# used to hold information about a range (VAD or kernel module) # (start address, size, file path) range_type = Tuple[int, int, str] ranges_type = List[range_type] @@ -158,7 +157,7 @@ class PESymbolFinder: class PDBSymbolFinder(PESymbolFinder): """ - PESymbolFinder implementation for PDB modules + PESymbolFinder implementation for PDB modules """ def _do_get_address(self, name: str) -> Optional[int]: @@ -195,7 +194,7 @@ class PDBSymbolFinder(PESymbolFinder): class ExportSymbolFinder(PESymbolFinder): """ - PESymbolFinder implementation for PDB modules + PESymbolFinder implementation for PDB modules """ def _get_name(self, export: pefile.ExportData) -> Optional[str]: @@ -230,7 +229,6 @@ class ExportSymbolFinder(PESymbolFinder): Returns: address: the address of the symbol, if found """ - for export in self._symbol_module: sym_name = self._get_name(export) if sym_name and sym_name == name: @@ -244,7 +242,9 @@ class PESymbols(interfaces.plugins.PluginInterface): _required_framework_version = (2, 7, 0) - _version = (1, 0, 0) + # 2.0.0 - changed signature of get_kernel_modules, get_all_vads_with_file_paths, addresses_for_process_symbols, get_process_modules + # 3.0.0 - find_symbols will now throw a ValueError if the provided wanted symbol information does not follow the spec + _version = (3, 0, 0) # used for special handling of the kernel PDB file. See later notes os_module_name = "ntoskrnl.exe" @@ -259,10 +259,10 @@ class PESymbols(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(2, 0, 0) + name="modules", component=modules.Modules, version=(3, 0, 0) ), requirements.VersionRequirement( name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) @@ -292,19 +292,20 @@ class PESymbols(interfaces.plugins.PluginInterface): ), ] - @staticmethod - def _get_pefile_obj( + @classmethod + def get_pefile_obj( + cls, context: interfaces.context.ContextInterface, pe_table_name: str, - layer_name: str, + process_layer_name: str, base_address: int, ) -> Optional[pefile.PE]: """ - Attempts to pefile object from the bytes of the PE file + Attempts to create a pefile object from the bytes of the PE file Args: pe_table_name: name of the pe types table - layer_name: name of the process layer + process_layer_name: name of the process layer base_address: base address of the module Returns: @@ -316,7 +317,7 @@ class PESymbols(interfaces.plugins.PluginInterface): dos_header = context.object( pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", offset=base_address, - layer_name=layer_name, + layer_name=process_layer_name, ) for offset, data in dos_header.reconstruct(): @@ -325,14 +326,14 @@ class PESymbols(interfaces.plugins.PluginInterface): pe_ret = pefile.PE(data=pe_data.getvalue(), fast_load=True) - except exceptions.InvalidAddressException: + except (exceptions.InvalidAddressException, ValueError): pe_ret = None return pe_ret - @staticmethod + @classmethod def range_info_for_address( - ranges: ranges_type, address: int + cls, ranges: ranges_type, address: int ) -> Optional[range_type]: """ Helper for getting the range information for an address. @@ -351,8 +352,8 @@ class PESymbols(interfaces.plugins.PluginInterface): return None - @staticmethod - def filepath_for_address(ranges: ranges_type, address: int) -> Optional[str]: + @classmethod + def filepath_for_address(cls, ranges: ranges_type, address: int) -> Optional[str]: """ Helper to get the file path for an address @@ -369,8 +370,8 @@ class PESymbols(interfaces.plugins.PluginInterface): return None - @staticmethod - def filename_for_path(filepath: str) -> str: + @classmethod + def filename_for_path(cls, filepath: str) -> str: """ Consistent way to get the filename regardless of platform @@ -382,12 +383,12 @@ class PESymbols(interfaces.plugins.PluginInterface): """ return ntpath.basename(filepath).lower() - @staticmethod + @classmethod def addresses_for_process_symbols( + cls, context: interfaces.context.ContextInterface, config_path: str, - layer_name: str, - symbol_table_name: str, + kernel_module_name: str, symbols: filter_modules_type, ) -> found_symbols_type: """ @@ -403,7 +404,7 @@ class PESymbols(interfaces.plugins.PluginInterface): found_symbols_type: The dictionary of symbols that were resolved """ collected_modules = PESymbols.get_process_modules( - context, layer_name, symbol_table_name, symbols + context, kernel_module_name, symbols ) found_symbols, missing_symbols = PESymbols.find_symbols( @@ -411,13 +412,16 @@ class PESymbols(interfaces.plugins.PluginInterface): ) for mod_name, unresolved_symbols in missing_symbols.items(): - for symbol in unresolved_symbols: - vollog.debug(f"Unable to resolve symbol {symbol} in module {mod_name}") + for symbol_key, symbols in unresolved_symbols.items(): + vollog.debug( + f"Unable to resolve symbols {symbols} of type {symbol_key} in module {mod_name}" + ) return found_symbols - @staticmethod + @classmethod def path_and_symbol_for_address( + cls, context: interfaces.context.ContextInterface, config_path: str, collected_modules: collected_modules_type, @@ -480,12 +484,12 @@ class PESymbols(interfaces.plugins.PluginInterface): instance for it """ - layer_name = module_info[0] + process_layer_name = module_info[0] module_start = module_info[1] # we need a valid PE with an export table - pe_module = PESymbols._get_pefile_obj( - context, pe_table_name, layer_name, module_start + pe_module = PESymbols.get_pefile_obj( + context, pe_table_name, process_layer_name, module_start ) if not pe_module: return None @@ -497,7 +501,7 @@ class PESymbols(interfaces.plugins.PluginInterface): return None return ExportSymbolFinder( - layer_name, + process_layer_name, mod_name.lower(), module_start, pe_module.DIRECTORY_ENTRY_EXPORT.symbols, @@ -529,7 +533,7 @@ class PESymbols(interfaces.plugins.PluginInterface): # a `ntoskrnl.exe` can have an internal PDB name of any of the ones in the following list # The code attempts to find all possible PDBs to ensure the best chance of recovery if mod_name == PESymbols.os_module_name: - pdb_names = [fn + ".pdb" for fn in KERNEL_MODULE_NAMES] + pdb_names = [fn + ".pdb" for fn in windows.KERNEL_MODULE_NAMES] # for non-kernel files, replace the exe, sys, or dll extension with pdb else: @@ -629,7 +633,7 @@ class PESymbols(interfaces.plugins.PluginInterface): def _get_symbol_value( wanted_symbols: filter_module_info, symbol_resolver: PESymbolFinder, - ) -> Generator[Tuple[str, int, str, int], None, None]: + ) -> Generator[Tuple[str, str, int], None, None]: """ Enumerates the symbols specified as wanted by the calling plugin @@ -638,14 +642,14 @@ class PESymbols(interfaces.plugins.PluginInterface): symbol_resolver: method in a layer to resolve the symbols Returns: - Tuple[str, int, str, int]: the index and value of the found symbol in the wanted list, and the name and address of resolved symbol + Tuple[str, str, int]: the symbol identifier (key) of the found symbol in the wanted list, and the name and address of resolved symbol """ if ( wanted_names_identifier not in wanted_symbols and wanted_addresses_identifier not in wanted_symbols ): vollog.warning( - f"Invalid `wanted_symbols` sent to `find_symbols`. addresses and names keys both misssing." + "Invalid `wanted_symbols` sent to `find_symbols`. addresses and names keys both missing." ) return @@ -658,15 +662,70 @@ class PESymbols(interfaces.plugins.PluginInterface): # address or name if symbol_key in wanted_symbols: # walk each wanted address or name - for value_index, wanted_value in enumerate(wanted_symbols[symbol_key]): - symbol_value = symbol_getter(wanted_value) + # build dict in this function for debugging and tracking + all_wanted = [] + for wanted_value in wanted_symbols[symbol_key]: + all_wanted.append(wanted_value) + + for value_index, wanted_value in enumerate(all_wanted): + symbol_value = symbol_getter(wanted_value) if symbol_value: - # yield out deleteion key, deletion index, symbol name, symbol address + # yield out deletion key, deletion index, symbol name, symbol address if symbol_key == wanted_names_identifier: - yield symbol_key, value_index, wanted_value, symbol_value # type: ignore + yield symbol_key, wanted_value, symbol_value else: - yield symbol_key, value_index, symbol_value, wanted_value # type: ignore + yield symbol_key, symbol_value, wanted_value + + for value in all_wanted: + vollog.debug( + f"Unable to resolve value {value} using getter {symbol_getter}" + ) + + @classmethod + def _validate_wanted_modules( + cls, + wanted: PESymbolFinder.cached_module_lists, + ) -> Optional[PESymbolFinder.cached_module_lists]: + """ + Validates and makes a copy of the address(es) and/or name(s) wanted from a particular module + Throws ValueError if invalid values found + """ + remaining: PESymbolFinder.cached_module_lists = {} + + valid_name_types = [str] + valid_address_types = [int, objects.Pointer] + + for wanted_type, wanted_symbols in wanted.items(): + if wanted_type not in [ + wanted_names_identifier, + wanted_addresses_identifier, + ]: + raise ValueError( + f"The symbol type specified ({wanted_type}) is not valid. Values choices: {wanted_names_identifier}, {wanted_addresses_identifier}" + ) + + remaining[wanted_type] = [] + + # symbol_info will be a symbol name or address requested + for symbol_info in wanted_symbols: + if wanted_type == wanted_names_identifier and not isinstance( + symbol_info, tuple(valid_name_types) + ): + raise ValueError( + f"The requested symbol name has a type of {type(symbol_info)} which is not in the allowed set of {valid_name_types}" + ) + + elif wanted_type == wanted_addresses_identifier and not isinstance( + symbol_info, tuple(valid_address_types) + ): + raise ValueError( + f"The requested address has a type of {type(symbol_info)} which is not in the allowed set of {valid_address_types}" + ) + + remaining[wanted_type].append(symbol_info) + + return remaining @staticmethod def _resolve_symbols_through_methods( @@ -692,13 +751,13 @@ class PESymbols(interfaces.plugins.PluginInterface): PESymbols._find_symbols_through_exports, ] - found: found_symbols_module = [] + found_symbols: found_symbols_module = [] # the symbols wanted from this module by the caller wanted = wanted_modules[mod_name] - # make a copy to remove from inside this function for returning to the caller - remaining = copy.deepcopy(wanted) + # The ValueError will pass through to the caller + remaining = PESymbols._validate_wanted_modules(wanted) done_processing = False @@ -710,12 +769,17 @@ class PESymbols(interfaces.plugins.PluginInterface): vollog.debug(f"Have resolver for method {method}") for ( symbol_key, - value_index, symbol_name, symbol_address, ) in PESymbols._get_symbol_value(remaining, symbol_resolver): - found.append((symbol_name, symbol_address)) - del remaining[symbol_key][value_index] + found_symbols.append((symbol_name, symbol_address)) + + if symbol_key == wanted_names_identifier: + to_remove = symbol_name + else: + to_remove = symbol_address + + remaining[symbol_key].remove(to_remove) # everything was resolved, stop this resolver # remove this key from the remaining symbols to resolve @@ -731,10 +795,11 @@ class PESymbols(interfaces.plugins.PluginInterface): if done_processing: break - return found, remaining + return found_symbols, remaining - @staticmethod + @classmethod def find_symbols( + cls, context: interfaces.context.ContextInterface, config_path: str, wanted_modules: PESymbolFinder.cached_value_dict, @@ -744,6 +809,8 @@ class PESymbols(interfaces.plugins.PluginInterface): Loops through each method of symbol analysis until each wanted symbol is found Returns the resolved symbols as a dictionary that includes the name and runtime address + `wanted_modules` must be correctly formatted or a ValueError will be thrown + Args: wanted_modules: the dictionary of modules and symbols to resolve. Modified to remove symbols as they are resolved. collected_modules: return value from `get_kernel_modules` or `get_process_modules` @@ -759,6 +826,7 @@ class PESymbols(interfaces.plugins.PluginInterface): module_instances = collected_modules[mod_name] + # The ValueError from an invalid wanted_modules will pass through to the caller # try to resolve the symbols for `mod_name` through each method (PDB and export table currently) ( found_in_module, @@ -775,11 +843,11 @@ class PESymbols(interfaces.plugins.PluginInterface): return found_symbols, missing_symbols - @staticmethod + @classmethod def get_kernel_modules( + cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, filter_modules: Optional[filter_modules_type], ) -> collected_modules_type: """ @@ -799,7 +867,9 @@ class PESymbols(interfaces.plugins.PluginInterface): filter_modules_check = None session_layers = list( - modules.Modules.get_session_layers(context, layer_name, symbol_table) + modules.Modules.get_session_layers( + context=context, kernel_module_name=kernel_module_name + ) ) # special handling for the kernel @@ -808,7 +878,7 @@ class PESymbols(interfaces.plugins.PluginInterface): ) for index, mod in enumerate( - modules.Modules.list_modules(context, layer_name, symbol_table) + modules.Modules.list_modules(context, kernel_module_name) ): try: mod_name = str(mod.BaseDllName.get_string().lower()) @@ -837,8 +907,9 @@ class PESymbols(interfaces.plugins.PluginInterface): return found_modules - @staticmethod + @classmethod def get_vads_for_process_cache( + cls, vads_cache: Dict[int, ranges_type], owner_proc: interfaces.objects.ObjectInterface, ) -> Optional[ranges_type]: @@ -865,8 +936,9 @@ class PESymbols(interfaces.plugins.PluginInterface): return vads - @staticmethod + @classmethod def get_proc_vads_with_file_paths( + cls, proc: interfaces.objects.ObjectInterface, ) -> ranges_type: """ @@ -899,8 +971,7 @@ class PESymbols(interfaces.plugins.PluginInterface): def get_all_vads_with_file_paths( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table_name: str, + kernel_module_name: str, ) -> Generator[ Tuple[interfaces.objects.ObjectInterface, str, ranges_type], None, @@ -914,8 +985,7 @@ class PESymbols(interfaces.plugins.PluginInterface): """ procs = pslist.PsList.list_processes( context=context, - layer_name=layer_name, - symbol_table=symbol_table_name, + kernel_module_name=kernel_module_name, ) for proc in procs: @@ -928,11 +998,11 @@ class PESymbols(interfaces.plugins.PluginInterface): yield proc, proc_layer_name, vads - @staticmethod + @classmethod def get_process_modules( + cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, filter_modules: Optional[filter_modules_type], ) -> collected_modules_type: """ @@ -952,7 +1022,7 @@ class PESymbols(interfaces.plugins.PluginInterface): filter_modules_check = None for _proc, proc_layer_name, vads in PESymbols.get_all_vads_with_file_paths( - context, layer_name, symbol_table + context, kernel_module_name ): for vad_start, vad_size, filepath in vads: filename = PESymbols.filename_for_path(filepath) @@ -969,8 +1039,6 @@ class PESymbols(interfaces.plugins.PluginInterface): return proc_modules def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: - kernel = self.context.modules[self.config["kernel"]] - if self.config["symbols"]: filter_module = { self.config["module"].lower(): { @@ -995,7 +1063,7 @@ class PESymbols(interfaces.plugins.PluginInterface): module_resolver = self.get_process_modules collected_modules = module_resolver( - self.context, kernel.layer_name, kernel.symbol_table_name, filter_module + self.context, self.config["kernel"], filter_module ) found_symbols, _missing_symbols = PESymbols.find_symbols( diff --git a/volatility3/framework/plugins/windows/pedump.py b/volatility3/framework/plugins/windows/pedump.py index 858d0615a..9f125410c 100644 --- a/volatility3/framework/plugins/windows/pedump.py +++ b/volatility3/framework/plugins/windows/pedump.py @@ -3,7 +3,7 @@ # import logging import ntpath -from typing import List, Type, Optional +from typing import List, Type, Optional, Iterator, Tuple from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements @@ -18,7 +18,9 @@ class PEDump(interfaces.plugins.PluginInterface): """Allows extracting PE Files from a specific address in a specific address space""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + + # 2.0.0 - changed the signature of `dump_kernel_pe_at_base` + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -30,7 +32,10 @@ class PEDump(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -64,30 +69,27 @@ class PEDump(interfaces.plugins.PluginInterface): """ Returns the filename of the dump file or None """ - try: - file_handle = open_method(file_name) + with open_method(file_name) as file_handle: + try: + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=base, + layer_name=layer_name, + ) - dos_header = context.object( - pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset=base, - layer_name=layer_name, - ) + for offset, data in dos_header.reconstruct(): + file_handle.seek(offset) + file_handle.write(data) + except ( + OSError, + exceptions.VolatilityException, + OverflowError, + ValueError, + ) as excp: + vollog.debug(f"Unable to dump PE file at offset {base}: {excp}") + return None - for offset, data in dos_header.reconstruct(): - file_handle.seek(offset) - file_handle.write(data) - except ( - IOError, - exceptions.VolatilityException, - OverflowError, - ValueError, - ) as excp: - vollog.debug(f"Unable to dump PE file at offset {base}: {excp}") - return None - finally: - file_handle.close() - - return file_handle.preferred_filename + return file_handle.preferred_filename @classmethod def dump_ldr_entry( @@ -96,7 +98,7 @@ class PEDump(interfaces.plugins.PluginInterface): pe_table_name: str, ldr_entry: interfaces.objects.ObjectInterface, open_method: Type[interfaces.plugins.FileHandlerInterface], - layer_name: str = None, + layer_name: Optional[str] = None, prefix: str = "", ) -> Optional[str]: """Extracts the PE file referenced an LDR_DATA_TABLE_ENTRY (DLL, kernel module) instance @@ -119,12 +121,7 @@ class PEDump(interfaces.plugins.PluginInterface): if layer_name is None: layer_name = ldr_entry.vol.layer_name - file_name = "{}{}.{:#x}.{:#x}.dmp".format( - prefix, - ntpath.basename(name), - ldr_entry.vol.offset, - ldr_entry.DllBase, - ) + file_name = f"{prefix}{ntpath.basename(name)}.{ldr_entry.vol.offset:#x}.{ldr_entry.DllBase:#x}.dmp" return cls.dump_pe( context, @@ -146,20 +143,26 @@ class PEDump(interfaces.plugins.PluginInterface): pid: int, base: int, ) -> Optional[str]: - file_name = "PE.{:#x}.{:d}.{:#x}.dmp".format( - proc_offset, - pid, - base, - ) + file_name = f"PE.{proc_offset:#x}.{pid:d}.{base:#x}.dmp" return PEDump.dump_pe( context, pe_table_name, layer_name, open_method, file_name, base ) @classmethod - def dump_kernel_pe_at_base(cls, context, kernel, pe_table_name, open_method, base): + def dump_kernel_pe_at_base( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + pe_table_name: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], + base: int, + ) -> Iterator[Tuple[int, str, str]]: + """ + Extracts a PE file from kernel memory at the given base address + """ session_layers = modules.Modules.get_session_layers( - context, kernel.layer_name, kernel.symbol_table_name + context=context, kernel_module_name=kernel_module_name ) session_layer_name = modules.Modules.find_session_layer( @@ -194,8 +197,7 @@ class PEDump(interfaces.plugins.PluginInterface): for proc in pslist.PsList.list_processes( context=context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=kernel.name, filter_func=filter_func, ): pid = proc.UniqueProcessId @@ -227,16 +229,20 @@ class PEDump(interfaces.plugins.PluginInterface): ) if self.config["kernel_module"] and self.config["pid"]: - vollog.error("Only --kernel_module or --pid should be set. Not both") + vollog.error("Only 'kernel-module' or 'pid' should be set, not both") return if not self.config["kernel_module"] and not self.config["pid"]: - vollog.error("--kernel_module or --pid must be set") + vollog.error("Either 'kernel-module' or 'pid' argument must be set") return if self.config["kernel_module"]: pe_files = self.dump_kernel_pe_at_base( - self.context, kernel, pe_table_name, self.open, self.config["base"] + context=self.context, + kernel_module_name=self.config["kernel"], + pe_table_name=pe_table_name, + open_method=self.open, + base=self.config["base"], ) else: filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 1f70cfb8c..43dcd0482 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -55,6 +55,8 @@ class PoolConstraint: class PoolHeaderScanner(interfaces.layers.ScannerInterface): + _version = (1, 0, 0) + def __init__( self, module: interfaces.context.ModuleInterface, @@ -79,6 +81,7 @@ class PoolHeaderScanner(interfaces.layers.ScannerInterface): offset=offset - self._header_offset, absolute=True, ) + constraint = self._constraint_lookup[pattern] try: # Size check @@ -127,8 +130,8 @@ class PoolHeaderScanner(interfaces.layers.ScannerInterface): class PoolScanner(plugins.PluginInterface): """A generic pool scanner plugin.""" - _version = (1, 0, 0) _required_framework_version = (2, 0, 0) + _version = (3, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -138,8 +141,13 @@ class PoolScanner(plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="handles", plugin=handles.Handles, version=(1, 0, 0) + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="pool_header_scanner", + component=PoolHeaderScanner, + version=(1, 0, 0), ), ] @@ -150,7 +158,7 @@ class PoolScanner(plugins.PluginInterface): constraints = self.builtin_constraints(symbol_table) for constraint, mem_object, header in self.generate_pool_scan( - self.context, kernel.layer_name, symbol_table, constraints + self.context, self.config["kernel"], constraints ): # generate some type-specific info for sanity checking if constraint.object_type == "Process": @@ -182,8 +190,38 @@ class PoolScanner(plugins.PluginInterface): ) @staticmethod + def gui_poolscanner_constraints( + gui_table: str, tags_filter: Optional[List[bytes]] = None + ) -> List[PoolConstraint]: + """ + Constraints for objects managed by the GUI subsystem (win32k*.sys) + """ + builtins = [ + PoolConstraint( + b"Wind", + type_name=gui_table + constants.BANG + "tagWINDOWSTATION", + size=(0x90, None), + page_type=PoolType.PAGED | PoolType.NONPAGED, + object_type="WindowStation", + skip_type_test=True, + ), + PoolConstraint( + b"Desk", + type_name=gui_table + constants.BANG + "tagDESKTOP", + page_type=PoolType.PAGED | PoolType.NONPAGED, + object_type="Desktop", + skip_type_test=True, + ), + ] + + if not tags_filter: + return builtins + + return [constraint for constraint in builtins if constraint.tag in tags_filter] + + @classmethod def builtin_constraints( - symbol_table: str, tags_filter: List[bytes] = None + cls, symbol_table: str, tags_filter: Optional[List[bytes]] = None ) -> List[PoolConstraint]: """Get built-in PoolConstraints given a list of pool tags. @@ -205,6 +243,7 @@ class PoolScanner(plugins.PluginInterface): b"AtmT", type_name=symbol_table + constants.BANG + "_RTL_ATOM_TABLE", size=(200, None), + # TODO - update this after the GUI code goes on page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, ), # processes on windows before windows 8 @@ -214,7 +253,7 @@ class PoolScanner(plugins.PluginInterface): object_type="Process", size=(600, None), skip_type_test=True, - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # processes on windows starting with windows 8 PoolConstraint( @@ -223,7 +262,7 @@ class PoolScanner(plugins.PluginInterface): object_type="Process", size=(600, None), skip_type_test=True, - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # threads on windows before windows8 PoolConstraint( @@ -232,7 +271,7 @@ class PoolScanner(plugins.PluginInterface): object_type="Thread", size=(600, None), # -> 0x0258 - size of struct in win5.1 skip_type_test=True, - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # threads on windows starting with windows8 PoolConstraint( @@ -240,7 +279,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_ETHREAD", object_type="Thread", size=(600, None), # -> 0x0258 - size of struct in win5.1 - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # files on windows before windows 8 PoolConstraint( @@ -248,7 +287,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_FILE_OBJECT", object_type="File", size=(150, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # files on windows starting with windows 8 PoolConstraint( @@ -256,7 +295,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_FILE_OBJECT", object_type="File", size=(150, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # mutants on windows before windows 8 PoolConstraint( @@ -264,7 +303,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_KMUTANT", object_type="Mutant", size=(64, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # mutants on windows starting with windows 8 PoolConstraint( @@ -272,7 +311,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_KMUTANT", object_type="Mutant", size=(64, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # drivers on windows before windows 8 PoolConstraint( @@ -280,7 +319,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_DRIVER_OBJECT", object_type="Driver", size=(248, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, additional_structures=["_DRIVER_EXTENSION"], ), # drivers on windows starting with windows 8 @@ -289,14 +328,14 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_DRIVER_OBJECT", object_type="Driver", size=(248, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # kernel modules PoolConstraint( b"MmLd", type_name=symbol_table + constants.BANG + "_LDR_DATA_TABLE_ENTRY", size=(76, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.NONPAGED | PoolType.FREE, ), # symlinks on windows before windows 8 PoolConstraint( @@ -304,7 +343,7 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", object_type="SymbolicLink", size=(72, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.PAGED | PoolType.FREE, ), # symlinks on windows starting with windows 8 PoolConstraint( @@ -312,14 +351,14 @@ class PoolScanner(plugins.PluginInterface): type_name=symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", object_type="SymbolicLink", size=(72, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.PAGED | PoolType.FREE, ), # registry hives PoolConstraint( b"CM10", type_name=symbol_table + constants.BANG + "_CMHIVE", size=(800, None), - page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + page_type=PoolType.PAGED | PoolType.FREE, skip_type_test=True, ), ] @@ -330,11 +369,11 @@ class PoolScanner(plugins.PluginInterface): return [constraint for constraint in builtins if constraint.tag in tags_filter] @classmethod - def generate_pool_scan( + def generate_pool_scan_extended( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, + object_symbol_table_name: str, constraints: List[PoolConstraint], ) -> Generator[ Tuple[ @@ -346,49 +385,62 @@ class PoolScanner(plugins.PluginInterface): None, ]: """ + The extended version of `generate_pool_scan` to support pool scanning for objects outside of the kernel (ntoskrnl). + This requires the symbol table of the object being scanned for. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel + object_symbol_table_name: The name of the symbol table for the object being scanned for constraints: List of pool constraints used to limit the scan results - Returns: Iterable of tuples, containing the constraint that matched, the object from memory, the object header used to determine the object """ + kernel = context.modules[kernel_module_name] + # get the object type map type_map = handles.Handles.get_type_map( - context=context, layer_name=layer_name, symbol_table=symbol_table + context=context, kernel_module_name=kernel_module_name ) cookie = handles.Handles.find_cookie( - context=context, layer_name=layer_name, symbol_table=symbol_table + context=context, kernel_module_name=kernel_module_name ) - is_windows_10 = versions.is_windows_10(context, symbol_table) - is_windows_8_or_later = versions.is_windows_8_or_later(context, symbol_table) + is_windows_10 = versions.is_windows_10(context, kernel.symbol_table_name) + is_windows_8_or_later = versions.is_windows_8_or_later( + context, kernel.symbol_table_name + ) # start off with the primary virtual layer - scan_layer = layer_name + scan_layer = kernel.layer_name # switch to a non-virtual layer if necessary if not is_windows_10: scan_layer = context.layers[scan_layer].config["memory_layer"] - if symbols.symbol_table_is_64bit(context, symbol_table): + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ): alignment = 0x10 else: alignment = 8 + # scan in the main kernel layer for the object(s) for constraint, header in cls.pool_scan( - context, scan_layer, symbol_table, constraints, alignment=alignment + context, + kernel_module_name, + scan_layer, + object_symbol_table_name, + constraints, + alignment=alignment, ): mem_objects = header.get_object( constraint=constraint, use_top_down=is_windows_8_or_later, - native_layer_name=layer_name, - kernel_symbol_table=symbol_table, + native_layer_name=kernel.layer_name, + kernel_symbol_table=kernel.symbol_table_name, ) for mem_object in mem_objects: @@ -397,6 +449,7 @@ class PoolScanner(plugins.PluginInterface): constants.LOGLEVEL_VVV, f"Cannot create an instance of {constraint.type_name}", ) + continue if constraint.object_type is not None and not constraint.skip_type_test: @@ -417,10 +470,45 @@ class PoolScanner(plugins.PluginInterface): yield constraint, mem_object, header + @classmethod + def generate_pool_scan( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + constraints: List[PoolConstraint], + ) -> Generator[ + Tuple[ + PoolConstraint, + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + ], + None, + None, + ]: + """ + The original version of `generate_pool_scan` which is sufficient for objects in the kernel (ntoskrnl), + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the module for the kernel + constraints: List of pool constraints used to limit the scan results + + Returns: + Iterable of tuples, containing the constraint that matched, the object from memory, the object header used to determine the object + """ + + kernel = context.modules[kernel_module_name] + + # repeat the symbol table to match the original `generate_pool_scan` behaviour + yield from cls.generate_pool_scan_extended( + context, kernel_module_name, kernel.symbol_table_name, constraints + ) + @classmethod def pool_scan( cls, context: interfaces.context.ContextInterface, + kernel_module_name: str, layer_name: str, symbol_table: str, pool_constraints: List[PoolConstraint], @@ -454,8 +542,16 @@ class PoolScanner(plugins.PluginInterface): ) constraint_lookup[constraint.tag] = constraint - pool_header_table_name = cls.get_pool_header_table(context, symbol_table) - module = context.module(pool_header_table_name, layer_name, offset=0) + kernel = context.modules[kernel_module_name] + + if kernel.has_type("_POOL_HEADER"): + pool_header_table_name = kernel.symbol_table_name + else: + pool_header_table_name = cls.get_pool_header_table(context, symbol_table) + + module = context.module( + pool_header_table_name, layer_name, offset=kernel.offset + ) # Run the scan locating the offsets of a particular tag layer = context.layers[layer_name] @@ -473,41 +569,37 @@ class PoolScanner(plugins.PluginInterface): context: The context that the symbol tables does (or will) reside in symbol_table: The expected symbol_table to contain the _POOL_HEADER type """ - # Setup the pool header and offset differential - try: - context.symbol_space.get_type( - symbol_table + constants.BANG + "_POOL_HEADER" - ) - table_name = symbol_table - except exceptions.SymbolError: - # We have to manually load a symbol table + # We have to manually load a symbol table - if symbols.symbol_table_is_64bit(context, symbol_table): - is_win_7 = versions.is_windows_7(context, symbol_table) - if is_win_7: - pool_header_json_filename = "poolheader-x64-win7" - else: - pool_header_json_filename = "poolheader-x64" + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=symbol_table + ): + is_win_7 = versions.is_windows_7(context, symbol_table) + if is_win_7: + pool_header_json_filename = "poolheader-x64-win7" else: - pool_header_json_filename = "poolheader-x86" + pool_header_json_filename = "poolheader-x64" + else: + pool_header_json_filename = "poolheader-x86" - # set the class_type to match the normal WindowsKernelIntermedSymbols - is_vista_or_later = versions.is_vista_or_later(context, symbol_table) - if is_vista_or_later: - class_type = extensions.pool.POOL_HEADER_VISTA - else: - class_type = extensions.pool.POOL_HEADER + # set the class_type to match the normal WindowsKernelIntermedSymbols + is_vista_or_later = versions.is_vista_or_later(context, symbol_table) + if is_vista_or_later: + class_type = extensions.pool.POOL_HEADER_VISTA + else: + class_type = extensions.pool.POOL_HEADER + + table_name = intermed.IntermediateSymbolTable.create( + context=context, + config_path=configuration.path_join( + context.symbol_space[symbol_table].config_path, "poolheader" + ), + sub_path="windows", + filename=pool_header_json_filename, + table_mapping={"nt_symbols": symbol_table}, + class_types={"_POOL_HEADER": class_type}, + ) - table_name = intermed.IntermediateSymbolTable.create( - context=context, - config_path=configuration.path_join( - context.symbol_space[symbol_table].config_path, "poolheader" - ), - sub_path="windows", - filename=pool_header_json_filename, - table_mapping={"nt_symbols": symbol_table}, - class_types={"_POOL_HEADER": class_type}, - ) return table_name def run(self) -> renderers.TreeGrid: diff --git a/volatility3/framework/plugins/windows/privileges.py b/volatility3/framework/plugins/windows/privileges.py index 0370dfc92..6bc59bab6 100644 --- a/volatility3/framework/plugins/windows/privileges.py +++ b/volatility3/framework/plugins/windows/privileges.py @@ -39,7 +39,7 @@ class Privs(interfaces.plugins.PluginInterface): ) # Get service sids dictionary (we need only the service sids). - with open(sids_json_file_name, "r") as file_handle: + with open(sids_json_file_name) as file_handle: temp_json = json.load(file_handle)["privileges"] self.privilege_info = { int(priv_num): temp_json[priv_num] for priv_num in temp_json @@ -60,8 +60,8 @@ class Privs(interfaces.plugins.PluginInterface): element_type=int, optional=True, ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), ] @@ -107,7 +107,6 @@ class Privs(interfaces.plugins.PluginInterface): def run(self): filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - kernel = self.context.modules[self.config["kernel"]] return renderers.TreeGrid( [ @@ -121,8 +120,7 @@ class Privs(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/processghosting.py b/volatility3/framework/plugins/windows/processghosting.py index 50f02c926..24eb6fa9a 100644 --- a/volatility3/framework/plugins/windows/processghosting.py +++ b/volatility3/framework/plugins/windows/processghosting.py @@ -1,104 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -import contextlib - -from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import processghosting vollog = logging.getLogger(__name__) -class ProcessGhosting(interfaces.plugins.PluginInterface): - """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0""" +class ProcessGhosting( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=processghosting.ProcessGhosting, + removal_date="2026-06-07", +): + """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose (deprecated).""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - ] - - def _generator(self, procs): - kernel = self.context.modules[self.config["kernel"]] - - if not kernel.get_type("_EPROCESS").has_member("ImageFilePointer"): - vollog.warning( - "This plugin only supports Windows 10 builds when the ImageFilePointer member of _EPROCESS is present" - ) - return - - for proc in procs: - delete_pending = renderers.UnreadableValue() - process_name = utility.array_to_string(proc.ImageFileName) - - # if it is 0 then its a side effect of process ghosting - if proc.ImageFilePointer.vol.offset != 0: - try: - file_object = proc.ImageFilePointer - delete_pending = file_object.DeletePending - except exceptions.InvalidAddressException: - file_object = 0 - - # ImageFilePointer equal to 0 means process ghosting or similar techniques were used - else: - file_object = 0 - - if isinstance(delete_pending, int) and delete_pending not in [0, 1]: - vollog.debug( - f"Invalid delete_pending value {delete_pending} found for {process_name} {proc.UniqueProcessId}" - ) - - # delete_pending besides 0 or 1 = smear - if file_object == 0 or delete_pending == 1: - path = renderers.UnreadableValue() - if file_object: - with contextlib.suppress(exceptions.InvalidAddressException): - path = file_object.FileName.String - - yield ( - 0, - ( - proc.UniqueProcessId, - process_name, - format_hints.Hex(file_object), - delete_pending, - path, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_active_process_filter() - kernel = self.context.modules[self.config["kernel"]] - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("FILE_OBJECT", format_hints.Hex), - ("DeletePending", str), - ("Path", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=filter_func, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 478cc8b1b..1043f8b42 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -4,15 +4,15 @@ import datetime import logging -from typing import Callable, Iterator, List, Type +from typing import Callable, Iterator, List, Optional, Type -from volatility3.framework import renderers, interfaces, layers, exceptions, constants +from volatility3.framework import constants, exceptions, interfaces, layers, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows.extensions import pe from volatility3.framework.symbols.windows import extensions +from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins import timeliner vollog = logging.getLogger(__name__) @@ -22,7 +22,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the processes present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + + # 3.0.0 - changed signature for `list_processes` + _version = (3, 0, 1) PHYSICAL_DEFAULT = False @classmethod @@ -39,6 +41,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): default=cls.PHYSICAL_DEFAULT, optional=True, ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), requirements.ListRequirement( name="pid", element_type=int, @@ -114,7 +121,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def create_pid_filter( - cls, pid_list: List[int] = None, exclude: bool = False + cls, pid_list: Optional[List[int]] = None, exclude: bool = False ) -> Callable[[interfaces.objects.ObjectInterface], bool]: """A factory for producing filter functions that filter based on a list of process IDs. @@ -126,15 +133,24 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: Filter function for passing to the `list_processes` method """ - filter_func = lambda _: False + + def filter_func(_): + return False + # FIXME: mypy #4973 or #2608 pid_list = pid_list or [] filter_list = [x for x in pid_list if x is not None] if filter_list: if exclude: - filter_func = lambda x: x.UniqueProcessId in filter_list + + def filter_func(x): + return x.UniqueProcessId in filter_list + else: - filter_func = lambda x: x.UniqueProcessId not in filter_list + + def filter_func(x): + return x.UniqueProcessId not in filter_list + return filter_func @classmethod @@ -162,7 +178,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def create_name_filter( - cls, name_list: List[str] = None, exclude: bool = False + cls, name_list: Optional[List[str]] = None, exclude: bool = False ) -> Callable[[interfaces.objects.ObjectInterface], bool]: """A factory for producing filter functions that filter based on a list of process names. @@ -173,51 +189,57 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: Filter function for passing to the `list_processes` method """ - filter_func = lambda _: False + + def filter_func(_): + return False + # FIXME: mypy #4973 or #2608 name_list = name_list or [] filter_list = [x for x in name_list if x is not None] if filter_list: if exclude: - filter_func = ( - lambda x: utility.array_to_string(x.ImageFileName) in filter_list - ) + + def filter_func(x): + return utility.array_to_string(x.ImageFileName) in filter_list + else: - filter_func = ( - lambda x: utility.array_to_string(x.ImageFileName) - not in filter_list - ) + + def filter_func(x): + return utility.array_to_string(x.ImageFileName) not in filter_list + return filter_func @classmethod def list_processes( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, filter_func: Callable[ [interfaces.objects.ObjectInterface], bool ] = lambda _: False, ) -> Iterator["extensions.EPROCESS"]: - """Lists all the processes in the primary layer that are in the pid + """Lists all the processes in the given layer that are in the pid config option. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + layer_iname: The name of the layer on which to operate + symbol_table_name: The name of the table containing the kernel symbols filter_func: A function which takes an EPROCESS object and returns True if the process should be ignored/filtered Returns: The list of EPROCESS objects from the `layer_name` layer's PsActiveProcessHead list after filtering """ - # We only use the object factory to demonstrate how to use one - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + kernel = context.modules[kernel_module_name] - ps_aph_offset = ntkrnlmp.get_symbol("PsActiveProcessHead").address - list_entry = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=ps_aph_offset) + if not kernel.offset: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) + + ps_aph_offset = kernel.get_symbol("PsActiveProcessHead").address + list_entry = kernel.object(object_type="_LIST_ENTRY", offset=ps_aph_offset) # This is example code to demonstrate how to use symbol_space directly, rather than through a module: # @@ -230,18 +252,27 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # Note: "nt_symbols!_EPROCESS" could have been used, but would rely on the "nt_symbols" symbol table not already # having been present. Strictly, the value of the requirement should be joined with the BANG character # defined in the constants file - reloff = ntkrnlmp.get_type("_EPROCESS").relative_child_offset( + reloff = kernel.get_type("_EPROCESS").relative_child_offset( "ActiveProcessLinks" ) - eproc = ntkrnlmp.object( + eproc = kernel.object( object_type="_EPROCESS", offset=list_entry.vol.offset - reloff, absolute=True, ) - for proc in eproc.ActiveProcessLinks: - if not filter_func(proc): - yield proc + seen = set() + for forward in (True, False): + for proc in eproc.ActiveProcessLinks.to_list( + symbol_type=eproc.vol.type_name, + member="ActiveProcessLinks", + forward=forward, + ): + if proc.vol.offset in seen: + continue + seen.add(proc.vol.offset) + if not filter_func(proc): + yield proc def _generator(self): kernel = self.context.modules[self.config["kernel"]] @@ -256,8 +287,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): for proc in self.list_processes( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], filter_func=self.create_pid_filter(self.config.get("pid", None)), ): if not self.config.get("physical", self.PHYSICAL_DEFAULT): diff --git a/volatility3/framework/plugins/windows/psscan.py b/volatility3/framework/plugins/windows/psscan.py index 5ce470cd8..ae37c20a1 100644 --- a/volatility3/framework/plugins/windows/psscan.py +++ b/volatility3/framework/plugins/windows/psscan.py @@ -23,7 +23,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Scans for processes present in a particular windows memory image.""" _required_framework_version = (2, 3, 1) - _version = (1, 1, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -33,11 +33,19 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="info", component=info.Info, version=(1, 0, 0) + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -89,7 +97,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, layer_name: str, - offset: int = None, + offset: Optional[int] = None, physical: bool = True, exclude: bool = False, ) -> Callable[[interfaces.objects.ObjectInterface], bool]: @@ -102,29 +110,38 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: Filter function to be passed to the list of processes. """ - filter_func = lambda _: False + + def filter_func(_): + return False if offset: if physical: if exclude: - filter_func = ( - lambda proc: cls.physical_offset_from_virtual( - context, layer_name, proc + + def filter_func(proc): + return ( + cls.physical_offset_from_virtual(context, layer_name, proc) + == offset ) - == offset - ) + else: - filter_func = ( - lambda proc: cls.physical_offset_from_virtual( - context, layer_name, proc + + def filter_func(proc): + return ( + cls.physical_offset_from_virtual(context, layer_name, proc) + != offset ) - != offset - ) + else: if exclude: - filter_func = lambda proc: proc.vol.offset == offset + + def filter_func(proc): + return proc.vol.offset == offset + else: - filter_func = lambda proc: proc.vol.offset != offset + + def filter_func(proc): + return proc.vol.offset != offset return filter_func @@ -132,8 +149,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def scan_processes( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, filter_func: Callable[ [interfaces.objects.ObjectInterface], bool ] = lambda _: False, @@ -142,19 +158,20 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel Returns: A list of processes found by scanning the `layer_name` layer for process pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Pro\xe3", b"Proc"] + kernel.symbol_table_name, [b"Pro\xe3", b"Proc"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result if not filter_func(mem_object): @@ -164,16 +181,14 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def virtual_process_from_physical( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, proc: interfaces.objects.ObjectInterface, ) -> Optional[interfaces.objects.ObjectInterface]: """Returns a virtual process from a physical addressed one Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module inside the kernel proc: the process object with physical address Returns: @@ -181,12 +196,9 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ - version = cls.get_osversion(context, layer_name, symbol_table) + ntkrnlmp = context.modules[kernel_module_name] - # If it's WinXP->8.1 we have now a physical process address. - # We'll use the first thread to bounce back to the virtual process - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + version = cls.get_osversion(context, kernel_module_name) tleoffset = ntkrnlmp.get_type("_ETHREAD").relative_child_offset( "ThreadListEntry" @@ -196,7 +208,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # If (and only if) we're dealing with 64-bit Windows 7 SP1 # then add the other commonly seen member offset to the list - bits = context.layers[layer_name].bits_per_register + bits = context.layers[ntkrnlmp.layer_name].bits_per_register if version == (6, 1, 7601) and bits == 64: offsets.append(tleoffset + 8) @@ -213,7 +225,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # Sanity check the bounce. # This compares the original offset with the new one (translated from virtual layer) (_, _, ph_offset, _, _) = list( - context.layers[layer_name].mapping( + context.layers[ntkrnlmp.layer_name].mapping( offset=virtual_process.vol.offset, length=0 ) )[0] @@ -225,23 +237,20 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def get_osversion( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Tuple[int, int, int]: """Returns the complete OS version (MAJ,MIN,BUILD) Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols - + kernel_module_name: The name of the module for the kernel Returns: A tuple with (MAJ,MIN,BUILD) """ - kuser = info.Info.get_kuser_structure(context, layer_name, symbol_table) + kuser = info.Info.get_kuser_structure(context, kernel_module_name) nt_major_version = int(kuser.NtMajorVersion) nt_minor_version = int(kuser.NtMinorVersion) - vers = info.Info.get_version_structure(context, layer_name, symbol_table) + vers = info.Info.get_version_structure(context, kernel_module_name) build = vers.MinorVersion return (nt_major_version, nt_minor_version, build) @@ -256,8 +265,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): for proc in self.scan_processes( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], filter_func=pslist.PsList.create_pid_filter(self.config.get("pid", None)), ): file_output = "Disabled" @@ -269,8 +277,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): try: vproc = self.virtual_process_from_physical( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], proc, ) except exceptions.PagedInvalidAddressException: diff --git a/volatility3/framework/plugins/windows/pstree.py b/volatility3/framework/plugins/windows/pstree.py index 2be96277c..373c555f6 100644 --- a/volatility3/framework/plugins/windows/pstree.py +++ b/volatility3/framework/plugins/windows/pstree.py @@ -5,7 +5,7 @@ import datetime import logging from typing import Callable, Dict, Set, Tuple -from volatility3.framework import objects, interfaces, renderers, exceptions +from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import pslist @@ -14,8 +14,7 @@ vollog = logging.getLogger(__name__) class PsTree(interfaces.plugins.PluginInterface): - """Plugin for listing processes in a tree based on their parent process - ID.""" + """Plugin for listing processes in a tree based on their parent process ID.""" _required_framework_version = (2, 0, 0) @@ -41,7 +40,7 @@ class PsTree(interfaces.plugins.PluginInterface): optional=True, ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -53,7 +52,7 @@ class PsTree(interfaces.plugins.PluginInterface): def find_level( self, - pid: objects.Pointer, + pid: int, filter_func: Callable[ [interfaces.objects.ObjectInterface], bool ] = lambda _: False, @@ -86,7 +85,7 @@ class PsTree(interfaces.plugins.PluginInterface): kernel = self.context.modules[self.config["kernel"]] for proc in pslist.PsList.list_processes( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, kernel_module_name=self.config["kernel"] ): if not self.config.get("physical", pslist.PsList.PHYSICAL_DEFAULT): offset = proc.vol.offset @@ -105,7 +104,7 @@ class PsTree(interfaces.plugins.PluginInterface): process_pids = set([]) - def yield_processes(pid, descendant: bool = False): + def yield_processes(pid: int, descendant: bool = False): if pid in process_pids: vollog.debug(f"Pid cycle: already processed pid {pid}") return None diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index a8d185a2c..625f02387 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,32 +1,26 @@ -import datetime +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# import logging -import string -from itertools import chain -from typing import Dict, Iterable, List - -from volatility3.framework import constants, exceptions -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import TreeGrid, format_hints -from volatility3.framework.symbols.windows import extensions -from volatility3.plugins.windows import ( - handles, - info, - pslist, - psscan, - sessions, - thrdscan, -) +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import psxview vollog = logging.getLogger(__name__) -class PsXView(plugins.PluginInterface): - """Lists all processes found via four of the methods described in \"The Art of Memory Forensics,\" which may help - identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this - plugin's output in a terminal.""" +class PsXView( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=psxview.PsXView, + removal_date="2026-06-07", +): + """Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \ + identify processes that are trying to hide themselves. - # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality + We recommend using -r pretty if you are looking at this plugin's output in a terminal. + deprecated.""" + + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality # which the original plugin used to do it. # The sessions method is omitted because it begins with the list of processes found by Pslist anyway. @@ -36,222 +30,3 @@ class PsXView(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 0, 0) - - valid_proc_name_chars = set( - string.ascii_lowercase + string.ascii_uppercase + "." + " " - ) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="info", component=info.Info, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="psscan", component=psscan.PsScan, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="handles", component=handles.Handles, version=(1, 0, 0) - ), - requirements.BooleanRequirement( - name="physical-offsets", - description="List processes with physical offsets instead of virtual offsets.", - optional=True, - ), - ] - - def _proc_name_to_string(self, proc): - return proc.ImageFileName.cast( - "string", max_length=proc.ImageFileName.vol.count, errors="replace" - ) - - def _is_valid_proc_name(self, string: str) -> bool: - return all(c in self.valid_proc_name_chars for c in string) - - def _filter_garbage_procs( - self, proc_list: Iterable[extensions.EPROCESS] - ) -> List[extensions.EPROCESS]: - return [ - p - for p in proc_list - if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) - ] - - def _translate_offset(self, offset: int) -> int: - if not self.config["physical-offsets"]: - return offset - - kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name - - try: - _original_offset, _original_length, offset, _length, _layer_name = list( - self.context.layers[layer_name].mapping(offset=offset, length=0) - )[0] - except exceptions.PagedInvalidAddressException: - vollog.debug(f"Page fault: unable to translate {offset:0x}") - - return offset - - def _proc_list_to_dict( - self, tasks: Iterable[extensions.EPROCESS] - ) -> Dict[int, extensions.EPROCESS]: - tasks = self._filter_garbage_procs(tasks) - return {self._translate_offset(proc.vol.offset): proc for proc in tasks} - - def _check_pslist(self, tasks): - return self._proc_list_to_dict(tasks) - - def _check_psscan( - self, layer_name: str, symbol_table: str - ) -> Dict[int, extensions.EPROCESS]: - res = psscan.PsScan.scan_processes( - context=self.context, layer_name=layer_name, symbol_table=symbol_table - ) - - return self._proc_list_to_dict(res) - - def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: - ret = [] - - for ethread in thrdscan.ThrdScan.scan_threads( - self.context, module_name="kernel" - ): - process = None - try: - process = ethread.owning_process() - if not process.is_valid(): - continue - - ret.append(process) - except AttributeError: - vollog.log( - constants.LOGLEVEL_VVV, - "Unable to find the owning process of ethread", - ) - - return self._proc_list_to_dict(ret) - - def _check_csrss_handles( - self, tasks: Iterable[extensions.EPROCESS], layer_name: str, symbol_table: str - ) -> Dict[int, extensions.EPROCESS]: - ret: List[extensions.EPROCESS] = [] - - handles_plugin = handles.Handles( - context=self.context, config_path=self.config_path - ) - - type_map = handles_plugin.get_type_map(self.context, layer_name, symbol_table) - - cookie = handles_plugin.find_cookie( - context=self.context, - layer_name=layer_name, - symbol_table=symbol_table, - ) - - for p in tasks: - name = self._proc_name_to_string(p) - if name != "csrss.exe": - continue - - try: - ret += [ - handle.Body.cast("_EPROCESS") - for handle in handles_plugin.handles(p.ObjectTable) - if handle.get_object_type(type_map, cookie) == "Process" - ] - except exceptions.InvalidAddressException: - vollog.log( - constants.LOGLEVEL_VVV, "Cannot access eprocess object table" - ) - - return self._proc_list_to_dict(ret) - - def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - layer_name = kernel.layer_name - symbol_table = kernel.symbol_table_name - - kdbg_list_processes = list( - pslist.PsList.list_processes( - context=self.context, layer_name=layer_name, symbol_table=symbol_table - ) - ) - - # get processes from each source - processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} - - processes["pslist"] = self._check_pslist(kdbg_list_processes) - processes["psscan"] = self._check_psscan(layer_name, symbol_table) - processes["thrdscan"] = self._check_thrdscan() - processes["csrss"] = self._check_csrss_handles( - kdbg_list_processes, layer_name, symbol_table - ) - - # Unique set of all offsets from all sources - offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) - - for offset in offsets: - # We know there will be at least one process mapped to each offset - proc: extensions.EPROCESS = next( - mapping[offset] for mapping in processes.values() if offset in mapping - ) - - in_sources = {src: False for src in processes} - - for source, process_mapping in processes.items(): - if offset in process_mapping: - in_sources[source] = True - - pid = proc.UniqueProcessId - name = self._proc_name_to_string(proc) - - exit_time = proc.get_exit_time() - if type(exit_time) != datetime.datetime: - exit_time = "" - else: - exit_time = str(exit_time) - - yield ( - 0, - ( - format_hints.Hex(offset), - name, - pid, - in_sources["pslist"], - in_sources["psscan"], - in_sources["thrdscan"], - in_sources["csrss"], - exit_time, - ), - ) - - def run(self): - offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" - offset_str = "Offset" + offset_type - - return TreeGrid( - [ - (offset_str, format_hints.Hex), - ("Name", str), - ("PID", int), - ("pslist", bool), - ("psscan", bool), - ("thrdscan", bool), - ("csrss", bool), - ("Exit Time", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/registry/amcache.py b/volatility3/framework/plugins/windows/registry/amcache.py new file mode 100644 index 000000000..ed078993c --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/amcache.py @@ -0,0 +1,684 @@ +# 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 +# + +import dataclasses +import datetime +import enum +import itertools +import logging +from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry +from volatility3.framework.renderers import conversion +from volatility3.framework.symbols.windows.extensions import registry as reg_extensions +from volatility3.plugins import timeliner +from volatility3.plugins.windows.registry import hivelist + +vollog = logging.getLogger(__name__) + +####################################################################### +# More information about the following enums can be found in the report +# 'Analysis of the AmCache` by Blanche Lagny, 2019 +####################################################################### + + +class Win8FileValName(enum.Enum): + """ + An enumeration that creates a helpful mapping of opaque Windows 8 Amcache + 'File' subkey value names to their human-readable equivalent. + """ + + ProgramID = "100" + SHA1Hash = "101" + Product = "0" + Company = "1" + Size = "6" + SizeOfImage = "7" + PEHeaderChecksum = "9" + LastModTime = "11" # REG_QWORD FILETIME + CreateTime = "12" # REG_QWORD FILETIME + Path = "15" + LastModTime2 = "17" # REG_QWORD FILETIME + Version = "d" + CompileTime = "f" # REG_QWORD UNIX EPOCH + + +class Win8ProgramValName(enum.Enum): + """ + An enumeration that creates a helpful mapping of opaque Windows 8 Amcache + 'Program' subkey value names to their human-readable equivalent. + """ + + Product = "0" + Version = "1" + Publisher = "2" + InstallTime = "a" + MSIProductCode = "11" + MSIPackageCode = "12" + ProductCode = "f" + PackageCode = "10" + + +class Win10InvAppFileValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryApplicationFile' subkey value names. + """ + + FileId = "FileId" + LinkDate = "LinkDate" + LowerCaseLongPath = "LowerCaseLongPath" + ProductName = "ProductName" + ProductVersion = "ProductVersion" + ProgramID = "ProgramId" + Publisher = "Publisher" + + +class Win10InvAppValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryApplication' subkey value names. + """ + + InstallDate = "InstallDate" + Name = "Name" + Publisher = "Publisher" + RootDirPath = "RootDirPath" + Version = "Version" + + +class Win10DriverBinaryValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryDriverBinary' subkey value names. + """ + + DriverId = "DriverId" + DriverName = "DriverName" + DriverCompany = "DriverCompany" + Product = "Product" + Service = "Service" + DriverTimeStamp = "DriverTimeStamp" + + +class AmcacheEntryType(enum.IntEnum): + Driver = 1 + Program = 2 + File = 3 + + +NullableString = Union[str, None, interfaces.renderers.BaseAbsentValue] +NullableDatetime = Union[datetime.datetime, None, interfaces.renderers.BaseAbsentValue] + + +@dataclasses.dataclass +class _AmcacheEntry: + """ + A class containing all information about an entry from the Amcache registry hive. + Because all values could potentially be paged out of memory or malformed, they are all + a union between their expected value and `interfaces.renderers.BaseAbsentValue`. + """ + + entry_type: str + path: NullableString = renderers.NotApplicableValue() + company: NullableString = renderers.NotApplicableValue() + last_modify_time: NullableDatetime = renderers.NotApplicableValue() + last_modify_time_2: NullableDatetime = renderers.NotApplicableValue() + install_time: NullableDatetime = renderers.NotApplicableValue() + compile_time: NullableDatetime = renderers.NotApplicableValue() + sha1_hash: NullableString = renderers.NotApplicableValue() + service: NullableString = renderers.NotApplicableValue() + product_name: NullableString = renderers.NotApplicableValue() + product_version: NullableString = renderers.NotApplicableValue() + + +def _entry_sort_key(entry_tuple: Tuple[NullableString, _AmcacheEntry]) -> str: + """Sorts entries by program ID. This is broken out as a function here + to ensure consistency in sorting between the `group_by` and `sorted` function + invocations. + """ + program_id, _ = entry_tuple + key = program_id if isinstance(program_id, str) else "" + return key + + +def _get_string_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableString: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, bytes): + return renderers.UnparsableValue() + + return data.decode("utf-16le", errors="replace").rstrip("\u0000") + + +def _get_datetime_filetime_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, int): + return renderers.UnparsableValue() + + return conversion.wintime_to_datetime(data) + + +def _get_datetime_utc_epoch_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, (int, float)): + return renderers.UnparsableValue() + + try: + return datetime.datetime.fromtimestamp(float(data), datetime.timezone.utc) + except (ValueError, OverflowError, OSError): + return renderers.UnparsableValue() + + +def _get_datetime_str_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, int): + return renderers.UnparsableValue() + + if isinstance(data, str): + try: + return datetime.datetime.strptime(data, "%m/%d/%Y %H:%M:%S") + except ValueError: + return renderers.UnparsableValue() + else: + return renderers.UnparsableValue() + + +class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): + """Extract information on executed applications from the AmCache.""" + + _required_framework_version = (2, 0, 0) + + # 2.0.0 - changed the signature of get_amcache_hive + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + ] + + def generate_timeline( + self, + ) -> 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, + ) + 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, + ) + if isinstance(entry.install_time, datetime.datetime): + 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, + ) + + @classmethod + def get_amcache_hive( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Optional[registry.RegistryHive]: + """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" + return next( + hivelist.HiveList.list_hives( + context=context, + base_config_path=interfaces.configuration.path_join( + config_path, "hivelist" + ), + kernel_module_name=kernel_module_name, + filter_string="amcache", + ), + None, + ) + + @classmethod + def parse_file_key( + cls, file_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: + """Parses File entries from the Windows 8 `Root\\File` key. + + :param programs_key: The `Root\\File` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. + """ + + val_enum = Win8FileValName + + wanted_values = [key.value for key in val_enum] + + for file_entry_key in itertools.chain( + *(key.get_subkeys() for key in file_key.get_subkeys()) + ): + vollog.debug(f"Checking Win8 File key {file_entry_key.get_name()}") + values = { + str(value.get_name()): value + for value in file_entry_key.get_values() + if value.get_name() in wanted_values + } + + program_id = _get_string_value(values, val_enum.ProgramID.value) + path = _get_string_value(values, val_enum.Path.value) + company = _get_string_value(values, val_enum.Company.value) + last_mod_time = _get_datetime_filetime_value( + values, val_enum.LastModTime.value + ) + last_mod_time_2 = _get_datetime_filetime_value( + values, val_enum.LastModTime2.value + ) + install_time = _get_datetime_filetime_value( + values, val_enum.CreateTime.value + ) + compile_time = _get_datetime_utc_epoch_value( + values, val_enum.CompileTime.value + ) + sha1_hash = _get_string_value(values, val_enum.SHA1Hash.value) + 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 + ), + product_name=product_name, + ), + ) + + @classmethod + def parse_programs_key( + cls, programs_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[str, _AmcacheEntry]]: + """Parses Program entries from the Windows 8 `Root\\Programs` key. + + :param programs_key: The `Root\\Programs` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\File` entries, and the second member is the `AmcacheEntry`. + """ + val_enum = Win8ProgramValName + + wanted_values = [key.value for key in val_enum] + for program_key in programs_key.get_subkeys(): + values = { + str(value.get_name()): value + for value in program_key.get_values() + if value.get_name() in wanted_values + } + vollog.debug(f"Parsing Win8 Program key {program_key.get_name()}") + program_id = program_key.get_name().strip().strip("\u0000") + + product = _get_string_value(values, val_enum.Product.value) + company = _get_string_value(values, val_enum.Publisher.value) + install_time = _get_datetime_utc_epoch_value( + values, val_enum.InstallTime.value + ) + 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 + ), + install_time=install_time, + product_name=product, + product_version=version, + ), + ) + + @classmethod + def parse_inventory_app_key( + cls, inv_app_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[str, _AmcacheEntry]]: + """Parses InventoryApplication entries from the Windows 10 `Root\\InventoryApplication` key. + + :param programs_key: The `Root\\InventoryApplication` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\InventoryApplicationFile` entries, and the second member is the `AmcacheEntry`. + """ + val_enum = Win10InvAppValName + + wanted_values = [key.value for key in val_enum] + + for program_key in inv_app_key.get_subkeys(): + program_id = program_key.get_name() + + values = { + str(value.get_name()): value + for value in program_key.get_values() + if value.get_name() in wanted_values + } + + name = _get_string_value(values, val_enum.Name.value) + version = _get_string_value(values, val_enum.Version.value) + publisher = _get_string_value(values, val_enum.Publisher.value) + path = _get_string_value(values, val_enum.RootDirPath.value) + install_date = _get_datetime_str_value(values, val_enum.InstallDate.value) + last_mod = conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ) + + 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, + ), + ) + + @classmethod + def parse_inventory_app_file_key( + cls, inv_app_file_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: + """Parses executable file entries from the `Root\\InventoryApplicationFile` registry key. + + :param inv_app_file_key: The `Root\\InventoryApplicationFile` registry key. + :return: An iterator of tuples, where the first member is the program ID string for correlating + with it's parent `InventoryApplication` program entry, and the second member is the `Amcache` entry. + """ + + val_enum = Win10InvAppFileValName + + wanted_values = [key.value for key in val_enum] + + for file_key in inv_app_file_key.get_subkeys(): + vollog.debug( + f"Parsing Win10 InventoryApplicationFile key {file_key.get_name()}" + ) + + values = { + str(value.get_name()): value + for value in file_key.get_values() + if value.get_name() in wanted_values + } + + last_mod = conversion.wintime_to_datetime(file_key.LastWriteTime.QuadPart) + path = _get_string_value(values, val_enum.LowerCaseLongPath.value) + linkdate = _get_datetime_str_value(values, val_enum.LinkDate.value) + sha1_hash = _get_string_value(values, val_enum.FileId.value) + publisher = _get_string_value(values, val_enum.Publisher.value) + prod_name = _get_string_value(values, val_enum.ProductName.value) + 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 + ), + product_name=prod_name, + product_version=prod_ver, + ), + ) + + @classmethod + def parse_driver_binary_key( + cls, driver_binary_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[_AmcacheEntry]: + """Parses information about installed drivers from the `Root\\InventoryDriverBinary` registry key. + + :param driver_binary_key: The `Root\\InventoryDriverBinary` registry key + :return: An iterator of `AmcacheEntry`s + """ + val_enum = Win10DriverBinaryValName + + 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() + if value.get_name() in wanted_values + } + + # Depending on the Windows version, the key name will be either the name + # of the driver, or its SHA1 hash. + if "/" in str(binary_key.get_name()): + driver_name = str(binary_key.get_name()) + sha1_hash = _get_string_value(values, val_enum.DriverId.name) + else: + sha1_hash = str(binary_key.get_name()) + driver_name = _get_string_value(values, val_enum.DriverName.name) + + if isinstance(sha1_hash, str): + sha1_hash = sha1_hash[4:] if sha1_hash.startswith("0000") else sha1_hash + + company, product, service, last_write_time, driver_timestamp = ( + _get_string_value(values, val_enum.DriverCompany.name), + _get_string_value(values, val_enum.Product.name), + _get_string_value(values, val_enum.Service.name), + conversion.wintime_to_datetime(binary_key.LastWriteTime.QuadPart), + _get_datetime_utc_epoch_value(values, val_enum.DriverTimeStamp.name), + ) + + yield _AmcacheEntry( + entry_type=AmcacheEntryType.Driver.name, + path=driver_name, + company=company, + last_modify_time=last_write_time, + compile_time=driver_timestamp, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + service=service, + product_name=product, + ) + + def _generator(self) -> Iterator[Tuple[int, _AmcacheEntry]]: + def indented( + entry_gen: Iterable[_AmcacheEntry], indent: int = 0 + ) -> Iterator[Tuple[int, _AmcacheEntry]]: + for item in entry_gen: + yield indent, item + + # Building the dictionary ahead of time is much better for performance + # vs looking up each service's DLL individually. + amcache = self.get_amcache_hive( + self.context, self.config_path, self.config["kernel"] + ) + if amcache is None: + return + + try: + yield from indented( + self.parse_driver_binary_key( + amcache.get_key("Root\\InventoryDriverBinary") # type: ignore + ) + ) + except (KeyError, registry.RegistryException): + # Registry key not found + pass + + try: + programs: Dict[str, _AmcacheEntry] = { + program_id: entry + for program_id, entry in self.parse_programs_key( + amcache.get_key("Root\\Programs") + ) # type: ignore + } + except (KeyError, registry.RegistryException): + programs = {} + + try: + files = sorted( + list( + self.parse_file_key(amcache.get_key("Root\\File")), # type: ignore + ), + key=_entry_sort_key, + ) + except (KeyError, registry.RegistryException): + files = [] + + for program_id, file_entries in itertools.groupby( + files, + key=_entry_sort_key, + ): + files_indent = 0 + if isinstance(program_id, str): + try: + program_entry = programs.pop(program_id.strip().strip("\u0000")) + yield (0, program_entry) + + files_indent = 1 + except KeyError: + # No parent program for this file entry + pass + for _, entry in file_entries: + yield files_indent, entry + + for empty_program in programs.values(): + yield 0, empty_program + + try: + programs: Dict[str, _AmcacheEntry] = dict( + self.parse_inventory_app_key( + amcache.get_key("Root\\InventoryApplication") # type: ignore + ) + ) + except (KeyError, registry.RegistryException): + programs = {} + + try: + files = sorted( + list( + self.parse_inventory_app_file_key( + amcache.get_key("Root\\InventoryApplicationFile") + ), + # type: ignore + ), + key=_entry_sort_key, + ) + except (KeyError, registry.RegistryException): + files = [] + + for program_id, file_entries in itertools.groupby( + files, + key=_entry_sort_key, + ): + files_indent = 0 + + if isinstance(program_id, str): + try: + program_entry = programs.pop(program_id.strip().strip("\u0000")) + yield (0, program_entry) + files_indent = 1 + except KeyError: + # No parent program for this file entry + pass + + for _, entry in file_entries: + yield files_indent, entry + + for empty_program in programs.values(): + yield 0, empty_program + + def run(self): + return renderers.TreeGrid( + [ + ("EntryType", str), + ("Path", str), + ("Company", str), + ("LastModifyTime", datetime.datetime), + ("LastModifyTime2", datetime.datetime), + ("InstallTime", datetime.datetime), + ("CompileTime", datetime.datetime), + ("SHA1", str), + ("Service", str), + ("ProductName", str), + ("ProductVersion", str), + ], + ( + (indent, dataclasses.astuple(entry)) + for indent, entry in self._generator() + ), + ) diff --git a/volatility3/framework/plugins/windows/registry/cachedump.py b/volatility3/framework/plugins/windows/registry/cachedump.py new file mode 100644 index 000000000..49c5495c2 --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/cachedump.py @@ -0,0 +1,186 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from struct import unpack +from typing import Tuple + +from Crypto.Cipher import ARC4, AES +from Crypto.Hash import HMAC + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry +from volatility3.framework.symbols.windows import versions +from volatility3.plugins.windows.registry import hashdump, hivelist, lsadump + +vollog = logging.getLogger(__name__) + + +class Cachedump(interfaces.plugins.PluginInterface): + """Dumps lsa secrets from memory""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 2) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="lsadump", component=lsadump.Lsadump, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="hashdump", component=hashdump.Hashdump, version=(1, 1, 0) + ), + ] + + @classmethod + def get_nlkm( + cls, sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool + ): + return lsadump.Lsadump.get_secret_by_name( + sechive, "NL$KM", lsakey, is_vista_or_later + ) + + @classmethod + def decrypt_hash(cls, edata: bytes, nlkm: bytes, ch, xp: bool): + if xp: + hmac_md5 = HMAC.new(nlkm, ch) + rc4key = hmac_md5.digest() + rc4 = ARC4.new(rc4key) + data = rc4.encrypt(edata) # lgtm [py/weak-cryptographic-algorithm] + else: + # Based on code from http://lab.mediaservice.net/code/cachedump.rb + aes = AES.new(nlkm[16:32], AES.MODE_CBC, ch) + data = b"" + for i in range(0, len(edata), 16): + buf = edata[i : i + 16] + if len(buf) < 16: + buf += (16 - len(buf)) * b"\00" + data += aes.decrypt(buf) + return data + + @classmethod + def parse_cache_entry(cls, cache_data: bytes) -> Tuple[int, int, int, bytes, bytes]: + (uname_len, domain_len) = unpack(" Tuple[str, str, str, bytes]: + """Get the data from the cache and separate it into the username, domain name, and hash data""" + uname_offset = 72 + pad = 2 * ((uname_len / 2) % 2) + domain_offset = int(uname_offset + uname_len + pad) + pad = 2 * ((domain_len / 2) % 2) + domain_name_offset = int(domain_offset + domain_len + pad) + hashh = dec_data[:0x10] + username = dec_data[uname_offset : uname_offset + uname_len].decode( + "utf-16-le", "replace" + ) + domain = dec_data[domain_offset : domain_offset + domain_len].decode( + "utf-16-le", "replace" + ) + domain_name = dec_data[ + domain_name_offset : domain_name_offset + domain_name_len + ].decode("utf-16-le", "replace") + + return (username, domain, domain_name, hashh) + + def _generator(self, syshive, sechive): + if not syshive or not sechive: + if syshive is None: + vollog.warning("Unable to locate SYSTEM hive") + if sechive is None: + vollog.warning("Unable to locate SECURITY hive") + return None + + bootkey = hashdump.Hashdump.get_bootkey(syshive) + if not bootkey: + vollog.warning("Unable to find bootkey") + return None + + kernel = self.context.modules[self.config["kernel"]] + + vista_or_later = versions.is_vista_or_later( + context=self.context, symbol_table=kernel.symbol_table_name + ) + + lsakey = lsadump.Lsadump.get_lsa_key(sechive, bootkey, vista_or_later) + if not lsakey: + vollog.warning("Unable to find lsa key") + return None + + nlkm = self.get_nlkm(sechive, lsakey, vista_or_later) + if not nlkm: + vollog.warning("Unable to find nlkma key") + return None + + cache = hashdump.Hashdump.get_hive_key(sechive, "Cache") + if not cache: + vollog.warning("Unable to find cache key") + return None + + for cache_item in cache.get_values(): + if cache_item.Name == "NL$Control": + continue + + try: + data = sechive.read(cache_item.Data + 4, cache_item.DataLength) + except exceptions.InvalidAddressException: + continue + + if not data: + continue + + ( + uname_len, + domain_len, + domain_name_len, + enc_data, + ch, + ) = self.parse_cache_entry(data) + # Skip if nothing in this cache entry + if uname_len == 0 or len(ch) == 0: + continue + dec_data = self.decrypt_hash(enc_data, nlkm, ch, not vista_or_later) + + (username, domain, domain_name, hashh) = self.parse_decrypted_cache( + dec_data, uname_len, domain_len, domain_name_len + ) + yield (0, (username, domain, domain_name, hashh)) + + def run(self): + offset = self.config.get("offset", None) + + syshive = sechive = None + + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + kernel_module_name=self.config["kernel"], + hive_offsets=None if offset is None else [offset], + ): + if hive.get_name().split("\\")[-1].upper() == "SYSTEM": + syshive = hive + if hive.get_name().split("\\")[-1].upper() == "SECURITY": + sechive = hive + + return renderers.TreeGrid( + [("Username", str), ("Domain", str), ("Domain name", str), ("Hash", bytes)], + self._generator(syshive, sechive), + ) diff --git a/volatility3/framework/plugins/windows/registry/getcellroutine.py b/volatility3/framework/plugins/windows/registry/getcellroutine.py index 200a45a82..5f3b1dcaa 100644 --- a/volatility3/framework/plugins/windows/registry/getcellroutine.py +++ b/volatility3/framework/plugins/windows/registry/getcellroutine.py @@ -27,19 +27,17 @@ class GetCellRoutine(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), ] def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - collection = ssdt.SSDT.build_module_collection( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, kernel_module_name=self.config["kernel"] ) # walk each hive and validate that the GetCellRoutine handler @@ -47,8 +45,7 @@ class GetCellRoutine(interfaces.plugins.PluginInterface): for hive_object in hivelist.HiveList.list_hives( context=self.context, base_config_path=self.config_path, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], ): hive = hive_object.hive diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py new file mode 100644 index 000000000..19bd60e81 --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -0,0 +1,645 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import binascii +import hashlib +import logging +from struct import pack, unpack +from typing import List, Optional, Tuple + +from Crypto.Cipher import AES, ARC4, DES + +from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry as registry_layer +from volatility3.framework.symbols.windows.extensions import registry +from volatility3.plugins.windows.registry import hivelist + +vollog = logging.getLogger(__name__) + + +class Hashdump(interfaces.plugins.PluginInterface): + """Dumps user hashes from memory""" + + _required_framework_version = (2, 0, 0) + _version = (1, 1, 1) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + ] + + odd_parity = [ + 1, + 1, + 2, + 2, + 4, + 4, + 7, + 7, + 8, + 8, + 11, + 11, + 13, + 13, + 14, + 14, + 16, + 16, + 19, + 19, + 21, + 21, + 22, + 22, + 25, + 25, + 26, + 26, + 28, + 28, + 31, + 31, + 32, + 32, + 35, + 35, + 37, + 37, + 38, + 38, + 41, + 41, + 42, + 42, + 44, + 44, + 47, + 47, + 49, + 49, + 50, + 50, + 52, + 52, + 55, + 55, + 56, + 56, + 59, + 59, + 61, + 61, + 62, + 62, + 64, + 64, + 67, + 67, + 69, + 69, + 70, + 70, + 73, + 73, + 74, + 74, + 76, + 76, + 79, + 79, + 81, + 81, + 82, + 82, + 84, + 84, + 87, + 87, + 88, + 88, + 91, + 91, + 93, + 93, + 94, + 94, + 97, + 97, + 98, + 98, + 100, + 100, + 103, + 103, + 104, + 104, + 107, + 107, + 109, + 109, + 110, + 110, + 112, + 112, + 115, + 115, + 117, + 117, + 118, + 118, + 121, + 121, + 122, + 122, + 124, + 124, + 127, + 127, + 128, + 128, + 131, + 131, + 133, + 133, + 134, + 134, + 137, + 137, + 138, + 138, + 140, + 140, + 143, + 143, + 145, + 145, + 146, + 146, + 148, + 148, + 151, + 151, + 152, + 152, + 155, + 155, + 157, + 157, + 158, + 158, + 161, + 161, + 162, + 162, + 164, + 164, + 167, + 167, + 168, + 168, + 171, + 171, + 173, + 173, + 174, + 174, + 176, + 176, + 179, + 179, + 181, + 181, + 182, + 182, + 185, + 185, + 186, + 186, + 188, + 188, + 191, + 191, + 193, + 193, + 194, + 194, + 196, + 196, + 199, + 199, + 200, + 200, + 203, + 203, + 205, + 205, + 206, + 206, + 208, + 208, + 211, + 211, + 213, + 213, + 214, + 214, + 217, + 217, + 218, + 218, + 220, + 220, + 223, + 223, + 224, + 224, + 227, + 227, + 229, + 229, + 230, + 230, + 233, + 233, + 234, + 234, + 236, + 236, + 239, + 239, + 241, + 241, + 242, + 242, + 244, + 244, + 247, + 247, + 248, + 248, + 251, + 251, + 253, + 253, + 254, + 254, + ] + + # Permutation matrix for boot key + bootkey_perm_table = [ + 0x8, + 0x5, + 0x4, + 0x2, + 0xB, + 0x9, + 0xD, + 0x3, + 0x0, + 0x6, + 0x1, + 0xC, + 0xE, + 0xA, + 0xF, + 0x7, + ] + + # Constants for SAM decrypt algorithm + aqwerty = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" + anum = b"0123456789012345678901234567890123456789\0" + antpassword = b"NTPASSWORD\0" + almpassword = b"LMPASSWORD\0" + lmkey = b"KGS!@#$%" + + empty_lm = b"\xaa\xd3\xb4\x35\xb5\x14\x04\xee\xaa\xd3\xb4\x35\xb5\x14\x04\xee" + empty_nt = b"\x31\xd6\xcf\xe0\xd1\x6a\xe9\x31\xb7\x3c\x59\xd7\xe0\xc0\x89\xc0" + + @classmethod + def get_hive_key( + cls, hive: registry_layer.RegistryHive, key: str + ) -> Optional["registry.CM_KEY_NODE"]: + result = None + try: + if hive: + result = hive.get_key(key) + except (KeyError, registry_layer.RegistryException): + vollog.info( + f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image" + ) + return result + + @classmethod + def get_user_keys( + cls, samhive: registry_layer.RegistryHive + ) -> List[interfaces.objects.ObjectInterface]: + user_key_path = "SAM\\Domains\\Account\\Users" + + user_key = cls.get_hive_key(samhive, user_key_path) + + if not user_key: + return [] + return [k for k in user_key.get_subkeys() if k.get_name() != "Names"] + + @classmethod + def get_bootkey(cls, syshive: registry_layer.RegistryHive) -> Optional[bytes]: + """ + Returns the scrambled bootkey necessary to decrypt hashes + """ + cs = 1 + lsa_base = f"ControlSet{cs:03}" + "\\Control\\Lsa" + lsa_keys = ["JD", "Skew1", "GBG", "Data"] + + lsa = cls.get_hive_key(syshive, lsa_base) + if not lsa: + return None + + bootkey = "" + + for lk in lsa_keys: + try: + key = cls.get_hive_key(syshive, lsa_base + "\\" + lk) + class_data = None + if key: + try: + class_data = syshive.read(key.Class + 4, key.ClassLength) + except exceptions.InvalidAddressException: + return None + + if class_data is None: + return None + bootkey += class_data.decode("utf-16-le") + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ) as excp: + vollog.log( + constants.LOGLEVEL_VVV, f"Unable to read Lsa key {lk}: {excp}" + ) + return None + + bootkey_str = binascii.unhexlify(bootkey) + bootkey_scrambled = bytes( + [bootkey_str[cls.bootkey_perm_table[i]] for i in range(len(bootkey_str))] + ) + return bootkey_scrambled + + @classmethod + def get_hbootkey( + cls, samhive: registry_layer.RegistryHive, bootkey: bytes + ) -> Optional[bytes]: + sam_account_path = "SAM\\Domains\\Account" + + if not bootkey: + return None + + sam_account_key = cls.get_hive_key(samhive, sam_account_path) + if not sam_account_key: + return None + + sam_data = None + for v in sam_account_key.get_values(): + if v.get_name() == "F": + try: + sam_data = samhive.read(v.Data + 4, v.DataLength) + except exceptions.InvalidAddressException: + return None + + if not sam_data: + return None + + revision = sam_data[0x00] + if revision == 2: + md5 = hashlib.md5() + + md5.update(sam_data[0x70:0x80] + cls.aqwerty + bootkey + cls.anum) + rc4_key = md5.digest() + + rc4 = ARC4.new(rc4_key) + hbootkey = rc4.encrypt( + sam_data[0x80:0xA0] + ) # lgtm [py/weak-cryptographic-algorithm] + return hbootkey + elif revision == 3: + # AES encrypted + iv = sam_data[0x78:0x88] + encryptedHBootKey = sam_data[0x88:0xA8] + cipher = AES.new(bootkey, AES.MODE_CBC, iv) + hbootkey = cipher.decrypt(encryptedHBootKey) + return hbootkey[:16] + return None + + @classmethod + def decrypt_single_salted_hash( + cls, rid, hbootkey: bytes, enc_hash: bytes, _lmntstr, salt: bytes + ) -> Optional[bytes]: + (des_k1, des_k2) = cls.sid_to_key(rid) + des1 = DES.new(des_k1, DES.MODE_ECB) + des2 = DES.new(des_k2, DES.MODE_ECB) + cipher = AES.new(hbootkey[:16], AES.MODE_CBC, salt) + obfkey = cipher.decrypt(enc_hash) + return des1.decrypt(obfkey[:8]) + des2.decrypt( + obfkey[8:16] + ) # lgtm [py/weak-cryptographic-algorithm] + + @classmethod + def get_user_hashes( + cls, + user: registry.CM_KEY_NODE, + samhive: registry_layer.RegistryHive, + hbootkey: bytes, + ) -> Optional[Tuple[bytes, bytes]]: + ## Will sometimes find extra user with rid = NAMES, returns empty strings right now + try: + rid = int(str(user.get_name()), 16) + except ValueError: + return None + sam_data = None + for v in user.get_values(): + if v.get_name() == "V": + try: + sam_data = samhive.read(v.Data + 4, v.DataLength) + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ): + return None + + if not sam_data: + return None + + lm_offset = unpack(" Tuple[bytes, bytes]: + """Takes rid of a user and converts it to a key to be used by the DES cipher""" + bytestr1 = [ + sid & 0xFF, + (sid >> 8) & 0xFF, + (sid >> 16) & 0xFF, + (sid >> 24) & 0xFF, + ] + bytestr1 += bytestr1[0:3] + bytestr2 = [bytestr1[3]] + bytestr1[0:3] + bytestr2 += bytestr2[0:3] + return cls.sidbytes_to_key(bytes(bytestr1)), cls.sidbytes_to_key( + bytes(bytestr2) + ) + + @classmethod + def sidbytes_to_key(cls, s: bytes) -> bytes: + """Builds final DES key from the strings generated in sid_to_key""" + key = [ + s[0] >> 1, + ((s[0] & 0x01) << 6) | (s[1] >> 2), + ((s[1] & 0x03) << 5) | (s[2] >> 3), + ((s[2] & 0x07) << 4) | (s[3] >> 4), + ((s[3] & 0x0F) << 3) | (s[4] >> 5), + ((s[4] & 0x1F) << 2) | (s[5] >> 6), + ((s[5] & 0x3F) << 1) | (s[6] >> 7), + s[6] & 0x7F, + ] + for i in range(8): + key[i] = key[i] << 1 + key[i] = cls.odd_parity[key[i]] + return bytes(key) + + @classmethod + def decrypt_single_hash( + cls, rid: int, hbootkey: bytes, enc_hash: bytes, lmntstr: bytes + ): + (des_k1, des_k2) = cls.sid_to_key(rid) + des1 = DES.new(des_k1, DES.MODE_ECB) + des2 = DES.new(des_k2, DES.MODE_ECB) + md5 = hashlib.md5() + + md5.update(hbootkey[:0x10] + pack(" Optional[bytes]: + value = None + for v in user.get_values(): + if v.get_name() == "V": + try: + value = samhive.read(v.Data + 4, v.DataLength) + except exceptions.InvalidAddressException: + return None + + if not value: + return None + + name_offset = unpack(" len(value): + return None + + username = value[name_offset : name_offset + name_length] + return username + + # replaces the dump_hashes method in vol2 + def _generator( + self, syshive: registry_layer.RegistryHive, samhive: registry_layer.RegistryHive + ): + if syshive is None: + vollog.debug("SYSTEM address is None: No system hive found") + if samhive is None: + vollog.debug("SAM address is None: No SAM hive found") + bootkey = self.get_bootkey(syshive) + hbootkey = self.get_hbootkey(samhive, bootkey) + if hbootkey: + for user in self.get_user_keys(samhive): + ret = self.get_user_hashes(user, samhive, hbootkey) + if ret: + lmhash, nthash = ret + + ## temporary fix to prevent UnicodeDecodeError backtraces + ## however this can cause truncated user names as a result + name = self.get_user_name(user, samhive) + if name is None: + name = renderers.NotAvailableValue() + else: + name = str(name, "utf-16-le", errors="ignore") + + lmout = str(binascii.hexlify(lmhash or self.empty_lm), "latin-1") + ntout = str(binascii.hexlify(nthash or self.empty_nt), "latin-1") + rid = int(str(user.get_name()), 16) + yield (0, (name, rid, lmout, ntout)) + else: + vollog.warning("Hbootkey is not valid") + + def run(self): + offset = self.config.get("offset", None) + syshive = None + samhive = None + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + kernel_module_name=self.config["kernel"], + hive_offsets=None if offset is None else [offset], + ): + if hive.get_name().split("\\")[-1].upper() == "SYSTEM": + syshive = hive + if hive.get_name().split("\\")[-1].upper() == "SAM": + samhive = hive + + return renderers.TreeGrid( + [("User", str), ("rid", int), ("lmhash", str), ("nthash", str)], + self._generator(syshive, samhive), + ) diff --git a/volatility3/framework/plugins/windows/registry/hivelist.py b/volatility3/framework/plugins/windows/registry/hivelist.py index ddc9c1855..ec2fbc4c7 100644 --- a/volatility3/framework/plugins/windows/registry/hivelist.py +++ b/volatility3/framework/plugins/windows/registry/hivelist.py @@ -41,9 +41,11 @@ class HiveGenerator: class HiveList(interfaces.plugins.PluginInterface): """Lists the registry hives present in a particular memory image.""" - _version = (1, 0, 0) _required_framework_version = (2, 0, 0) + # 2.0.0 - changed the signature of list_hives + _version = (2, 0, 0) + @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ @@ -58,8 +60,8 @@ class HiveList(interfaces.plugins.PluginInterface): optional=True, default=None, ), - requirements.PluginRequirement( - name="hivescan", plugin=hivescan.HiveScan, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivescan", component=hivescan.HiveScan, version=(2, 0, 0) ), requirements.BooleanRequirement( name="dump", @@ -93,10 +95,9 @@ class HiveList(interfaces.plugins.PluginInterface): # Construct the hive hive = next( self.list_hives( - self.context, - self.config_path, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + context=self.context, + base_config_path=self.config_path, + kernel_module_name=self.config["kernel"], hive_offsets=[hive_object.vol.offset], ) ) @@ -137,8 +138,7 @@ class HiveList(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, base_config_path: str, - layer_name: str, - symbol_table: str, + kernel_module_name: str, filter_string: Optional[str] = None, hive_offsets: Optional[List[int]] = None, ) -> Iterator[registry.RegistryHive]: @@ -148,20 +148,24 @@ class HiveList(interfaces.plugins.PluginInterface): Args: context: The context to retrieve required elements (layers, symbol tables) from base_config_path: The configuration path for any settings required by the new table - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: The name of the module for the kernel filter_string: An optional string which must be present in the hive name if specified offset: An optional offset to specify a specific hive to iterate over (takes precedence over filter_string) Yields: A registry hive layer name """ + kernel = context.modules[kernel_module_name] + if hive_offsets is None: try: hive_offsets = [ hive.vol.offset for hive in cls.list_hive_objects( - context, layer_name, symbol_table, filter_string + context=context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string=filter_string, ) ] except ImportError: @@ -178,8 +182,9 @@ class HiveList(interfaces.plugins.PluginInterface): context=context, base_config_path=base_config_path, hive_offset=hive_offset, - base_layer=layer_name, - nt_symbols=symbol_table, + base_layer=kernel.layer_name, + nt_symbols=kernel.symbol_table_name, + kernel_module_name=kernel_module_name, ) try: @@ -215,7 +220,11 @@ class HiveList(interfaces.plugins.PluginInterface): """ # We only use the object factory to demonstrate how to use one - kvo = context.layers[layer_name].config["kernel_virtual_offset"] + kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None) + if not kvo: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) list_head = ntkrnlmp.get_symbol("CmpHiveListHead").address @@ -232,10 +241,8 @@ class HiveList(interfaces.plugins.PluginInterface): for hive in hg: if hive.vol.offset in seen: vollog.debug( - "Hivelist found an already seen offset {} while " - "traversing forwards, this should not occur".format( - hex(hive.vol.offset) - ) + f"Hivelist found an already seen offset {hex(hive.vol.offset)} while " + "traversing forwards, this should not occur" ) break seen.add(hive.vol.offset) @@ -249,18 +256,14 @@ class HiveList(interfaces.plugins.PluginInterface): forward_invalid = hg.invalid if forward_invalid: vollog.debug( - "Hivelist failed traversing the list forwards at {}, traversing backwards".format( - hex(forward_invalid) - ) + f"Hivelist failed traversing the list forwards at {hex(forward_invalid)}, traversing backwards" ) hg = HiveGenerator(cmhive, forward=False) for hive in hg: if hive.vol.offset in seen: vollog.debug( - "Hivelist found an already seen offset {} while " - "traversing backwards, list walking met in the middle".format( - hex(hive.vol.offset) - ) + f"Hivelist found an already seen offset {hex(hive.vol.offset)} while " + "traversing backwards, list walking met in the middle" ) break seen.add(hive.vol.offset) @@ -281,14 +284,10 @@ class HiveList(interfaces.plugins.PluginInterface): # by walking the list, so revert to scanning, and walk the list forwards and backwards from each # found hive vollog.debug( - "Hivelist failed traversing backwards at {}, a different " - "location from forwards, revert to scanning".format( - hex(backward_invalid) - ) + f"Hivelist failed traversing backwards at {hex(backward_invalid)}, a different " + "location from forwards, revert to scanning" ) - for hive in hivescan.HiveScan.scan_hives( - context, layer_name, symbol_table - ): + for hive in hivescan.HiveScan.scan_hives(context, ntkrnlmp.name): try: if hive.HiveList.Flink: start_hive_offset = hive.HiveList.Flink - reloff @@ -320,9 +319,7 @@ class HiveList(interfaces.plugins.PluginInterface): yield linked_hive except exceptions.InvalidAddressException: vollog.debug( - "InvalidAddressException when traversing hive {} found from scan, skipping".format( - hex(hive.vol.offset) - ) + f"InvalidAddressException when traversing hive {hex(hive.vol.offset)} found from scan, skipping" ) def run(self) -> renderers.TreeGrid: diff --git a/volatility3/framework/plugins/windows/registry/hivescan.py b/volatility3/framework/plugins/windows/registry/hivescan.py index 7b3c0b622..2ebc52f53 100644 --- a/volatility3/framework/plugins/windows/registry/hivescan.py +++ b/volatility3/framework/plugins/windows/registry/hivescan.py @@ -12,11 +12,10 @@ from volatility3.plugins.windows import poolscanner, bigpools class HiveScan(interfaces.plugins.PluginInterface): - """Scans for registry hives present in a particular windows memory - image.""" + """Scans for registry hives present in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -26,20 +25,17 @@ class HiveScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="bigpools", plugin=bigpools.BigPools, version=(1, 0, 0) + requirements.VersionRequirement( + name="bigpools", component=bigpools.BigPools, version=(2, 0, 0) ), ] @classmethod def scan_hives( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + cls, context: interfaces.context.ContextInterface, kernel_name: str ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for hives using the poolscanner module and constraints or bigpools module with tag. @@ -52,17 +48,22 @@ class HiveScan(interfaces.plugins.PluginInterface): A list of Hive objects as found from the `layer_name` layer based on Hive pool signatures """ - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) + kernel = context.modules[kernel_name] + + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) is_windows_8_1_or_later = versions.is_windows_8_1_or_later( - context=context, symbol_table=symbol_table + context=context, symbol_table=kernel.symbol_table_name ) if is_windows_8_1_or_later and is_64bit: - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = kernel for pool in bigpools.BigPools.list_big_pools( - context, layer_name=layer_name, symbol_table=symbol_table, tags=["CM10"] + context, + kernel_module_name=kernel_name, + tags=["CM10"], ): cmhive = ntkrnlmp.object( object_type="_CMHIVE", offset=pool.Va, absolute=True @@ -71,21 +72,17 @@ class HiveScan(interfaces.plugins.PluginInterface): else: constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"CM10"] + kernel.symbol_table_name, [b"CM10"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_name, constraints ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - for hive in self.scan_hives( - self.context, kernel.layer_name, kernel.symbol_table_name - ): + for hive in self.scan_hives(self.context, self.config["kernel"]): yield (0, (format_hints.Hex(hive.vol.offset),)) def run(self): diff --git a/volatility3/framework/plugins/windows/registry/lsadump.py b/volatility3/framework/plugins/windows/registry/lsadump.py new file mode 100644 index 000000000..50ecaebc1 --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/lsadump.py @@ -0,0 +1,258 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from struct import unpack +from typing import Optional +import hashlib + +from Crypto.Cipher import ARC4, DES, AES + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements + +from volatility3.framework.layers import registry as registry_layer +from volatility3.framework.symbols.windows import versions +from volatility3.plugins.windows.registry import hashdump, hivelist +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class Lsadump(interfaces.plugins.PluginInterface): + """Dumps lsa secrets from memory""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hashdump", component=hashdump.Hashdump, version=(1, 1, 0) + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + ] + + @classmethod + def decrypt_aes(cls, secret: bytes, key: bytes) -> bytes: + """ + Based on code from http://lab.mediaservice.net/code/cachedump.rb + """ + sha = hashlib.sha256() + sha.update(key) + for _i in range(1, 1000 + 1): + sha.update(secret[28:60]) + aeskey = sha.digest() + + data = b"" + for i in range(60, len(secret), 16): + aes = AES.new(aeskey, AES.MODE_CBC, b"\x00" * 16) + buf = secret[i : i + 16] + if len(buf) < 16: + buf += (16 - len(buf)) * "\00" + data += aes.decrypt(buf) + + return data + + @classmethod + def get_lsa_key( + cls, sechive: registry_layer.RegistryHive, bootkey: bytes, vista_or_later: bool + ) -> Optional[bytes]: + if not bootkey: + return None + + if vista_or_later: + policy_key = "PolEKList" + else: + policy_key = "PolSecretEncryptionKey" + + enc_reg_key = hashdump.Hashdump.get_hive_key(sechive, "Policy\\" + policy_key) + if not enc_reg_key: + return None + enc_reg_value = next(enc_reg_key.get_values(), None) + if not enc_reg_value: + return None + + try: + obf_lsa_key = sechive.read(enc_reg_value.Data + 4, enc_reg_value.DataLength) + except exceptions.InvalidAddressException: + return None + + if not obf_lsa_key: + return None + if not vista_or_later: + md5 = hashlib.md5() + md5.update(bootkey) + for _i in range(1000): + md5.update(obf_lsa_key[60:76]) + rc4key = md5.digest() + + rc4 = ARC4.new(rc4key) + lsa_key = rc4.decrypt( + obf_lsa_key[12:60] + ) # lgtm [py/weak-cryptographic-algorithm] + lsa_key = lsa_key[0x10:0x20] + else: + lsa_key = cls.decrypt_aes(obf_lsa_key, bootkey) + lsa_key = lsa_key[68:100] + return lsa_key + + @classmethod + def get_secret_by_name( + cls, + sechive: registry_layer.RegistryHive, + name: str, + lsakey: bytes, + is_vista_or_later: bool, + ) -> Optional[bytes]: + enc_secret_key = hashdump.Hashdump.get_hive_key( + sechive, "Policy\\Secrets\\" + name + "\\CurrVal" + ) + + secret = None + if enc_secret_key: + try: + enc_secret_value = next(enc_secret_key.get_values(), None) + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ): + enc_secret_value = None + + if enc_secret_value: + try: + enc_secret = sechive.read( + enc_secret_value.Data + 4, enc_secret_value.DataLength + ) + except exceptions.InvalidAddressExceptions: + return None + + if enc_secret: + if not is_vista_or_later: + secret = cls.decrypt_secret(enc_secret[0xC:], lsakey) + else: + secret = cls.decrypt_aes(enc_secret, lsakey) + + return secret + + @classmethod + def decrypt_secret(cls, secret: bytes, key: bytes) -> bytes: + """Python implementation of SystemFunction005. + + Decrypts a block of data with DES using given key. + Note that key can be longer than 7 bytes.""" + decrypted_data = b"" + j = 0 # key index + + for i in range(0, len(secret), 8): + enc_block = secret[i : i + 8] + block_key = key[j : j + 7] + des_key = hashdump.Hashdump.sidbytes_to_key(block_key) + des = DES.new(des_key, DES.MODE_ECB) + enc_block = enc_block + b"\x00" * int(abs(8 - len(enc_block)) % 8) + decrypted_data += des.decrypt( + enc_block + ) # lgtm [py/weak-cryptographic-algorithm] + j += 7 + if len(key[j : j + 7]) < 7: + j = len(key[j : j + 7]) + + (dec_data_len,) = unpack(" Iterable[ Tuple[ @@ -77,9 +77,19 @@ class PrintKey(interfaces.plugins.PluginInterface): return None node = node_path[-1] key_path_items = [hive] + node_path[1:] - key_path = "\\".join([k.get_name() for k in key_path_items]) + key_path_names = [] + for k in key_path_items: + try: + key_path_names.append(k.get_name()) + except ( + registry_layer.InvalidAddressException, + registry_layer.RegistryException, + ): + key_path_names.append("-") + key_path = "\\".join([k for k in key_path_names]) + if node.vol.type_name.endswith(constants.BANG + "_CELL_DATA"): - raise RegistryFormatException( + raise registry_layer.RegistryFormatException( hive.name, "Encountered _CELL_DATA instead of _CM_KEY_NODE" ) last_write_time = conversion.wintime_to_datetime(node.LastWriteTime.QuadPart) @@ -99,7 +109,10 @@ class PrintKey(interfaces.plugins.PluginInterface): if key_node.vol.offset not in [x.vol.offset for x in node_path]: try: key_node.get_name() - except exceptions.InvalidAddressException as excp: + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ) as excp: vollog.debug(excp) continue @@ -120,8 +133,8 @@ class PrintKey(interfaces.plugins.PluginInterface): def _printkey_iterator( self, - hive: RegistryHive, - node_path: Sequence[objects.StructType] = None, + hive: registry_layer.RegistryHive, + node_path: Optional[Sequence[objects.StructType]] = None, recurse: bool = False, ): """Method that wraps the more generic key_iterator, to provide output @@ -148,7 +161,7 @@ class PrintKey(interfaces.plugins.PluginInterface): key_node_name = node.get_name() except ( exceptions.InvalidAddressException, - RegistryFormatException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) key_node_name = renderers.UnreadableValue() @@ -175,16 +188,16 @@ class PrintKey(interfaces.plugins.PluginInterface): value_node_name = node.get_name() or "(Default)" except ( exceptions.InvalidAddressException, - RegistryFormatException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_node_name = renderers.UnreadableValue() try: - value_type = RegValueTypes(node.Type).name + value_type = registry.RegValueTypes(node.Type).name except ( exceptions.InvalidAddressException, - RegistryFormatException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_type = renderers.UnreadableValue() @@ -204,11 +217,17 @@ class PrintKey(interfaces.plugins.PluginInterface): value_data = format_hints.MultiTypeData( value_data, encoding="utf-8" ) - elif RegValueTypes(node.Type) == RegValueTypes.REG_BINARY: + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_BINARY + ): value_data = format_hints.MultiTypeData( value_data, show_hex=True ) - elif RegValueTypes(node.Type) == RegValueTypes.REG_MULTI_SZ: + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_MULTI_SZ + ): value_data = format_hints.MultiTypeData( value_data, encoding="utf-16-le", split_nulls=True ) @@ -219,7 +238,7 @@ class PrintKey(interfaces.plugins.PluginInterface): except ( ValueError, exceptions.InvalidAddressException, - RegistryFormatException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_data = renderers.UnreadableValue() @@ -240,17 +259,14 @@ class PrintKey(interfaces.plugins.PluginInterface): def _registry_walker( self, - layer_name: str, - symbol_table: str, - hive_offsets: List[int] = None, - key: str = None, + hive_offsets: Optional[List[int]] = None, + key: Optional[str] = None, recurse: bool = False, ): for hive in hivelist.HiveList.list_hives( - self.context, - self.config_path, - layer_name=layer_name, - symbol_table=symbol_table, + context=self.context, + base_config_path=self.config_path, + kernel_module_name=self.config["kernel"], hive_offsets=hive_offsets, ): try: @@ -264,13 +280,13 @@ class PrintKey(interfaces.plugins.PluginInterface): except ( exceptions.InvalidAddressException, KeyError, - RegistryFormatException, + registry_layer.RegistryException, ) as excp: if isinstance(excp, KeyError): vollog.debug( f"Key '{key}' not found in Hive at offset {hex(hive.hive_offset)}." ) - elif isinstance(excp, RegistryFormatException): + elif isinstance(excp, registry_layer.RegistryException): vollog.debug(excp) elif isinstance(excp, exceptions.InvalidAddressException): vollog.debug( @@ -292,9 +308,8 @@ class PrintKey(interfaces.plugins.PluginInterface): def run(self): offset = self.config.get("offset", None) - kernel = self.context.modules[self.config["kernel"]] - return TreeGrid( + return renderers.TreeGrid( columns=[ ("Last Write Time", datetime.datetime), ("Hive Offset", format_hints.Hex), @@ -305,8 +320,6 @@ class PrintKey(interfaces.plugins.PluginInterface): ("Volatile", bool), ], generator=self._registry_walker( - kernel.layer_name, - kernel.symbol_table_name, hive_offsets=None if offset is None else [offset], key=self.config.get("key", None), recurse=self.config.get("recurse", None), diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py new file mode 100644 index 000000000..a3e5fabe2 --- /dev/null +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -0,0 +1,1444 @@ +# 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 base64 +import binascii +import dataclasses +import datetime +import enum +import io +import itertools +import logging +import struct +from typing import Dict, Iterator, List, Optional, Tuple, Union + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry +from volatility3.framework.renderers import conversion +from volatility3.framework.symbols.windows.extensions import registry as reg_extensions +from volatility3.plugins import timeliner +from volatility3.plugins.windows.registry import hivelist + +vollog = logging.getLogger(__name__) + +# Reference: https://cyber.wtf/2022/06/01/windows-registry-analysis-todays-episode-tasks/ + + +class TimeMode(enum.Enum): + """ + Enumeration containing the different time modes that a 'Time' trigger can be configured to run in. + """ + + Once = "Once" + + # run at and repeat every days + Daily = "Daily" + + # run on days of week <(data2 as day_of_week bitmap)> every weeks starting at + Weekly = "Weekly" + + # run in months <(data3 as months bitmap> on days <(data2:data1 as day in month bitmap)> + # starting at + DaysInMonths = "Days In Months" + + # run in months <(data3 as months bitmap> in weeks <(data2 as week bitmap)> + # on days <(data1 as day_of_week bitmap)> starting at + DaysInWeeksInMonths = "Days In Weeks in Months" + + Unknown = "Unknown" + + +TIME_MODE_DESCRIPTION = { + TimeMode.Once: "Once", + TimeMode.Daily: "Daily", + TimeMode.Weekly: "Weekly", + TimeMode.DaysInMonths: "Days In Months", + TimeMode.DaysInWeeksInMonths: "Days In Weeks In Months", + TimeMode.Unknown: "Unknown", +} + + +class ActionType(enum.Enum): + """ + Enumeration that maps action types to their magic number encodings + """ + + Exe = 0x6666 + ComHandler = 0x7777 + Email = 0x8888 + MessageBox = 0x9999 + + +class TriggerType(enum.Enum): + """ + Enumeration that maps trigger types to their magic number encodings + """ + + WindowsNotificationFacility = 0x6666 + Session = 0x7777 + Registration = 0x8888 + Logon = 0xAAAA + Event = 0xCCCC + Time = 0xDDDD + Idle = 0xEEEE + Boot = 0xFFFF + + +class Weekday(enum.Enum): + """ + Enumeration that contains bitwise values for days of the week. + """ + + Sunday = 0x1 + Monday = 0x2 + Tuesday = 0x4 + Wednesday = 0x8 + Thursday = 0x10 + Friday = 0x20 + Saturday = 0x40 + + +class Months(enum.Enum): + """ + Enumeration that contains bitwise values for months of the year. + """ + + January = 0x1 + February = 0x2 + March = 0x4 + April = 0x8 + May = 0x10 + June = 0x20 + July = 0x40 + August = 0x80 + September = 0x100 + October = 0x200 + November = 0x400 + December = 0x800 + + +class SidType(enum.Enum): + """ + Enumeration that maps SID types to their encoded integer values + """ + + User = 1 + Group = 2 + Domain = 3 + Alias = 4 + WellKnownGroup = 5 + DeletedAccount = 6 + Invalid = 7 + Unknown = 8 + Computer = 9 + Label = 10 + LogonSession = 11 + + +@dataclasses.dataclass +class TaskSchedulerTimePeriod: + """ + Class containing information delimiting time periods within scheduled tasks. + """ + + years: int + months: int + weeks: int + days: int + hours: int + minutes: int + seconds: int + + +JOB_BUCKET_FLAGS = { + 0x2: "Run only if idle", + 0x4: "Restart on idle", + 0x8: "Stop on idle end", + 0x10: "Disallow start if on batteries", + 0x20: "Stop if going on batteries", + 0x40: "Start when available", + 0x80: "Run only if network available", + 0x100: "Allow start on demand", + 0x200: "Wake to run", + 0x400: "Execute parallel", + 0x800: "Execute stop existing", + 0x1000: "Execute queue", + 0x2000: "Execute ignore new", + 0x4000: "Logon type s4u", + 0x10000: "Logon type InteractiveToken", + 0x40000: "Logon type Password", + 0x80000: "Logon type InteractiveTokenOrPassword", + 0x400000: "Enabled", + 0x800000: "Hidden", + 0x1000000: "Runlevel highest available", + 0x2000000: "Task", + 0x4000000: "Version", + 0x8000000: "Token SID type none", + 0x10000000: "Token SID type unrestricted", + 0x20000000: "Interval", + 0x40000000: "Allow hard terminate", +} + +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() + if filetime is None: + return None + + return filetime + + def read_bool(self, aligned=False) -> Optional[bool]: + try: + val = struct.unpack("?", self.read(1))[0] + if aligned: + self.seek(7) + return val + except struct.error: + return None + + def decode_filetime(self) -> Optional[datetime.datetime]: + filetime = self.read_u8() + if filetime is None: + return None + + if filetime == 0 or filetime == 0xFFFFFFFFFFFFFFFF: + return None + filetime = conversion.wintime_to_datetime(filetime) + if isinstance(filetime, datetime.datetime): + return filetime + else: + return None + + def _read_uint( + self, size: int, format: str, aligned: bool = False + ) -> Optional[int]: + try: + val = struct.unpack(format, self.read(size))[0] + if aligned: + self.seek(8 - size, io.SEEK_CUR) + return val + except struct.error: + return None + + def read_aligned_u1(self) -> Optional[int]: + return self._read_uint(1, "B", True) + + def read_u2(self) -> Optional[int]: + return self._read_uint(2, " Optional[int]: + return self._read_uint(2, " Optional[int]: + return self._read_uint(4, " Optional[int]: + return self._read_uint(8, " Optional[int]: + return self._read_uint(4, " Optional[bytes]: + count = self.read_u4() if not aligned else self.read_aligned_u4() + if count is None: + return None + data = self.read(count) + if aligned: + self.seek((8 - (count % 8)) % 8, io.SEEK_CUR) + return data + + def read_bstring(self, aligned=False) -> Optional[str]: + size = self.read_u4() if not aligned else self.read_aligned_u4() + if size is None: + return None + try: + raw = self.read(size) + val = raw.decode("utf-16le", errors="replace").rstrip(NULL) or None + except UnicodeDecodeError: + val = None + + if aligned: + self.seek((8 - (size % 8)) % 8, io.SEEK_CUR) + + return val + + def read_aligned_bstring_expand_sz(self) -> Optional[str]: + sz = self.read_aligned_u4() + if sz is None: + return None + byte_count = sz * 2 + 2 + + if sz == 0: + return None + + try: + content = self.read(byte_count).decode("utf-16le") + except UnicodeDecodeError: + content = None + + self.seek((8 - (byte_count % 8)) % 8, io.SEEK_CUR) + return content.rstrip("\x00") if content is not None else None + + def read_tstimeperiod(self) -> Optional[TaskSchedulerTimePeriod]: + values = ( + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + ) + + if any(value is None for value in values): + return None + + return TaskSchedulerTimePeriod(*values) + + +def _build_guid_name_map(key: reg_extensions.CM_KEY_NODE) -> Dict[str, str]: + mapping = {} + task_id_value = None + for value in key.get_values(): + try: + if value.get_name() == "Id": + task_id_value = value + break + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): + continue + + if ( + task_id_value is not None + and task_id_value.get_type() == reg_extensions.RegValueTypes.REG_SZ + ): + try: + id_str = task_id_value.decode_data() + except exceptions.InvalidAddressException: + id_str = None + + try: + if isinstance(id_str, bytes): + mapping[id_str.decode("utf-16le", errors="replace").rstrip(NULL)] = str( + key.get_name() + ) + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ) as excp: + vollog.debug(f"Exception occurred while decoding id_str: {excp}") + + for subkey in key.get_subkeys(): + mapping.update(_build_guid_name_map(subkey)) + return mapping + + +@dataclasses.dataclass +class TaskAction: + action_type: ActionType + action: str + action_args: Optional[str] + working_directory: Optional[str] + + @classmethod + def decode_messagebox_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + caption, content = reader.read_bstring(), reader.read_bstring() + return cls( + ActionType.MessageBox, + f'"{caption or ""}": {content or ""}', + None, + None, + ) + + @classmethod + def _decode_exe_action( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskAction"]: + command = reader.read_bstring() + args = reader.read_bstring() + if command is None or args is None: + return None + + workdir = reader.read_bstring() + if version == 3: + _flags = reader.read_u2() + + return cls(ActionType.Exe, command, args, workdir) + + @classmethod + def _decode_email_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + props = { + "From": reader.read_bstring(), + "To": reader.read_bstring(), + "Cc": reader.read_bstring(), + "Bcc": reader.read_bstring(), + "Reply_to": reader.read_bstring(), + "Server": reader.read_bstring(), + "Subject": reader.read_bstring(), + "Body": reader.read_bstring(), + } + + num_attachment_filenames = reader.read_u4() + if num_attachment_filenames is not None: + attachment_filenames = [ + reader.read_bstring() for _ in range(num_attachment_filenames) + ] + + props["Attachments"] = ( + "<" + + ", ".join( + filename + for filename in attachment_filenames + if filename is not None + ) + + ">" + ) + + num_headers = reader.read_u4() + if num_headers is not None: + headers = [ + (reader.read_bstring(), reader.read_bstring()) + for _ in range(num_headers) + ] + + props["Headers"] = ( + "<" + + ", ".join( + f"{field}: {value}" + for field, value in headers + if field is not None and value is not None + ) + + ">" + ) + + cls( + ActionType.Email, + ", ".join( + f"{key}: {value}" for key, value in props.items() if value is not None + ), + None, + None, + ) + + @classmethod + def _decode_comhandler_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + guid_raw = reader.read(16) + if not guid_raw and len(guid_raw) == 16: + return None + clsid = conversion.windows_bytes_to_guid(guid_raw) + args = reader.read_bstring() + + return cls(ActionType.ComHandler, clsid, args, None) + + +@dataclasses.dataclass +class _ScheduledTaskEntry: + name: Union[str, interfaces.renderers.BaseAbsentValue] + principal_id: Union[str, interfaces.renderers.BaseAbsentValue] + display_name: Union[str, interfaces.renderers.BaseAbsentValue] + enabled: Union[bool, interfaces.renderers.BaseAbsentValue] + creation_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + last_run_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + last_successful_run_time: Union[ + datetime.datetime, interfaces.renderers.BaseAbsentValue + ] + trigger_type: Union[str, interfaces.renderers.BaseAbsentValue] + trigger_description: Union[str, interfaces.renderers.BaseAbsentValue] + action_type: Union[str, interfaces.renderers.BaseAbsentValue] + action_description: Union[str, interfaces.renderers.BaseAbsentValue] + action_args: Union[str, interfaces.renderers.BaseAbsentValue] + action_context: Union[str, interfaces.renderers.BaseAbsentValue] + working_directory: Union[str, interfaces.renderers.BaseAbsentValue] + guid: str + + +@dataclasses.dataclass +class _JobSchedule: + start_boundary: Optional[datetime.datetime] + end_boundary: Optional[datetime.datetime] + repetition_interval_secs: Optional[int] + repetition_duration_secs: Optional[int] + execution_time_limit_secs: Optional[int] + mode: Optional[TimeMode] + data1: Optional[int] + data2: Optional[int] + data3: Optional[int] + stop_tasks_at_duration_end: Optional[int] + is_enabled: Optional[bool] + max_delay_seconds: Optional[int] + + def get_description(self) -> Optional[str]: + if self.mode == TimeMode.Once: + return "Run one time starting at {}".format( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ) + + elif self.mode == TimeMode.Daily: + if self.data1 is None: + return None + return "Run at {} and repeat every {} days".format( + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + self.data1, + ) + + elif self.mode == TimeMode.Weekly: + if self.data2 is None: + return None + + days = [k.name for k in Weekday if k.value & self.data2] + return "Run on {} every {} weeks starting at {}".format( + ", ".join(days), + self.data1, + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + elif self.mode == TimeMode.DaysInMonths: + if self.data2 is None or self.data1 is None or self.data3 is None: + return None + months = [month.name for month in Months if month.value & self.data3] + days_bitmap = (self.data2 << 16) + self.data1 + days = [str(v + 1) for v in range(31) if (1 << v) & days_bitmap] + return "Run in months {} on days {} starting at {}".format( + ", ".join(months), + ", ".join(days), + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + elif self.mode == TimeMode.DaysInWeeksInMonths: + if self.data1 is None or self.data2 is None or self.data3 is None: + return None + + months = [month.name for month in Months if month.value & self.data3] + weeks = [str(v + 1) for v in range(5) if (v << 1) & self.data2] + days = [day.name for day in Weekday if day.value & self.data1] + return "Run in months {} in weeks {} on days {} starting at {}".format( + ", ".join(months), + ", ".join(weeks), + ", ".join(days), + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + else: + return None + + @classmethod + def decode(cls, reader: _ScheduledTasksReader) -> Optional["_JobSchedule"]: + start_boundary = reader.read_task_scheduler_time() + end_boundary = reader.read_task_scheduler_time() + + _ = reader.read_task_scheduler_time() + repetition_interval_secs = reader.read_u4() + repetition_duration_secs = reader.read_u4() + execution_time_limit_secs = reader.read_u4() + mode_index = reader.read_u4() + if mode_index is not None: + try: + mode = TimeMode(mode_index) + except ValueError: + mode = TimeMode.Unknown + else: + mode = None + + data1 = reader.read_u2() + data2 = reader.read_u2() + data3 = reader.read_u2() + + reader.seek(2, io.SEEK_CUR) # pad + stop_tasks_at_duration_end = reader.read_bool() + is_enabled = reader.read_bool() + reader.seek(6, io.SEEK_CUR) # pad (2) + unknown (4) + max_delay_seconds = reader.read_u4() + reader.seek(4, io.SEEK_CUR) # pad + + return cls( + start_boundary, + end_boundary, + repetition_interval_secs, + repetition_duration_secs, + execution_time_limit_secs, + mode, + data1, + data2, + data3, + stop_tasks_at_duration_end, + is_enabled, + max_delay_seconds, + ) + + +def decode_sid(data: bytes) -> Optional[str]: + """ + Decodes a windows SID from variable-length raw bytes + + Returns the string representation of the SID if decoding was successful, or None + if the data could not be parsed due to an insufficient number of bytes. + """ + try: + revision, subid_count, id_authority = struct.unpack( + ">BBQ", data[:2] + b"\x00\x00" + data[2:8] + ) + subauthorities = struct.unpack( + "<" + "I" * subid_count, data[8 : 8 + subid_count * 4] + ) + sid_string = "S-" + "-".join( + [str(item) for item in [revision, id_authority] + list(subauthorities)] + ) + except struct.error: + return None + + return sid_string + + +@dataclasses.dataclass +class UserInfo: + sid_type: Optional[SidType] + sid: Optional[str] + username: Optional[str] + + @classmethod + def _decode(cls, reader: _ScheduledTasksReader) -> Optional["UserInfo"]: + skip_user = reader.read_aligned_u1() != 0 + if not skip_user: + skip_sid = reader.read_aligned_u1() != 0 + else: + skip_sid = None + + sid_type = None + sid = None + if not skip_user and not skip_sid: + try: + sid_type = SidType(reader.read_aligned_u4()) + except ValueError: + sid_type = SidType.Unknown + + sid_raw = reader.read_buffer(aligned=True) + if sid_raw is None: + return None + sid = decode_sid(sid_raw) + + username = reader.read_bstring(aligned=True) if not skip_user else None + + return UserInfo(sid_type, sid, username) + + +@dataclasses.dataclass +class OptionalSettings: + IdleDurationSeconds: int + idleWaitTimeoutSeconds: int + ExecutionTimeLimitSeconds: int + DeleteExpiredTaskAfter: int + Priority: int + RestartOnFailureDelay: int + RestartOnFailureRetries: int + NetworkId: bytes + Privileges: Optional[List[str]] + Periodicity: Optional[TaskSchedulerTimePeriod] + Deadline: Optional[TaskSchedulerTimePeriod] + Exclusive: Optional[bool] + + @classmethod + def _decode(cls, reader: _ScheduledTasksReader) -> Optional["OptionalSettings"]: + LEN_WITH_PRIVILEGES = 0x38 + LEN_WITH_TIME_PERIODS = 0x58 + length = reader.read_aligned_u4() + if length == 0: + return None + + base_values = ( + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + binascii.hexlify(reader.read(16)), + ) + + if any(value is None for value in base_values): + return None + + reader.seek(4, io.SEEK_CUR) # padding + + privileges = None + periodicity = None + deadline = None + exclusive = None + if length == LEN_WITH_PRIVILEGES or length == LEN_WITH_TIME_PERIODS: + privileges_raw = reader.read_u8() + if privileges_raw is None: + return None + privileges = [ + priv.name for priv in Privileges if priv.value & privileges_raw + ] + if length == LEN_WITH_TIME_PERIODS: + periodicity = reader.read_tstimeperiod() + deadline = reader.read_tstimeperiod() + exclusive = reader.read_bool() + reader.seek(3, io.SEEK_CUR) # padding + + return OptionalSettings( + *base_values, privileges, periodicity, deadline, exclusive + ) + + +@dataclasses.dataclass +class JobBucket: + flags: List[str] + crc32: int + principal_id: Optional[str] + display_name: Optional[str] + user_info: Optional[UserInfo] + optional_settings: Optional[OptionalSettings] + + @classmethod + def _decode( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["JobBucket"]: + flags_raw = reader.read_aligned_u4() + if flags_raw is None: + return None + flags = [y for x, y in JOB_BUCKET_FLAGS.items() if x & flags_raw] + crc32 = reader.read_aligned_u4() + if crc32 is None: + return None + + principal_id = None + display_name = None + if version >= 0x16: + principal_id = reader.read_bstring(aligned=True) + if version >= 0x17: + display_name = reader.read_bstring(aligned=True) + + user_info = UserInfo._decode(reader) + optional_settings = OptionalSettings._decode(reader) + + return JobBucket( + flags, crc32, principal_id, display_name, user_info, optional_settings + ) + + +class Privileges(enum.Enum): + SeCreateTokenPrivilege = 0x4 + SeAssignPrimaryTokenPrivilege = 0x8 + SeLockMemoryPrivilege = 0x10 + SeIncreaseQuotaPrivilege = 0x20 + SeMachineAccountPrivilege = 0x40 + SeTcbPrivilege = 0x80 + SeSecurityPrivilege = 0x100 + SeTakeOwnershipPrivilege = 0x200 + SeLoadDriverPrivilege = 0x400 + SeSystemProfilePrivilege = 0x800 + SeSystemtimePrivilege = 0x1000 + SeProfileSingleProcessPrivilege = 0x2000 + SeIncreaseBasePriorityPrivilege = 0x4000 + SeCreatePagefilePrivilege = 0x8000 + SeCreatePermanentPrivilege = 0x10000 + SeBackupPrivilege = 0x20000 + SeRestorePrivilege = 0x40000 + SeShutdownPrivilege = 0x80000 + SeDebugPrivilege = 0x100000 + SeAuditPrivilege = 0x200000 + SeSystemEnvironmentPrivilege = 0x400000 + SeChangeNotifyPrivilege = 0x800000 + SeRemoteShutdownPrivilege = 0x1000000 + SeUndockPrivilege = 0x2000000 + SeSyncAgentPrivilege = 0x4000000 + SeEnableDelegationPrivilege = 0x8000000 + SeManageVolumePrivilege = 0x10000000 + SeImpersonatePrivilege = 0x20000000 + SeCreateGlobalPrivilege = 0x40000000 + SeTrustedCredManAccessPrivilege = 0x80000000 + SeRelabelPrivilege = 0x100000000 + SeIncreaseWorkingSetPrivilege = 0x200000000 + SeTimeZonePrivilege = 0x400000000 + SeCreateSymbolicLinkPrivilege = 0x800000000 + SeDelegateSessionUserImpersonatePrivilege = 0x1000000000 + + +class SessionState(enum.Enum): + ConsoleConnect = 1 + ConsoleDisconnect = 2 + RemoteConnect = 3 + RemoteDisconnect = 4 + SessionLock = 5 + SessionUnlock = 6 + Unknown = "Unknown" + + +@dataclasses.dataclass +class TaskTrigger: + start_boundary: Optional[datetime.datetime] + end_boundary: Optional[datetime.datetime] + repetition_interval_seconds: Optional[int] + enabled: Optional[bool] + trigger_type: TriggerType + description: Optional[str] + + @classmethod + def _decode_generic_trigger( + cls, reader: _ScheduledTasksReader, version: int, trigger_type: TriggerType + ) -> Optional["TaskTrigger"]: + start_boundary = reader.read_task_scheduler_time() + end_boundary = reader.read_task_scheduler_time() + + _ = reader.read_u4() # delay seconds + _ = reader.read_u4() # timeout seconds + + repetition_interval_secs = reader.read_u4() + _ = reader.read_u4() # repetition duration seconds + _ = reader.read_u4() # repetition duration seconds 2 + + _ = reader.read_bool() # stop at duration end + reader.seek(3, io.SEEK_CUR) + trigger_enabled = bool(reader.read_aligned_u1()) + reader.seek(8, io.SEEK_CUR) # unknown field + + if version >= 0x16: + cur = reader.tell() + _ = reader.read_bstring() # trigger id + reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block + + return cls( + start_boundary, + end_boundary, + repetition_interval_secs, + trigger_enabled, + trigger_type, + f"{trigger_type.name} trigger", + ) + + @classmethod + def _decode_logon_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Logon) + if base is None: + return None + + user = UserInfo._decode(reader) + if user is not None and user.username is not None: + base.description = f"{user.username}: {user.sid} ({user.sid_type})" + + return base + + @classmethod + def _decode_session_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Session) + if base is None: + return None + session_type_raw = reader.read_u4() + reader.seek(4, io.SEEK_CUR) + + try: + session_type = SessionState(session_type_raw) + except ValueError: + session_type = SessionState.Unknown + + user_info = UserInfo._decode(reader) + if user_info is not None and user_info.username is not None: + base.description = f"{session_type.name} for user {user_info.username}" + else: + base.description = session_type.name + + return base + + @classmethod + def _decode_time_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + job_schedule = _JobSchedule.decode(reader) + if job_schedule is None: + return None + + if version >= 0x16: + cur = reader.tell() + _ = reader.read_bstring() # trigger id + reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block + + return cls( + job_schedule.start_boundary, + job_schedule.end_boundary, + job_schedule.repetition_interval_secs, + job_schedule.is_enabled, + TriggerType.Time, + job_schedule.get_description() or None, + ) + + @classmethod + def _decode_event_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Event) + if base is None: + return base + + subscription = reader.read_aligned_bstring_expand_sz() + reader.seek(8, io.SEEK_CUR) # 2 4-byte unknown fields + reader.read_aligned_bstring_expand_sz() # another unknown field + len_value_queries = reader.read_aligned_u4() + + if len_value_queries is None: + return base + + queries = [ + ( + reader.read_aligned_bstring_expand_sz(), + reader.read_aligned_bstring_expand_sz(), + ) + for _ in range(len_value_queries) + ] + valid = [(k, v) for (k, v) in queries if k is not None and v is not None] + if base.description is None: + base.description = "Event Trigger" + base.description += f": Subscription: {subscription}, Queries: {str(valid)}" + return base + + @classmethod + def _decode_boot_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Boot) + + @classmethod + def _decode_wnf_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger( + reader, version, TriggerType.WindowsNotificationFacility + ) + if base is None: + return None + + state_name = binascii.hexlify(reader.read(8)).decode("ascii") + datalen = reader.read_aligned_u4() + _ = base64.b64encode(reader.read(datalen)) # state binary data + base.description = f"WNF state {state_name}" + return base + + @classmethod + def _decode_idle_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Logon) + + @classmethod + def _decode_registration_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Logon) + + +@dataclasses.dataclass +class TriggerSet: + job_bucket: JobBucket + triggers: List[TaskTrigger] + + @classmethod + def decode(cls, data) -> Optional["TriggerSet"]: + reader = _ScheduledTasksReader(data) + + version = reader.read_aligned_u1() + _ = reader.read_task_scheduler_time() # start boundary + _ = reader.read_task_scheduler_time() # end_boundary + + if version is None: + return None + + job_bucket = JobBucket._decode(reader, version) + if job_bucket is None: + return None + + triggers = [] + + while True: + magic = reader.read_aligned_u4() + if magic is None: + break + try: + trigger_type = TriggerType(magic) + except ValueError: + vollog.warning(f"Invalid trigger magic {hex(magic)}") + break + + if trigger_type == TriggerType.Logon: + trigger = TaskTrigger._decode_logon_trigger(reader, version) + elif trigger_type == TriggerType.Session: + trigger = TaskTrigger._decode_session_trigger(reader, version) + elif trigger_type == TriggerType.WindowsNotificationFacility: + trigger = TaskTrigger._decode_wnf_trigger(reader, version) + elif trigger_type == TriggerType.Boot: + trigger = TaskTrigger._decode_boot_trigger(reader, version) + elif trigger_type == TriggerType.Registration: + trigger = TaskTrigger._decode_registration_trigger(reader, version) + elif trigger_type == TriggerType.Event: + trigger = TaskTrigger._decode_event_trigger(reader, version) + elif trigger_type == TriggerType.Idle: + trigger = TaskTrigger._decode_idle_trigger(reader, version) + elif trigger_type == TriggerType.Time: + trigger = TaskTrigger._decode_time_trigger(reader, version) + else: + vollog.warning( + f"Invalid trigger magic {hex(magic)} encountered at offset {hex(reader.tell() - 8)}, stopping parsing" + ) + break + triggers.append(trigger) + + return cls(job_bucket, triggers) + + +@dataclasses.dataclass +class ActionSet: + actions: List[TaskAction] + context: Optional[str] + + @classmethod + def decode(cls, data: bytes) -> Optional["ActionSet"]: + reader = _ScheduledTasksReader(data) + actions = [] + + version = reader.read_u2() + if version is None: + return None + + if version in [2, 3]: + action_context = reader.read_bstring() + else: + action_context = None + + while True: + magic = reader.read_u2() + if magic is None: + break + + _ = ( + reader.read_bstring() + ) # action identifier, usually (but not always) empty + + if magic == ActionType.Email.value: + action = TaskAction._decode_email_action(reader) + elif magic == ActionType.Exe.value: + action = TaskAction._decode_exe_action(reader, version) + elif magic == ActionType.ComHandler.value: + action = TaskAction._decode_comhandler_action(reader) + elif magic == ActionType.MessageBox.value: + action = TaskAction.decode_messagebox_action(reader) + else: + break + actions.append(action) + + return cls(actions, action_context) + + +@dataclasses.dataclass +class DynamicInfo: + """ + Contains information about execution history for this task, + including timestamps and the last error code + """ + + creation_time: Optional[datetime.datetime] + last_run_time: Optional[datetime.datetime] + last_successful_run_time: Optional[datetime.datetime] + last_error_code: Optional[int] + + @classmethod + def decode(cls, data: bytes) -> Optional["DynamicInfo"]: + """ + Decodes a DynamicInfo structure from RegBin value data. + Raises a `ScheduledTaskDecodingError` if the magic bytes are invalid, but otherwise + attempts to decode as much as possible without returning an error. + """ + DYNAMICINFO_MAGIC = 3 + + reader = _ScheduledTasksReader(data) + magic = reader.read_u4() + if magic != DYNAMICINFO_MAGIC: + return None + + creation_time = reader.decode_filetime() + last_run_time = reader.decode_filetime() + + reader.seek(4, io.SEEK_CUR) # deprecated field 'TaskState' + + last_error_code = reader.read_u4() + last_success_time = reader.decode_filetime() + + vollog.debug((creation_time, last_run_time, last_success_time)) + + return cls( + last_run_time, + creation_time, + last_success_time, + last_error_code, + ) + + +class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): + """Decodes scheduled task information from the Windows registry, including + information about triggers, actions, run times, and creation times.""" + + _required_framework_version = (2, 11, 0) + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel33", "Intel64"], + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + ] + + def generate_timeline( + self, + ) -> 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, + ) + 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, + ) + 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, + ) + + @classmethod + def get_software_hive( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Optional[registry.RegistryHive]: + """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" + return next( + hivelist.HiveList.list_hives( + context=context, + base_config_path=interfaces.configuration.path_join( + config_path, "hivelist" + ), + kernel_module_name=kernel_module_name, + filter_string="SOFTWARE", + ), + None, + ) + + @classmethod + def parse_actions_value( + cls, actions_value: reg_extensions.CM_KEY_VALUE + ) -> Optional[ActionSet]: + """Parses File entries from the Windows 8 `Root\\File` key. + + :param programs_key: The `Root\\File` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. + """ + try: + data = actions_value.decode_data() + except exceptions.InvalidAddressException: + data = None + + if not isinstance(data, bytes): + return None + + return ActionSet.decode(data) + + @classmethod + def parse_triggers_value( + cls, triggers_value: reg_extensions.CM_KEY_VALUE + ) -> Optional[TriggerSet]: + try: + data = triggers_value.decode_data() + except exceptions.InvalidAddressException: + data = None + + if not isinstance(data, bytes): + return None + + return TriggerSet.decode(data) + + @classmethod + 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: + data = None + + if not isinstance(data, bytes): + return None + + return DynamicInfo.decode(data) + + @classmethod + def _get_task_keys( + cls, software_hive: registry.RegistryHive + ) -> Tuple[ + Optional[reg_extensions.CM_KEY_NODE], Optional[reg_extensions.CM_KEY_NODE] + ]: + try: + task_key = software_hive.get_key( + "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tasks" + ) + except (KeyError, registry.RegistryException): + task_key = None + + try: + task_tree = software_hive.get_key( + "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree" + ) + except (KeyError, registry.RegistryException): + task_tree = None + + return (task_key, task_tree) # type: ignore + + @classmethod + def _parse_task_key( + cls, key: reg_extensions.CM_KEY_NODE, guid_mapping: Dict[str, str] + ) -> Iterator[_ScheduledTaskEntry]: + values = {} + for value in key.get_values(): + try: + name = str(value.get_name()) + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): + continue + + if name in ["Actions", "Triggers", "DynamicInfo"]: + values[name] = value + + try: + key_name = str(key.get_name()) + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): + key_name = None + + try: + task_name = guid_mapping.get(key_name, renderers.NotAvailableValue()) + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): + task_name = renderers.NotAvailableValue() + + try: + action_set = cls.parse_actions_value(values["Actions"]) + except KeyError: + vollog.debug("Failed to get Actions value") + action_set = None + + try: + triggers_value = values["Triggers"] + trigger_set = cls.parse_triggers_value(triggers_value) + except KeyError: + vollog.debug("Failed to get Triggers value") + trigger_set = None + + if trigger_set is not None: + vollog.debug("Parsed triggers successfully") + + principal_id = ( + trigger_set.job_bucket.principal_id or renderers.NotAvailableValue() + ) + display_name = ( + trigger_set.job_bucket.display_name or renderers.NotAvailableValue() + ) + else: + vollog.debug("Failed to parse triggers") + + principal_id = renderers.NotAvailableValue() + display_name = renderers.NotAvailableValue() + + try: + dynamic_info = cls.parse_dynamic_info_value(values["DynamicInfo"]) + except KeyError: + vollog.debug("DynamicInfo value not found") + dynamic_info = None + + vollog.debug(dynamic_info) + + creation_time = dynamic_info.creation_time if dynamic_info is not None else None + last_run_time = dynamic_info.last_run_time if dynamic_info is not None else None + last_successful_run_time = ( + dynamic_info.last_successful_run_time if dynamic_info is not None else None + ) + + all_triggers = ( + trigger_set.triggers or [None] if trigger_set is not None else [None] + ) + + 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, + ActionType.ComHandler, + ): + if action.action_args is None: + args = renderers.NotAvailableValue() + else: + args = action.action_args + else: + args = renderers.NotApplicableValue() + + if action.action_type == ActionType.Exe: + working_directory = ( + action.working_directory or renderers.NotAvailableValue() + ) + else: + working_directory = renderers.NotApplicableValue() + + else: + args = renderers.NotAvailableValue() + working_directory = renderers.NotAvailableValue() + + if trigger is not None and trigger.enabled is not None: + enabled = trigger.enabled + else: + enabled = renderers.NotAvailableValue() + + yield _ScheduledTaskEntry( + task_name, + principal_id, + display_name, + enabled, + creation_time or renderers.NotAvailableValue(), + last_run_time or renderers.NotAvailableValue(), + last_successful_run_time or renderers.NotAvailableValue(), + ( + trigger.trigger_type.name + if trigger is not None + else renderers.NotAvailableValue() + ), + ( + trigger.description or renderers.NotAvailableValue() + if trigger is not None + else renderers.NotAvailableValue() + ), + ( + action.action_type.name + if action is not None + else renderers.NotAvailableValue() + ), + ( + action.action + if action is not None + else renderers.NotAvailableValue() + ), + args, + ( + action_set.context + if (action_set is not None and action_set.context is not None) + else renderers.NotAvailableValue() + ), + working_directory, + key_name or renderers.NotAvailableValue(), + ) + + def _generator(self) -> Iterator[Tuple[int, _ScheduledTaskEntry]]: + # Building the dictionary ahead of time is much better for performance + # vs looking up each service's DLL individually. + software_hive = self.get_software_hive( + self.context, self.config_path, self.config["kernel"] + ) + if software_hive is None: + vollog.warning("Failed to get SOFTWARE hive") + return + + task_key_root, task_tree = self._get_task_keys(software_hive) + if task_key_root is None: + vollog.warning("Failed to get 'Tasks' key") + return + + if task_tree is not None: + task_name_map = _build_guid_name_map(task_tree) + else: + vollog.info("'Tree' key not found, can't map GUIDs to task names") + task_name_map = {} + + for key in task_key_root.get_subkeys(): + for task in self._parse_task_key(key, task_name_map): + yield 0, task + + def run(self): + return renderers.TreeGrid( + [ + ("Task Name", str), + ("Principal ID", str), + ("Display Name", str), + ("Enabled", bool), + ("Creation Time", datetime.datetime), + ("Last Run Time", datetime.datetime), + ("Last Successful Run Time", datetime.datetime), + ("Trigger Type", str), + ("Trigger Description", str), + ("Action Type", str), + ("Action", str), + ("Action Arguments", str), + ("Action Context", str), + ("Working Directory", str), + ("Key Name", str), + ], + ( + (indent, dataclasses.astuple(entry)) + for indent, entry in self._generator() + ), + ) diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index bd832b20c..d27c8eb0c 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -12,8 +12,8 @@ from typing import Any, Generator, List, Tuple from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.layers.physical import BufferDataLayer -from volatility3.framework.layers.registry import RegistryHive +from volatility3.framework.layers import physical +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.plugins.windows.registry import hivelist @@ -39,7 +39,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac os.path.join(os.path.dirname(__file__), "userassist.json"), "rb" ) as fp: self._folder_guids = json.load(fp) - except IOError: + except OSError: vollog.error("Usersassist data file not found") @classmethod @@ -53,8 +53,13 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac requirements.IntRequirement( name="offset", description="Hive Offset", default=None, optional=True ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), ] @@ -86,7 +91,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac return item userassist_layer_name = self.context.layers.free_layer_name("userassist_buffer") - buffer = BufferDataLayer( + buffer = physical.BufferDataLayer( self.context, self._config_path, userassist_layer_name, userassist_data ) self.context.add_layer(buffer) @@ -150,7 +155,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac ).has_member("CookiePad") def list_userassist( - self, hive: RegistryHive + self, hive: registry_layer.RegistryHive ) -> Generator[Tuple[int, Tuple], None, None]: """Generate userassist data for a registry hive.""" @@ -167,10 +172,21 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac self._determine_userassist_type() - userassist_node_path = hive.get_key( - "software\\microsoft\\windows\\currentversion\\explorer\\userassist", - return_list=True, - ) + try: + userassist_node_path = hive.get_key( + "software\\microsoft\\windows\\currentversion\\explorer\\userassist", + return_list=True, + ) + except registry_layer.RegistryException as e: + vollog.warning( + f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}" + ) + return None + except KeyError: + vollog.warning( + f"UserAssist key not found in {hive_name} at {hive.hive_offset:#x}" + ) + return None if not userassist_node_path: vollog.warning("list_userassist did not find a valid node_path (or None)") @@ -227,7 +243,14 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac # output any subkeys under Count for subkey in countkey.get_subkeys(): - subkey_name = subkey.get_name() + try: + subkey_name = subkey.get_name() + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ): + subkey_name = renderers.UnreadableValue() + result = ( 1, ( @@ -249,7 +272,14 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac # output any values under Count for value in countkey.get_values(): - value_name = value.get_name() + try: + value_name = value.get_name() + except ( + exceptions.InvalidAddressException, + registry_layer.RegistryException, + ): + value_name = renderers.UnreadableValue() + with contextlib.suppress(UnicodeDecodeError): value_name = codecs.encode(value_name, "rot_13") @@ -284,7 +314,6 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac hive_offsets = None if self.config.get("offset", None) is not None: hive_offsets = [self.config.get("offset", None)] - kernel = self.context.modules[self.config["kernel"]] self._reg_table_name = intermed.IntermediateSymbolTable.create( self.context, self._config_path, "windows", "registry" @@ -294,8 +323,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac for hive in hivelist.HiveList.list_hives( context=self.context, base_config_path=self.config_path, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_string="ntuser.dat", hive_offsets=hive_offsets, ): @@ -308,9 +336,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac ) except exceptions.InvalidAddressException as excp: vollog.debug( - "Invalid address identified in lower layer {}: {}".format( - excp.layer_name, excp.invalid_address - ) + f"Invalid address identified in lower layer {excp.layer_name}: {excp.invalid_address}" ) except KeyError: vollog.debug( diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 277a0d856..104d062b2 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -1,1408 +1,22 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -import base64 -import binascii -import dataclasses -import datetime -import enum -import io -import itertools import logging -import struct -from typing import Dict, Iterator, List, Optional, Tuple, Union -from volatility3.framework import exceptions, interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import registry -from volatility3.framework.renderers import conversion -from volatility3.framework.symbols.windows.extensions import registry as reg_extensions -from volatility3.plugins import timeliner -from volatility3.plugins.windows.registry import hivelist +from volatility3.framework import deprecation, interfaces +from volatility3.plugins.windows.registry import scheduled_tasks vollog = logging.getLogger(__name__) -# Reference: https://cyber.wtf/2022/06/01/windows-registry-analysis-todays-episode-tasks/ - -class TimeMode(enum.Enum): - """ - Enumeration containing the different time modes that a 'Time' trigger can be configured to run in. - """ - - Once = "Once" - - # run at and repeat every days - Daily = "Daily" - - # run on days of week <(data2 as day_of_week bitmap)> every weeks starting at - Weekly = "Weekly" - - # run in months <(data3 as months bitmap> on days <(data2:data1 as day in month bitmap)> - # starting at - DaysInMonths = "Days In Months" - - # run in months <(data3 as months bitmap> in weeks <(data2 as week bitmap)> - # on days <(data1 as day_of_week bitmap)> starting at - DaysInWeeksInMonths = "Days In Weeks in Months" - - Unknown = "Unknown" - - -TIME_MODE_DESCRIPTION = { - TimeMode.Once: "Once", - TimeMode.Daily: "Daily", - TimeMode.Weekly: "Weekly", - TimeMode.DaysInMonths: "Days In Months", - TimeMode.DaysInWeeksInMonths: "Days In Weeks In Months", - TimeMode.Unknown: "Unknown", -} - - -class ActionType(enum.Enum): - """ - Enumeration that maps action types to their magic number encodings - """ - - Exe = 0x6666 - ComHandler = 0x7777 - Email = 0x8888 - MessageBox = 0x9999 - - -class TriggerType(enum.Enum): - """ - Enumeration that maps trigger types to their magic number encodings - """ - - WindowsNotificationFacility = 0x6666 - Session = 0x7777 - Registration = 0x8888 - Logon = 0xAAAA - Event = 0xCCCC - Time = 0xDDDD - Idle = 0xEEEE - Boot = 0xFFFF - - -class Weekday(enum.Enum): - """ - Enumeration that contains bitwise values for days of the week. - """ - - Sunday = 0x1 - Monday = 0x2 - Tuesday = 0x4 - Wednesday = 0x8 - Thursday = 0x10 - Friday = 0x20 - Saturday = 0x40 - - -class Months(enum.Enum): - """ - Enumeration that contains bitwise values for months of the year. - """ - - January = 0x1 - February = 0x2 - March = 0x4 - April = 0x8 - May = 0x10 - June = 0x20 - July = 0x40 - August = 0x80 - September = 0x100 - October = 0x200 - November = 0x400 - December = 0x800 - - -class SidType(enum.Enum): - """ - Enumeration that maps SID types to their encoded integer values - """ - - User = 1 - Group = 2 - Domain = 3 - Alias = 4 - WellKnownGroup = 5 - DeletedAccount = 6 - Invalid = 7 - Unknown = 8 - Computer = 9 - Label = 10 - LogonSession = 11 - - -@dataclasses.dataclass -class TaskSchedulerTimePeriod: - """ - Class containing information delimiting time periods within scheduled tasks. - """ - - years: int - months: int - weeks: int - days: int - hours: int - minutes: int - seconds: int - - -JOB_BUCKET_FLAGS = { - 0x2: "Run only if idle", - 0x4: "Restart on idle", - 0x8: "Stop on idle end", - 0x10: "Disallow start if on batteries", - 0x20: "Stop if going on batteries", - 0x40: "Start when available", - 0x80: "Run only if network available", - 0x100: "Allow start on demand", - 0x200: "Wake to run", - 0x400: "Execute parallel", - 0x800: "Execute stop existing", - 0x1000: "Execute queue", - 0x2000: "Execute ignore new", - 0x4000: "Logon type s4u", - 0x10000: "Logon type InteractiveToken", - 0x40000: "Logon type Password", - 0x80000: "Logon type InteractiveTokenOrPassword", - 0x400000: "Enabled", - 0x800000: "Hidden", - 0x1000000: "Runlevel highest available", - 0x2000000: "Task", - 0x4000000: "Version", - 0x8000000: "Token SID type none", - 0x10000000: "Token SID type unrestricted", - 0x20000000: "Interval", - 0x40000000: "Allow hard terminate", -} - -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() - if filetime is None: - return None - - return filetime - - def read_bool(self, aligned=False) -> Optional[bool]: - try: - val = struct.unpack("?", self.read(1))[0] - if aligned: - self.seek(7) - return val - except struct.error: - return None - - def decode_filetime(self) -> Optional[datetime.datetime]: - filetime = self.read_u8() - if filetime is None: - return None - - if filetime == 0 or filetime == 0xFFFFFFFFFFFFFFFF: - return None - filetime = conversion.wintime_to_datetime(filetime) - if isinstance(filetime, datetime.datetime): - return filetime - else: - return None - - def _read_uint( - self, size: int, format: str, aligned: bool = False - ) -> Optional[int]: - try: - val = struct.unpack(format, self.read(size))[0] - if aligned: - self.seek(8 - size, io.SEEK_CUR) - return val - except struct.error: - return None - - def read_aligned_u1(self) -> Optional[int]: - return self._read_uint(1, "B", True) - - def read_u2(self) -> Optional[int]: - return self._read_uint(2, " Optional[int]: - return self._read_uint(2, " Optional[int]: - return self._read_uint(4, " Optional[int]: - return self._read_uint(8, " Optional[int]: - return self._read_uint(4, " Optional[bytes]: - count = self.read_u4() if not aligned else self.read_aligned_u4() - if count is None: - return None - data = self.read(count) - if aligned: - self.seek((8 - (count % 8)) % 8, io.SEEK_CUR) - return data - - def read_bstring(self, aligned=False) -> Optional[str]: - size = self.read_u4() if not aligned else self.read_aligned_u4() - if size is None: - return None - try: - raw = self.read(size) - val = raw.decode("utf-16le", errors="replace").rstrip(NULL) or None - except UnicodeDecodeError: - val = None - - if aligned: - self.seek((8 - (size % 8)) % 8, io.SEEK_CUR) - - return val - - def read_aligned_bstring_expand_sz(self) -> Optional[str]: - # type: () -> Optional[str] - sz = self.read_aligned_u4() - if sz is None: - return None - byte_count = sz * 2 + 2 - - if sz == 0: - return None - - try: - content = self.read(byte_count).decode("utf-16le") - except UnicodeDecodeError: - content = None - - self.seek((8 - (byte_count % 8)) % 8, io.SEEK_CUR) - return content.rstrip("\x00") if content is not None else None - - def read_tstimeperiod(self) -> Optional[TaskSchedulerTimePeriod]: - values = ( - self.read_u2(), - self.read_u2(), - self.read_u2(), - self.read_u2(), - self.read_u2(), - self.read_u2(), - self.read_u2(), - ) - - if any(value is None for value in values): - return None - - return TaskSchedulerTimePeriod(*values) - - -def _build_guid_name_map(key: reg_extensions.CM_KEY_NODE) -> Dict[str, str]: - mapping = {} - task_id_value = None - for value in key.get_values(): - try: - if value.get_name() == "Id": - task_id_value = value - break - except exceptions.InvalidAddressException: - continue - - if ( - task_id_value is not None - and task_id_value.get_type() == reg_extensions.RegValueTypes.REG_SZ - ): - try: - id_str = task_id_value.decode_data() - except exceptions.InvalidAddressException: - id_str = None - - if isinstance(id_str, bytes): - mapping[id_str.decode("utf-16le", errors="replace").rstrip(NULL)] = str( - key.get_name() - ) - - for subkey in key.get_subkeys(): - mapping.update(_build_guid_name_map(subkey)) - return mapping - - -@dataclasses.dataclass -class TaskAction: - action_type: ActionType - action: str - action_args: Optional[str] - working_directory: Optional[str] - - @classmethod - def decode_messagebox_action( - cls, reader: _ScheduledTasksReader - ) -> Optional["TaskAction"]: - caption, content = reader.read_bstring(), reader.read_bstring() - return cls( - ActionType.MessageBox, - f'"{caption or ""}": {content or ""}', - None, - None, - ) - - @classmethod - def _decode_exe_action( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskAction"]: - command = reader.read_bstring() - args = reader.read_bstring() - if command is None or args is None: - return None - - workdir = reader.read_bstring() - if version == 3: - _flags = reader.read_u2() - - return cls(ActionType.Exe, command, args, workdir) - - @classmethod - def _decode_email_action( - cls, reader: _ScheduledTasksReader - ) -> Optional["TaskAction"]: - props = { - "From": reader.read_bstring(), - "To": reader.read_bstring(), - "Cc": reader.read_bstring(), - "Bcc": reader.read_bstring(), - "Reply_to": reader.read_bstring(), - "Server": reader.read_bstring(), - "Subject": reader.read_bstring(), - "Body": reader.read_bstring(), - } - - num_attachment_filenames = reader.read_u4() - if num_attachment_filenames is not None: - - attachment_filenames = [ - reader.read_bstring() for _ in range(num_attachment_filenames) - ] - - props["Attachments"] = ( - "<" - + ", ".join( - filename - for filename in attachment_filenames - if filename is not None - ) - + ">" - ) - - num_headers = reader.read_u4() - if num_headers is not None: - headers = [ - (reader.read_bstring(), reader.read_bstring()) - for _ in range(num_headers) - ] - - props["Headers"] = ( - "<" - + ", ".join( - f"{field}: {value}" - for field, value in headers - if field is not None and value is not None - ) - + ">" - ) - - cls( - ActionType.Email, - ", ".join( - f"{key}: {value}" for key, value in props.items() if value is not None - ), - None, - None, - ) - - @classmethod - def _decode_comhandler_action( - cls, reader: _ScheduledTasksReader - ) -> Optional["TaskAction"]: - guid_raw = reader.read(16) - if not guid_raw and len(guid_raw) == 16: - return None - clsid = conversion.windows_bytes_to_guid(guid_raw) - args = reader.read_bstring() - - return cls(ActionType.ComHandler, clsid, args, None) - - -@dataclasses.dataclass -class _ScheduledTaskEntry: - name: Union[str, interfaces.renderers.BaseAbsentValue] - principal_id: Union[str, interfaces.renderers.BaseAbsentValue] - display_name: Union[str, interfaces.renderers.BaseAbsentValue] - enabled: Union[bool, interfaces.renderers.BaseAbsentValue] - creation_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] - last_run_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] - last_successful_run_time: Union[ - datetime.datetime, interfaces.renderers.BaseAbsentValue - ] - trigger_type: Union[str, interfaces.renderers.BaseAbsentValue] - trigger_description: Union[str, interfaces.renderers.BaseAbsentValue] - action_type: Union[str, interfaces.renderers.BaseAbsentValue] - action_description: Union[str, interfaces.renderers.BaseAbsentValue] - action_args: Union[str, interfaces.renderers.BaseAbsentValue] - action_context: Union[str, interfaces.renderers.BaseAbsentValue] - working_directory: Union[str, interfaces.renderers.BaseAbsentValue] - guid: str - - -@dataclasses.dataclass -class _JobSchedule: - start_boundary: Optional[datetime.datetime] - end_boundary: Optional[datetime.datetime] - repetition_interval_secs: Optional[int] - repetition_duration_secs: Optional[int] - execution_time_limit_secs: Optional[int] - mode: Optional[TimeMode] - data1: Optional[int] - data2: Optional[int] - data3: Optional[int] - stop_tasks_at_duration_end: Optional[int] - is_enabled: Optional[bool] - max_delay_seconds: Optional[int] - - def get_description(self) -> Optional[str]: - if self.mode == TimeMode.Once: - return "Run one time starting at {}".format( - self.start_boundary.isoformat() - if self.start_boundary is not None - else "" - ) - - elif self.mode == TimeMode.Daily: - if self.data1 is None: - return None - return "Run at {} and repeat every {} days".format( - ( - self.start_boundary.isoformat() - if self.start_boundary is not None - else "" - ), - self.data1, - ) - - elif self.mode == TimeMode.Weekly: - if self.data2 is None: - return None - - days = [k.name for k in Weekday if k.value & self.data2] - return "Run on {} every {} weeks starting at {}".format( - ", ".join(days), - self.data1, - ( - self.start_boundary.isoformat() - if self.start_boundary is not None - else "" - ), - ) - elif self.mode == TimeMode.DaysInMonths: - if self.data2 is None or self.data1 is None or self.data3 is None: - return None - months = [month.name for month in Months if month.value & self.data3] - days_bitmap = (self.data2 << 16) + self.data1 - days = [str(v + 1) for v in range(31) if (1 << v) & days_bitmap] - return "Run in months {} on days {} starting at {}".format( - ", ".join(months), - ", ".join(days), - ( - self.start_boundary.isoformat() - if self.start_boundary is not None - else "" - ), - ) - elif self.mode == TimeMode.DaysInWeeksInMonths: - if self.data1 is None or self.data2 is None or self.data3 is None: - return None - - months = [month.name for month in Months if month.value & self.data3] - weeks = [str(v + 1) for v in range(5) if (v << 1) & self.data2] - days = [day.name for day in Weekday if day.value & self.data1] - return "Run in months {} in weeks {} on days {} starting at {}".format( - ", ".join(months), - ", ".join(weeks), - ", ".join(days), - ( - self.start_boundary.isoformat() - if self.start_boundary is not None - else "" - ), - ) - else: - return None - - @classmethod - def decode(cls, reader: _ScheduledTasksReader) -> Optional["_JobSchedule"]: - start_boundary = reader.read_task_scheduler_time() - end_boundary = reader.read_task_scheduler_time() - - _ = reader.read_task_scheduler_time() - repetition_interval_secs = reader.read_u4() - repetition_duration_secs = reader.read_u4() - execution_time_limit_secs = reader.read_u4() - mode_index = reader.read_u4() - if mode_index is not None: - try: - mode = TimeMode(mode_index) - except ValueError: - mode = TimeMode.Unknown - else: - mode = None - - data1 = reader.read_u2() - data2 = reader.read_u2() - data3 = reader.read_u2() - - reader.seek(2, io.SEEK_CUR) # pad - stop_tasks_at_duration_end = reader.read_bool() - is_enabled = reader.read_bool() - reader.seek(6, io.SEEK_CUR) # pad (2) + unknown (4) - max_delay_seconds = reader.read_u4() - reader.seek(4, io.SEEK_CUR) # pad - - return cls( - start_boundary, - end_boundary, - repetition_interval_secs, - repetition_duration_secs, - execution_time_limit_secs, - mode, - data1, - data2, - data3, - stop_tasks_at_duration_end, - is_enabled, - max_delay_seconds, - ) - - -def decode_sid(data: bytes) -> Optional[str]: - """ - Decodes a windows SID from variable-length raw bytes - - Returns the string representation of the SID if decoding was successful, or None - if the data could not be parsed due to an insufficent number of bytes. - """ - try: - revision, subid_count, id_authority = struct.unpack( - ">BBQ", data[:2] + b"\x00\x00" + data[2:8] - ) - subauthorities = struct.unpack( - "<" + "I" * subid_count, data[8 : 8 + subid_count * 4] - ) - sid_string = "S-" + "-".join( - [str(item) for item in [revision, id_authority] + list(subauthorities)] - ) - except struct.error: - return None - - return sid_string - - -@dataclasses.dataclass -class UserInfo: - sid_type: Optional[SidType] - sid: Optional[str] - username: Optional[str] - - @classmethod - def _decode(cls, reader: _ScheduledTasksReader) -> Optional["UserInfo"]: - skip_user = reader.read_aligned_u1() != 0 - if not skip_user: - skip_sid = reader.read_aligned_u1() != 0 - else: - skip_sid = None - - sid_type = None - sid = None - if not skip_user and not skip_sid: - try: - sid_type = SidType(reader.read_aligned_u4()) - except ValueError: - sid_type = SidType.Unknown - - sid_raw = reader.read_buffer(aligned=True) - if sid_raw is None: - return None - sid = decode_sid(sid_raw) - - username = reader.read_bstring(aligned=True) if not skip_user else None - - return UserInfo(sid_type, sid, username) - - -@dataclasses.dataclass -class OptionalSettings: - IdleDurationSeconds: int - idleWaitTimeoutSeconds: int - ExecutionTimeLimitSeconds: int - DeleteExpiredTaskAfter: int - Priority: int - RestartOnFailureDelay: int - RestartOnFailureRetries: int - NetworkId: bytes - Privileges: Optional[List[str]] - Periodicity: Optional[TaskSchedulerTimePeriod] - Deadline: Optional[TaskSchedulerTimePeriod] - Exclusive: Optional[bool] - - @classmethod - def _decode(cls, reader: _ScheduledTasksReader) -> Optional["OptionalSettings"]: - LEN_WITH_PRIVILEGES = 0x38 - LEN_WITH_TIME_PERIODS = 0x58 - length = reader.read_aligned_u4() - if length == 0: - return None - - base_values = ( - reader.read_u4(), - reader.read_u4(), - reader.read_u4(), - reader.read_u4(), - reader.read_u4(), - reader.read_u4(), - reader.read_u4(), - binascii.hexlify(reader.read(16)), - ) - - if any(value is None for value in base_values): - return None - - reader.seek(4, io.SEEK_CUR) # padding - - privileges = None - periodicity = None - deadline = None - exclusive = None - if length == LEN_WITH_PRIVILEGES or length == LEN_WITH_TIME_PERIODS: - privileges_raw = reader.read_u8() - if privileges_raw is None: - return None - privileges = [ - priv.name for priv in Privileges if priv.value & privileges_raw - ] - if length == LEN_WITH_TIME_PERIODS: - periodicity = reader.read_tstimeperiod() - deadline = reader.read_tstimeperiod() - exclusive = reader.read_bool() - reader.seek(3, io.SEEK_CUR) # padding - - return OptionalSettings( - *base_values, privileges, periodicity, deadline, exclusive - ) - - -@dataclasses.dataclass -class JobBucket: - flags: List[str] - crc32: int - principal_id: Optional[str] - display_name: Optional[str] - user_info: Optional[UserInfo] - optional_settings: Optional[OptionalSettings] - - @classmethod - def _decode( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["JobBucket"]: - flags_raw = reader.read_aligned_u4() - if flags_raw is None: - return None - flags = [y for x, y in JOB_BUCKET_FLAGS.items() if x & flags_raw] - crc32 = reader.read_aligned_u4() - if crc32 is None: - return None - - principal_id = None - display_name = None - if version >= 0x16: - principal_id = reader.read_bstring(aligned=True) - if version >= 0x17: - display_name = reader.read_bstring(aligned=True) - - user_info = UserInfo._decode(reader) - optional_settings = OptionalSettings._decode(reader) - - return JobBucket( - flags, crc32, principal_id, display_name, user_info, optional_settings - ) - - -class Privileges(enum.Enum): - SeCreateTokenPrivilege = 0x4 - SeAssignPrimaryTokenPrivilege = 0x8 - SeLockMemoryPrivilege = 0x10 - SeIncreaseQuotaPrivilege = 0x20 - SeMachineAccountPrivilege = 0x40 - SeTcbPrivilege = 0x80 - SeSecurityPrivilege = 0x100 - SeTakeOwnershipPrivilege = 0x200 - SeLoadDriverPrivilege = 0x400 - SeSystemProfilePrivilege = 0x800 - SeSystemtimePrivilege = 0x1000 - SeProfileSingleProcessPrivilege = 0x2000 - SeIncreaseBasePriorityPrivilege = 0x4000 - SeCreatePagefilePrivilege = 0x8000 - SeCreatePermanentPrivilege = 0x10000 - SeBackupPrivilege = 0x20000 - SeRestorePrivilege = 0x40000 - SeShutdownPrivilege = 0x80000 - SeDebugPrivilege = 0x100000 - SeAuditPrivilege = 0x200000 - SeSystemEnvironmentPrivilege = 0x400000 - SeChangeNotifyPrivilege = 0x800000 - SeRemoteShutdownPrivilege = 0x1000000 - SeUndockPrivilege = 0x2000000 - SeSyncAgentPrivilege = 0x4000000 - SeEnableDelegationPrivilege = 0x8000000 - SeManageVolumePrivilege = 0x10000000 - SeImpersonatePrivilege = 0x20000000 - SeCreateGlobalPrivilege = 0x40000000 - SeTrustedCredManAccessPrivilege = 0x80000000 - SeRelabelPrivilege = 0x100000000 - SeIncreaseWorkingSetPrivilege = 0x200000000 - SeTimeZonePrivilege = 0x400000000 - SeCreateSymbolicLinkPrivilege = 0x800000000 - SeDelegateSessionUserImpersonatePrivilege = 0x1000000000 - - -class SessionState(enum.Enum): - ConsoleConnect = 1 - ConsoleDisconnect = 2 - RemoteConnect = 3 - RemoteDisconnect = 4 - SessionLock = 5 - SessionUnlock = 6 - Unknown = "Unknown" - - -@dataclasses.dataclass -class TaskTrigger: - start_boundary: Optional[datetime.datetime] - end_boundary: Optional[datetime.datetime] - repetition_interval_seconds: Optional[int] - enabled: Optional[bool] - trigger_type: TriggerType - description: Optional[str] - - @classmethod - def _decode_generic_trigger( - cls, reader: _ScheduledTasksReader, version: int, trigger_type: TriggerType - ) -> Optional["TaskTrigger"]: - start_boundary = reader.read_task_scheduler_time() - end_boundary = reader.read_task_scheduler_time() - - _ = reader.read_u4() # delay seconds - _ = reader.read_u4() # timeout seconds - - repetition_interval_secs = reader.read_u4() - _ = reader.read_u4() # reptition duration seconds - _ = reader.read_u4() # repetition duration seconds 2 - - _ = reader.read_bool() # stop at duration end - reader.seek(3, io.SEEK_CUR) - trigger_enabled = bool(reader.read_aligned_u1()) - reader.seek(8, io.SEEK_CUR) # unknown field - - if version >= 0x16: - cur = reader.tell() - _ = reader.read_bstring() # trigger id - reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block - - return cls( - start_boundary, - end_boundary, - repetition_interval_secs, - trigger_enabled, - trigger_type, - f"{trigger_type.name} trigger", - ) - - @classmethod - def _decode_logon_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - base = cls._decode_generic_trigger(reader, version, TriggerType.Logon) - if base is None: - return None - - user = UserInfo._decode(reader) - if user is not None and user.username is not None: - base.description = f"{user.username}: {user.sid} ({user.sid_type})" - - return base - - @classmethod - def _decode_session_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - base = cls._decode_generic_trigger(reader, version, TriggerType.Session) - if base is None: - return None - session_type_raw = reader.read_u4() - reader.seek(4, io.SEEK_CUR) - - try: - session_type = SessionState(session_type_raw) - except ValueError: - session_type = SessionState.Unknown - - user_info = UserInfo._decode(reader) - if user_info is not None and user_info.username is not None: - base.description = f"{session_type.name} for user {user_info.username}" - else: - base.description = session_type.name - - return base - - @classmethod - def _decode_time_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - job_schedule = _JobSchedule.decode(reader) - if job_schedule is None: - return None - - if version >= 0x16: - cur = reader.tell() - _ = reader.read_bstring() # trigger id - reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block - - return cls( - job_schedule.start_boundary, - job_schedule.end_boundary, - job_schedule.repetition_interval_secs, - job_schedule.is_enabled, - TriggerType.Time, - job_schedule.get_description() or None, - ) - - @classmethod - def _decode_event_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - base = cls._decode_generic_trigger(reader, version, TriggerType.Event) - if base is None: - return base - - subscription = reader.read_aligned_bstring_expand_sz() - reader.seek(8, io.SEEK_CUR) # 2 4-byte unknown fields - reader.read_aligned_bstring_expand_sz() # another unknown field - len_value_queries = reader.read_aligned_u4() - - if len_value_queries is None: - return base - - queries = [ - ( - reader.read_aligned_bstring_expand_sz(), - reader.read_aligned_bstring_expand_sz(), - ) - for _ in range(len_value_queries) - ] - valid = [(k, v) for (k, v) in queries if k is not None and v is not None] - if base.description is None: - base.description = "Event Trigger" - base.description += f": Subscription: {subscription}, Queries: {str(valid)}" - return base - - @classmethod - def _decode_boot_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - return cls._decode_generic_trigger(reader, version, TriggerType.Boot) - - @classmethod - def _decode_wnf_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - base = cls._decode_generic_trigger( - reader, version, TriggerType.WindowsNotificationFacility - ) - if base is None: - return None - - state_name = binascii.hexlify(reader.read(8)).decode("ascii") - datalen = reader.read_aligned_u4() - _ = base64.b64encode(reader.read(datalen)) # state binary data - base.description = f"WNF state {state_name}" - return base - - @classmethod - def _decode_idle_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - return cls._decode_generic_trigger(reader, version, TriggerType.Logon) - - @classmethod - def _decode_registration_trigger( - cls, reader: _ScheduledTasksReader, version: int - ) -> Optional["TaskTrigger"]: - return cls._decode_generic_trigger(reader, version, TriggerType.Logon) - - -@dataclasses.dataclass -class TriggerSet: - job_bucket: JobBucket - triggers: List[TaskTrigger] - - @classmethod - def decode(cls, data) -> Optional["TriggerSet"]: - reader = _ScheduledTasksReader(data) - - version = reader.read_aligned_u1() - _ = reader.read_task_scheduler_time() # start boundary - _ = reader.read_task_scheduler_time() # end_boundary - - if version is None: - return None - - job_bucket = JobBucket._decode(reader, version) - if job_bucket is None: - return None - - triggers = [] - - while True: - magic = reader.read_aligned_u4() - if magic is None: - break - try: - trigger_type = TriggerType(magic) - except ValueError: - vollog.warning(f"Invalid trigger magic {hex(magic)}") - break - - if trigger_type == TriggerType.Logon: - trigger = TaskTrigger._decode_logon_trigger(reader, version) - elif trigger_type == TriggerType.Session: - trigger = TaskTrigger._decode_session_trigger(reader, version) - elif trigger_type == TriggerType.WindowsNotificationFacility: - trigger = TaskTrigger._decode_wnf_trigger(reader, version) - elif trigger_type == TriggerType.Boot: - trigger = TaskTrigger._decode_boot_trigger(reader, version) - elif trigger_type == TriggerType.Registration: - trigger = TaskTrigger._decode_registration_trigger(reader, version) - elif trigger_type == TriggerType.Event: - trigger = TaskTrigger._decode_event_trigger(reader, version) - elif trigger_type == TriggerType.Idle: - trigger = TaskTrigger._decode_idle_trigger(reader, version) - elif trigger_type == TriggerType.Time: - trigger = TaskTrigger._decode_time_trigger(reader, version) - else: - vollog.warning( - f"Invalid trigger magic {hex(magic)} encountered at offset {hex(reader.tell() - 8)}, stopping parsing" - ) - break - triggers.append(trigger) - - return cls(job_bucket, triggers) - - -@dataclasses.dataclass -class ActionSet: - actions: List[TaskAction] - context: Optional[str] - - @classmethod - def decode(cls, data: bytes) -> Optional["ActionSet"]: - reader = _ScheduledTasksReader(data) - actions = [] - - version = reader.read_u2() - if version is None: - return None - - if version in [2, 3]: - action_context = reader.read_bstring() - else: - action_context = None - - while True: - magic = reader.read_u2() - if magic is None: - break - - _ = ( - reader.read_bstring() - ) # action identifier, usually (but not always) empty - - if magic == ActionType.Email.value: - action = TaskAction._decode_email_action(reader) - elif magic == ActionType.Exe.value: - action = TaskAction._decode_exe_action(reader, version) - elif magic == ActionType.ComHandler.value: - action = TaskAction._decode_comhandler_action(reader) - elif magic == ActionType.MessageBox.value: - action = TaskAction.decode_messagebox_action(reader) - else: - break - actions.append(action) - - return cls(actions, action_context) - - -@dataclasses.dataclass -class DynamicInfo: - """ - Contains information about execution history for this task, - including timestamps and the last error code - """ - - creation_time: Optional[datetime.datetime] - last_run_time: Optional[datetime.datetime] - last_successful_run_time: Optional[datetime.datetime] - last_error_code: Optional[int] - - @classmethod - def decode(cls, data: bytes) -> Optional["DynamicInfo"]: - """ - Decodes a DynamicInfo structure from RegBin value data. - Raises a `ScheduledTaskDecodingError` if the magic bytes are invalid, but otherwise - attempts to decode as much as possible without returning an error. - """ - DYNAMICINFO_MAGIC = 3 - - reader = _ScheduledTasksReader(data) - magic = reader.read_u4() - if magic != DYNAMICINFO_MAGIC: - return None - - creation_time = reader.decode_filetime() - last_run_time = reader.decode_filetime() - - reader.seek(4, io.SEEK_CUR) # deprecated field 'TaskState' - - last_error_code = reader.read_u4() - last_success_time = reader.decode_filetime() - - vollog.debug((creation_time, last_run_time, last_success_time)) - - return cls( - last_run_time, - creation_time, - last_success_time, - last_error_code, - ) - - -class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): +class ScheduledTasks( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=scheduled_tasks.ScheduledTasks, + removal_date="2026-09-25", +): """Decodes scheduled task information from the Windows registry, including - information about triggers, actions, run times, and creation times. - """ + information about triggers, actions, run times, and creation times (deprecated).""" _required_framework_version = (2, 11, 0) - _version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel33", "Intel64"], - ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) - ), - ] - - def generate_timeline( - self, - ) -> 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 - 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 - 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 - - @classmethod - def get_software_hive( - cls, - context: interfaces.context.ContextInterface, - config_path: str, - kernel: interfaces.context.ModuleInterface, - ) -> Optional[registry.RegistryHive]: - """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" - return next( - hivelist.HiveList.list_hives( - context=context, - base_config_path=interfaces.configuration.path_join( - config_path, "hivelist" - ), - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_string="SOFTWARE", - ), - None, - ) - - @classmethod - def parse_actions_value( - cls, actions_value: reg_extensions.CM_KEY_VALUE - ) -> Optional[ActionSet]: - """Parses File entries from the Windows 8 `Root\\File` key. - - :param programs_key: The `Root\\File` registry key. - - :return: An iterator of tuples, where the first member is the program ID string for - correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. - """ - try: - data = actions_value.decode_data() - except exceptions.InvalidAddressException: - data = None - - if not isinstance(data, bytes): - return None - - return ActionSet.decode(data) - - @classmethod - def parse_triggers_value( - cls, triggers_value: reg_extensions.CM_KEY_VALUE - ) -> Optional[TriggerSet]: - try: - data = triggers_value.decode_data() - except exceptions.InvalidAddressException: - data = None - - if not isinstance(data, bytes): - return None - - return TriggerSet.decode(data) - - @classmethod - 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: - data = None - - if not isinstance(data, bytes): - return None - - return DynamicInfo.decode(data) - - @classmethod - def _get_task_keys( - cls, software_hive: reg_extensions.RegistryHive - ) -> Tuple[ - Optional[reg_extensions.CM_KEY_NODE], Optional[reg_extensions.CM_KEY_NODE] - ]: - try: - task_key = software_hive.get_key( - "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tasks" - ) - except (KeyError, registry.RegistryFormatException): - task_key = None - - try: - task_tree = software_hive.get_key( - "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree" - ) - except (KeyError, registry.RegistryFormatException): - task_tree = None - - return (task_key, task_tree) # type: ignore - - @classmethod - def _parse_task_key( - cls, key: reg_extensions.CM_KEY_NODE, guid_mapping: Dict[str, str] - ) -> Iterator[_ScheduledTaskEntry]: - values = {} - for value in key.get_values(): - try: - name = str(value.get_name()) - except exceptions.InvalidAddressException: - continue - - if name in ["Actions", "Triggers", "DynamicInfo"]: - values[name] = value - - task_name = guid_mapping.get(str(key.get_name()), renderers.NotAvailableValue()) - - try: - action_set = cls.parse_actions_value(values["Actions"]) - except KeyError: - vollog.debug("Failed to get Actions value") - action_set = None - - try: - triggers_value = values["Triggers"] - trigger_set = cls.parse_triggers_value(triggers_value) - except KeyError: - vollog.debug("Failed to get Triggers value") - trigger_set = None - - if trigger_set is not None: - vollog.debug("Parsed triggers successfully") - - principal_id = ( - trigger_set.job_bucket.principal_id or renderers.NotAvailableValue() - ) - display_name = ( - trigger_set.job_bucket.display_name or renderers.NotAvailableValue() - ) - else: - vollog.debug("Failed to parse triggers") - - principal_id = renderers.NotAvailableValue() - display_name = renderers.NotAvailableValue() - - try: - dynamic_info = cls.parse_dynamic_info_value(values["DynamicInfo"]) - except KeyError: - vollog.debug("DynamicInfo value not found") - dynamic_info = None - - vollog.debug(dynamic_info) - - creation_time = dynamic_info.creation_time if dynamic_info is not None else None - last_run_time = dynamic_info.last_run_time if dynamic_info is not None else None - last_successful_run_time = ( - dynamic_info.last_successful_run_time if dynamic_info is not None else None - ) - - all_triggers = ( - trigger_set.triggers or [None] if trigger_set is not None else [None] - ) - - 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, - ActionType.ComHandler, - ): - if action.action_args is None: - args = renderers.NotAvailableValue() - else: - args = action.action_args - else: - args = renderers.NotApplicableValue() - - if action.action_type == ActionType.Exe: - working_directory = ( - action.working_directory or renderers.NotAvailableValue() - ) - else: - working_directory = renderers.NotApplicableValue() - - else: - args = renderers.NotAvailableValue() - working_directory = renderers.NotAvailableValue() - - if trigger is not None and trigger.enabled is not None: - enabled = trigger.enabled - else: - enabled = renderers.NotAvailableValue() - - yield _ScheduledTaskEntry( - task_name, - principal_id, - display_name, - enabled, - creation_time or renderers.NotAvailableValue(), - last_run_time or renderers.NotAvailableValue(), - last_successful_run_time or renderers.NotAvailableValue(), - ( - trigger.trigger_type.name - if trigger is not None - else renderers.NotAvailableValue() - ), - ( - trigger.description or renderers.NotAvailableValue() - if trigger is not None - else renderers.NotAvailableValue() - ), - ( - action.action_type.name - if action is not None - else renderers.NotAvailableValue() - ), - ( - action.action - if action is not None - else renderers.NotAvailableValue() - ), - args, - ( - action_set.context - if action_set is not None - else renderers.NotAvailableValue() - ), - working_directory, - str(key.get_name()), - ) - - def _generator(self) -> Iterator[Tuple[int, _ScheduledTaskEntry]]: - kernel = self.context.modules[self.config["kernel"]] - - # Building the dictionary ahead of time is much better for performance - # vs looking up each service's DLL individually. - software_hive = self.get_software_hive(self.context, self.config_path, kernel) - if software_hive is None: - vollog.warning("Failed to get SOFTWARE hive") - return - - task_key_root, task_tree = self._get_task_keys(software_hive) - if task_key_root is None: - vollog.warning("Failed to get 'Tasks' key") - return - - if task_tree is not None: - task_name_map = _build_guid_name_map(task_tree) - else: - vollog.info("'Tree' key not found, can't map GUIDs to task names") - task_name_map = {} - - for key in task_key_root.get_subkeys(): - for task in self._parse_task_key(key, task_name_map): - yield 0, task - - def run(self): - return renderers.TreeGrid( - [ - ("Task Name", str), - ("Principal ID", str), - ("Display Name", str), - ("Enabled", bool), - ("Creation Time", datetime.datetime), - ("Last Run Time", datetime.datetime), - ("Last Successful Run Time", datetime.datetime), - ("Trigger Type", str), - ("Trigger Description", str), - ("Action Type", str), - ("Action", str), - ("Action Arguments", str), - ("Action Context", str), - ("Working Directory", str), - ("Key Name", str), - ], - ( - (indent, dataclasses.astuple(entry)) - for indent, entry in self._generator() - ), - ) + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/sessions.py b/volatility3/framework/plugins/windows/sessions.py index d766b40ea..158820529 100644 --- a/volatility3/framework/plugins/windows/sessions.py +++ b/volatility3/framework/plugins/windows/sessions.py @@ -27,8 +27,13 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.ListRequirement( name="pid", @@ -39,16 +44,14 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) ] def _generator(self): - kernel = self.context.modules[self.config["kernel"]] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) # Collect all the values as we will want to group them later sessions = {} for proc in pslist.PsList.list_processes( - self.context, - kernel.layer_name, - kernel.symbol_table_name, + context=self.context, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ): session_id = proc.get_session_id() @@ -92,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 b425918e1..8935757d4 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -17,8 +17,6 @@ from volatility3.framework.symbols.windows.extensions import pe, shimcache from volatility3.plugins import timeliner from volatility3.plugins.windows import modules, pslist, vadinfo -# from volatility3.plugins.windows import pslist, vadinfo, modules - vollog = logging.getLogger(__name__) @@ -26,6 +24,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf """Reads Shimcache entries from the ahcache.sys AVL tree""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) # These checks must be completed from newest -> oldest OS version. _win_version_file_map: List[Tuple[versions.OsDistinguisher, bool, str]] = [ @@ -52,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]: @@ -65,21 +72,27 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), requirements.VersionRequirement( name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(2, 0, 0) + name="modules", component=modules.Modules, version=(3, 0, 0) ), ] - @staticmethod + @classmethod def create_shimcache_table( + cls, context: interfaces.context.ContextInterface, - symbol_table: str, + symbol_table_name: str, config_path: str, ) -> str: """Creates a shimcache symbol table @@ -92,16 +105,18 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf Returns: The name of the constructed shimcache table """ - native_types = context.symbol_space[symbol_table].natives - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) - table_mapping = {"nt_symbols": symbol_table} + native_types = context.symbol_space[symbol_table_name].natives + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=symbol_table_name + ) + table_mapping = {"nt_symbols": symbol_table_name} try: symbol_filename = next( filename for version_check, for_64bit, filename in ShimcacheMem._win_version_file_map if is_64bit == for_64bit - and version_check(context=context, symbol_table=symbol_table) + and version_check(context=context, symbol_table=symbol_table_name) ) except StopIteration: raise NotImplementedError("This version of Windows is not supported!") @@ -122,8 +137,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf def find_shimcache_win_xp( cls, context: interfaces.context.ContextInterface, - layer_name: str, - kernel_symbol_table: str, + kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Attempts to find the shimcache in a Windows XP memory image @@ -142,11 +156,9 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf seen = set() - for process in pslist.PsList.list_processes( - context, layer_name, kernel_symbol_table - ): + for process in pslist.PsList.list_processes(context, kernel_module_name): pid = process.UniqueProcessId - vollog.debug("checking process %d" % pid) + vollog.debug("checking process %d", pid) for vad in vadinfo.VadInfo.list_vads( process, lambda x: x.get_tag() == b"Vad " and x.Protection == 4 ): @@ -170,6 +182,8 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf vad.get_start() + SHIM_NUM_ENTRIES_OFFSET, ) + vollog.debug(f"Found {num_entries} shimcache entries") + if num_entries > SHIM_MAX_ENTRIES: continue @@ -200,7 +214,6 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf if physical_addr in seen: continue - seen.add(physical_addr) shim_entry = proc_layer.context.object( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", @@ -212,6 +225,8 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf if not shim_entry.is_valid(): continue + seen.add(physical_addr) + yield shim_entry @classmethod @@ -219,8 +234,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf cls, context: interfaces.context.ContextInterface, config_path: str, - kernel_layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Implements the algorithm to search for the shim cache on Windows 2000 @@ -230,7 +244,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf 2) Iterate over every 4/8 bytes (depending on OS bitness) in the .data section and test for the following: a) offset represents a valid RTL_AVL_TABLE object - b) RTL_AVL_TABLE is preceeded by an ERESOURCE object + b) RTL_AVL_TABLE is preceded by an ERESOURCE object c) RTL_AVL_TABLE is followed by the beginning of the SHIM LRU list :param context: The context to retrieve required elements (layers, symbol tables) from @@ -239,31 +253,37 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf :param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols """ + kernel = context.modules[kernel_module_name] + data_sec = cls.get_module_section_range( context, config_path, - kernel_layer_name, - nt_symbol_table, + kernel_module_name, cls.NT_KRNL_MODS, ".data", ) mod_page = cls.get_module_section_range( context, config_path, - kernel_layer_name, - nt_symbol_table, + kernel_module_name, cls.NT_KRNL_MODS, "PAGE", ) # We require both in order to accurately handle AVL table if not (data_sec and mod_page): - return None + return data_sec_offset, data_sec_size = data_sec mod_page_offset, mod_page_size = mod_page - addr_size = 8 if symbols.symbol_table_is_64bit(context, nt_symbol_table) else 4 + addr_size = ( + 8 + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) + else 4 + ) shim_head = None for offset in range( @@ -272,8 +292,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf shim_head = cls.try_get_shim_head_at_offset( context, shimcache_symbol_table, - nt_symbol_table, - kernel_layer_name, + kernel_module_name, mod_page_offset, mod_page_offset + mod_page_size, offset, @@ -285,18 +304,16 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf if not shim_head: return - for shim_entry in shim_head.ListEntry.to_list( + yield from shim_head.ListEntry.to_list( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", "ListEntry" - ): - yield shim_entry + ) @classmethod def try_get_shim_head_at_offset( cls, context: interfaces.context.ContextInterface, - symbol_table: str, - kernel_symbol_table: str, - layer_name: str, + shimcache_symbol_table: str, + kernel_module_name: str, mod_page_start: int, mod_page_end: int, offset: int, @@ -308,35 +325,42 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf If a number of validity checks are passed, this method will return the `SHIM_CACHE_HEAD` object. Otherwise, `None` is returned. """ - # print("checking RTL_AVL_TABLE at offset %s" % hex(offset)) + + kernel = context.modules[kernel_module_name] + + # Check RTL_AVL_TABLE at offset rtl_avl_table = context.object( - symbol_table + constants.BANG + "_RTL_AVL_TABLE", layer_name, offset + shimcache_symbol_table + constants.BANG + "_RTL_AVL_TABLE", + kernel.layer_name, + offset, ) if not rtl_avl_table.is_valid(mod_page_start, mod_page_end): return None - vollog.debug(f"Candidate RTL_AVL_TABLE found at offset {hex(offset)}") + vollog.debug(f"Candidate RTL_AVL_TABLE found at offset {offset:#x}") ersrc_size = context.symbol_space.get_type( - kernel_symbol_table + constants.BANG + "_ERESOURCE" + kernel.symbol_table_name + constants.BANG + "_ERESOURCE" ).size ersrc_alignment = ( 0x20 - if symbols.symbol_table_is_64bit(context, kernel_symbol_table) + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) else 0x10 # 0x20 if context.symbol_space.get_type("pointer").size == 8 else 0x10 ) vollog.debug( - f"ERESOURCE size: {hex(ersrc_size)}, ERESOURCE alignment: {hex(ersrc_alignment)}" + f"ERESOURCE size: {ersrc_size:#x}, ERESOURCE alignment: {ersrc_alignment:#x}" ) eresource_rel_off = ersrc_size + ((offset - ersrc_size) % ersrc_alignment) eresource_offset = offset - eresource_rel_off - vollog.debug("Constructing ERESOURCE at %s" % hex(eresource_offset)) + vollog.debug(f"Constructing ERESOURCE at {eresource_offset:#x}") eresource = context.object( - kernel_symbol_table + constants.BANG + "_ERESOURCE", - layer_name, + kernel.symbol_table_name + constants.BANG + "_ERESOURCE", + kernel.layer_name, eresource_offset, ) if not eresource.is_valid(): @@ -345,12 +369,12 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf shim_head_offset = offset + rtl_avl_table.vol.size - if not context.layers[layer_name].is_valid(shim_head_offset): + if not context.layers[kernel.layer_name].is_valid(shim_head_offset): return None shim_head = context.object( - symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", - layer_name, + shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", + kernel.layer_name, shim_head_offset, ) @@ -366,8 +390,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf cls, context: interfaces.context.ContextInterface, config_path: str, - kernel_layer_name: str, - nt_symbol_table: str, + kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Attempts to locate and yield shimcache entries from a Windows 8 or later memory image. @@ -377,10 +400,11 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf :param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols :param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols """ + kernel = context.modules[kernel_module_name] is_8_1_or_later = versions.is_windows_8_1_or_later( - context, nt_symbol_table - ) or versions.is_win10(context, nt_symbol_table) + context, kernel.symbol_table_name + ) or versions.is_win10(context, kernel.symbol_table_name) module_names = ["ahcache.sys"] if is_8_1_or_later else cls.NT_KRNL_MODS vollog.debug(f"Searching for modules {module_names}") @@ -388,16 +412,14 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf data_sec = cls.get_module_section_range( context, config_path, - kernel_layer_name, - nt_symbol_table, + kernel_module_name, module_names, ".data", ) mod_page = cls.get_module_section_range( context, config_path, - kernel_layer_name, - nt_symbol_table, + kernel_module_name, module_names, "PAGE", ) @@ -411,8 +433,8 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf # iterate over ahcache kernel module's .data section in search of *two* SHIM handles shim_heads = [] - vollog.debug(f"PAGE offset: {hex(mod_page_offset)}") - vollog.debug(f".data offset: {hex(data_sec_offset)}") + vollog.debug(f"PAGE offset: {mod_page_offset:#x}") + vollog.debug(f".data offset: {data_sec_offset:#x}") handle_type = context.symbol_space.get_type( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_HANDLE" @@ -420,12 +442,18 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf for offset in range( data_sec_offset, data_sec_offset + data_sec_size, - 8 if symbols.symbol_table_is_64bit(context, nt_symbol_table) else 4, + ( + 8 + if symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) + else 4 + ), ): - vollog.debug(f"Building shim handle pointer at {hex(offset)}") + vollog.debug(f"Building shim handle pointer at {offset:#x}") shim_handle = context.object( object_type=shimcache_symbol_table + constants.BANG + "pointer", - layer_name=kernel_layer_name, + layer_name=kernel.layer_name, subtype=handle_type, offset=offset, ) @@ -433,7 +461,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf if shim_handle.is_valid(mod_page_offset, mod_page_offset + mod_page_size): if shim_handle.head is not None: vollog.debug( - f"Found valid shim handle @ {hex(shim_handle.vol.offset)}" + f"Found valid shim handle @ {shim_handle.vol.offset:#x}" ) shim_heads.append(shim_handle.head) if len(shim_heads) == 2: @@ -443,10 +471,12 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf vollog.debug("Failed to identify two valid SHIM_CACHE_HANDLE structures") return - # On Windows 8 x64, the frist cache contains the shim cache + # On Windows 8 x64, the first cache contains the shim cache. # On Windows 8 x86, 8.1 x86/x64, and 10, the second cache contains the shim cache. if ( - not symbols.symbol_table_is_64bit(context, nt_symbol_table) + not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) and not is_8_1_or_later ): valid_head = shim_heads[1] @@ -475,8 +505,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf entries = self.find_shimcache_win_8_or_later( self.context, self.config_path, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], shimcache_table_name, ) @@ -489,8 +518,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf entries = self.find_shimcache_win_2k3_to_7( self.context, self.config_path, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], shimcache_table_name, ) @@ -500,8 +528,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf vollog.info("Finding shimcache entries for WinXP") entries = self.find_shimcache_win_xp( self._context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], shimcache_table_name, ) else: @@ -548,8 +575,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf cls, context: interfaces.context.ContextInterface, config_path: str, - layer_name: str, - symbol_table: str, + kernel_module_name: str, module_list: List[str], section_name: str, ) -> Optional[Tuple[int, int]]: @@ -564,17 +590,23 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf :return: The offset and size of the module, if found; Otherwise, returns `None` """ - try: - krnl_mod = next( - module - for module in modules.Modules.list_modules( - context, layer_name, symbol_table + krnl_mod = None + for module in modules.Modules.list_modules(context, kernel_module_name): + try: + if module.BaseDllName.String in module_list: + krnl_mod = module + break + except exceptions.InvalidAddressException as exc: + vollog.warning( + f"Failed to get kernel module due to {exc.__class__.__name__}: {exc.invalid_address:#x}" ) - if module.BaseDllName.String in module_list - ) - except StopIteration: + + if krnl_mod is None: + vollog.warning("Failed to find kernel module") return None + kernel = context.modules[kernel_module_name] + pe_table_name = intermed.IntermediateSymbolTable.create( context, interfaces.configuration.path_join(config_path, "pe"), @@ -586,7 +618,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf # code taken from Win32KBase._section_chunks (win32_core.py) dos_header = context.object( pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - layer_name, + kernel.layer_name, offset=krnl_mod.DllBase, ) diff --git a/volatility3/framework/plugins/windows/sids_and_privileges.json b/volatility3/framework/plugins/windows/sids_and_privileges.json index 0378699ac..4333617d0 100644 --- a/volatility3/framework/plugins/windows/sids_and_privileges.json +++ b/volatility3/framework/plugins/windows/sids_and_privileges.json @@ -573,7 +573,7 @@ ["S-1-5-21-[0-9-]+-553$", "Remote Access Services (RAS)"] ], "privileges":{ - "2": ["SeCreateTokenPrivilege", "Create a token object"], + "2": ["SeCreateTokenPrivilege", "Create a token object"], "3": ["SeAssignPrimaryTokenPrivilege", "Replace a process-level token"], "4": ["SeLockMemoryPrivilege", "Lock pages in memory"], "5": ["SeIncreaseQuotaPrivilege", "Increase quotas"], @@ -609,4 +609,4 @@ "35": ["SeCreateSymbolicLinkPrivilege", "Required to create a symbolic link"], "36": ["SeDelegateSessionUserImpersonatePrivilege", "Obtain an impersonation token for another user in the same session."] } -} \ No newline at end of file +} diff --git a/volatility3/framework/plugins/windows/skeleton_key_check.py b/volatility3/framework/plugins/windows/skeleton_key_check.py index d321c2cc0..86c5cf1df 100644 --- a/volatility3/framework/plugins/windows/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/skeleton_key_check.py @@ -1,712 +1,20 @@ -# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -# This module attempts to locate skeleton-key like function hooks. -# It does this by locating the CSystems array through a variety of methods, -# and then validating the entry for RC4 HMAC (0x17 / 23) -# -# For a thorough walkthrough on how the R&D was performed to develop this plugin, -# please see our blogpost here: -# -# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html - -import io import logging -from typing import Iterable, Tuple, List, Optional - -import pefile - -from volatility3.framework import interfaces, symbols, exceptions -from volatility3.framework import renderers, constants -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import scanners -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows import pdbutil -from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, vadinfo - -try: - import capstone - - has_capstone = True -except ImportError: - has_capstone = False +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import skeleton_key_check vollog = logging.getLogger(__name__) -class Skeleton_Key_Check(interfaces.plugins.PluginInterface): +class Skeleton_Key_Check( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=skeleton_key_check.Skeleton_Key_Check, + removal_date="2026-06-07", +): """Looks for signs of Skeleton Key malware""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) - ), - ] - - def _get_pefile_obj( - self, pe_table_name: str, layer_name: str, base_address: int - ) -> pefile.PE: - """ - Attempts to pefile object from the bytes of the PE file - - Args: - pe_table_name: name of the pe types table - layer_name: name of the lsass.exe process layer - base_address: base address of cryptdll.dll in lsass.exe - - Returns: - the constructed pefile object - """ - pe_data = io.BytesIO() - - try: - dos_header = self.context.object( - pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset=base_address, - layer_name=layer_name, - ) - - for offset, data in dos_header.reconstruct(): - pe_data.seek(offset) - pe_data.write(data) - - pe_ret = pefile.PE(data=pe_data.getvalue(), fast_load=True) - - except exceptions.InvalidAddressException: - vollog.debug("Unable to reconstruct cryptdll.dll in memory") - pe_ret = None - - return pe_ret - - def _check_for_skeleton_key_vad( - self, - csystem: interfaces.objects.ObjectInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> bool: - """ - Checks if Initialize and/or Decrypt is hooked by determining if - these function pointers reference addresses inside of the cryptdll VAD - - Args: - csystem: The RC4HMAC KERB_ECRYPT instance - cryptdll_base: Base address of the cryptdll.dll VAD - cryptdll_size: Size of the VAD - Returns: - bool: if a skeleton key hook is present - """ - return not ( - (cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) - and (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size) - ) - - def _check_for_skeleton_key_symbols( - self, - csystem: interfaces.objects.ObjectInterface, - rc4HmacInitialize: int, - rc4HmacDecrypt: int, - ) -> bool: - """ - Uses the PDB information to specifically check if the csystem for RC4HMAC - has an initialization pointer to rc4HmacInitialize and a decryption pointer - to rc4HmacDecrypt. - - Args: - csystem: The RC4HMAC KERB_ECRYPT instance - rc4HmacInitialize: The expected address of csystem Initialization function - rc4HmacDecrypt: The expected address of the csystem Decryption function - - Returns: - bool: if a skeleton key hook was found - """ - return ( - csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt - ) - - def _construct_ecrypt_array( - self, - array_start: int, - count: int, - cryptdll_types: interfaces.context.ModuleInterface, - ) -> interfaces.context.ModuleInterface: - """ - Attempts to construct an array of _KERB_ECRYPT structures - - Args: - array_start: starting virtual address of the array - count: how many elements are in the array - cryptdll_types: the reverse engineered types - - Returns: - The instantiated array - """ - - try: - array = cryptdll_types.object( - object_type="array", - offset=array_start, - subtype=cryptdll_types.get_type("_KERB_ECRYPT"), - count=count, - absolute=True, - ) - - except exceptions.InvalidAddressException: - vollog.debug( - "Unable to construct cSystems array at given offset: {:x}".format( - array_start - ) - ) - array = None - - return array - - def _find_array_with_pdb_symbols( - self, - cryptdll_symbols: str, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str, - cryptdll_base: int, - ) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: - """ - Finds the CSystems array through use of PDB symbols - - Args: - cryptdll_symbols: The symbols table from the PDB file - cryptdll_types: The types from cryptdll binary analysis - proc_layer_name: The lsass.exe process layer name - cryptdll_base: Base address of cryptdll.dll inside of lsass.exe - - Returns: - Tuple of: - array: The cSystems array - rc4HmacInitialize: The runtime address of the expected initialization function - rc4HmacDecrypt: The runtime address of the expected decryption function - """ - cryptdll_module = self.context.module( - cryptdll_symbols, layer_name=proc_layer_name, offset=cryptdll_base - ) - - rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address( - "rc4HmacInitialize" - ) - - rc4HmacDecrypt = cryptdll_module.get_absolute_symbol_address("rc4HmacDecrypt") - - count_address = cryptdll_module.get_symbol("cCSystems").address - - # we do not want to fail just because the count is not in memory - # 16 was the size on samples I tested, so I chose it as the default - try: - count = cryptdll_types.object( - object_type="unsigned long", offset=count_address - ) - except exceptions.InvalidAddressException: - count = 16 - - array_start = cryptdll_module.get_absolute_symbol_address("CSystems") - - array = self._construct_ecrypt_array(array_start, count, cryptdll_types) - - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping PDB based analysis." - ) - - return array, rc4HmacInitialize, rc4HmacDecrypt - - def _get_cryptdll_types( - self, - context: interfaces.context.ContextInterface, - config, - config_path: str, - proc_layer_name: str, - cryptdll_base: int, - ): - """ - Builds a symbol table from the cryptdll types generated after binary analysis - - Args: - context: the context to operate upon - config: - config_path: - proc_layer_name: name of the lsass.exe process layer - cryptdll_base: base address of cryptdll.dll inside of lsass.exe - """ - kernel = self.context.modules[self.config["kernel"]] - table_mapping = {"nt_symbols": kernel.symbol_table_name} - - cryptdll_symbol_table = intermed.IntermediateSymbolTable.create( - context=context, - config_path=config_path, - sub_path="windows", - filename="kerb_ecrypt", - table_mapping=table_mapping, - ) - - return context.module( - cryptdll_symbol_table, proc_layer_name, offset=cryptdll_base - ) - - def _find_lsass_proc( - self, proc_list: Iterable - ) -> Tuple[interfaces.context.ContextInterface, str]: - """ - Walks the process list and returns the first valid lsass instances. - There should be only one lsass process, but malware will often use the - process name to try and blend in. - - Args: - proc_list: The process list generator - - Return: - The process object for lsass - """ - - for proc in proc_list: - try: - proc_id = proc.UniqueProcessId - proc_layer_name = proc.add_process_layer() - - return proc, proc_layer_name - - except exceptions.InvalidAddressException as excp: - vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) - ) - - return None, None - - def _find_cryptdll( - self, lsass_proc: interfaces.context.ContextInterface - ) -> Tuple[int, int]: - """ - Finds the base address of cryptdll.dll inside of lsass.exe - - Args: - lsass_proc: the process object for lsass.exe - - Returns: - A tuple of: - cryptdll_base: the base address of cryptdll.dll - crytpdll_size: the size of the VAD for cryptdll.dll - """ - for vad in lsass_proc.get_vad_root().traverse(): - filename = vad.get_file_name() - - if isinstance(filename, str) and filename.lower().endswith("cryptdll.dll"): - base = vad.get_start() - return base, vad.get_size() - - return None, None - - def _find_csystems_with_symbols( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> Tuple[interfaces.objects.ObjectInterface, int, int]: - """ - Attempts to find CSystems and the expected address of the handlers. - Relies on downloading and parsing of the cryptdll PDB file. - - Args: - proc_layer_name: the name of the lsass.exe process layer - cryptdll_types: The types from cryptdll binary analysis - cryptdll_base: the base address of cryptdll.dll - crytpdll_size: the size of the VAD for cryptdll.dll - - Returns: - A tuple of: - array: An initialized Volatility array of _KERB_ECRYPT structures - rc4HmacInitialize: The expected address of csystem Initialization function - rc4HmacDecrypt: The expected address of the csystem Decryption function - """ - try: - cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( - self.context, - interfaces.configuration.path_join(self.config_path, "cryptdll"), - proc_layer_name, - "cryptdll.pdb", - cryptdll_base, - cryptdll_size, - ) - except exceptions.VolatilityException: - vollog.debug( - "Unable to use the cryptdll PDB. Stopping PDB symbols based analysis." - ) - return None, None, None - - array, rc4HmacInitialize, rc4HmacDecrypt = self._find_array_with_pdb_symbols( - cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base - ) - - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping PDB symbols based analysis." - ) - - return array, rc4HmacInitialize, rc4HmacDecrypt - - def _get_rip_relative_target(self, inst) -> int: - """ - Returns the target address of a RIP-relative instruction. - - These instructions contain the offset of a target address - relative to the current instruction pointer. - - Args: - inst: A capstone instruction instance - - Returns: - None or the target address of the instruction - """ - try: - opnd = inst.operands[1] - except capstone.CsError: - return None - - if opnd.type != capstone.x86.X86_OP_MEM: - return None - - if inst.reg_name(opnd.mem.base) != "rip": - return None - - return inst.address + inst.size + opnd.mem.disp - - def _analyze_cdlocatecsystem( - self, - function_bytes: bytes, - function_start: int, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str, - ) -> Optional[interfaces.objects.ObjectInterface]: - """ - Performs static analysis on CDLocateCSystem to find the instructions that - reference CSystems as well as cCsystems - - Args: - function_bytes: the instruction bytes of CDLocateCSystem - function_start: the address of CDLocateCSystem - proc_layer_name: the name of the lsass.exe process layer - - Return: - The cSystems array of ecrypt instances - """ - found_count = False - array_start = None - count = None - - ## we only support 64bit disassembly analysis - md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) - md.detail = True - - for inst in md.disasm(function_bytes, function_start): - # we should not reach debug traps - if inst.mnemonic == "int3": - break - - # cCsystems is referenced by a mov instruction - elif inst.mnemonic == "mov": - if not found_count: - target_address = self._get_rip_relative_target(inst) - - # we do not want to fail just because the count is not in memory - # 16 was the size on samples I tested, so I chose it as the default - if target_address: - count = int.from_bytes( - self.context.layers[proc_layer_name].read( - target_address, 4 - ), - "little", - ) - else: - count = 16 - - found_count = True - - elif inst.mnemonic == "lea": - target_address = self._get_rip_relative_target(inst) - - if target_address: - array_start = target_address - - # we find the count before, so we can terminate the static analysis here - break - - if array_start and count: - array = self._construct_ecrypt_array(array_start, count, cryptdll_types) - else: - array = None - - return array - - def _find_csystems_with_export( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - _, - ) -> Optional[interfaces.objects.ObjectInterface]: - """ - Uses export table analysis to locate CDLocateCsystem - This function references CSystems and cCsystems - - Args: - proc_layer_name: The lsass.exe process layer name - cryptdll_types: The types from cryptdll binary analysis - cryptdll_base: Base address of cryptdll.dll inside of lsass.exe - _: unused in this source - Returns: - The cSystems array - """ - - if not has_capstone: - vollog.debug( - "capstone is not installed so cannot fall back to export table analysis." - ) - return None - - vollog.debug( - "Unable to perform analysis using PDB symbols, falling back to export table analysis." - ) - - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - cryptdll = self._get_pefile_obj(pe_table_name, proc_layer_name, cryptdll_base) - if not cryptdll: - return None - - cryptdll.parse_data_directories( - directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] - ) - if not hasattr(cryptdll, "DIRECTORY_ENTRY_EXPORT"): - return None - - # find the location of CDLocateCSystem and then perform static analysis - for export in cryptdll.DIRECTORY_ENTRY_EXPORT.symbols: - if export.name != b"CDLocateCSystem": - continue - - function_start = cryptdll_base + export.address - - try: - function_bytes = self.context.layers[proc_layer_name].read( - function_start, 0x50 - ) - except exceptions.InvalidAddressException: - vollog.debug( - "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis." - ) - break - - array = self._analyze_cdlocatecsystem( - function_bytes, function_start, cryptdll_types, proc_layer_name - ) - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping export based analysis." - ) - - return array - - return None - - def _find_csystems_with_scanning( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> List[interfaces.context.ModuleInterface]: - """ - Performs scanning to find potential RC4 HMAC csystem instances - - This function may return several values as it cannot validate which is the active one - - Args: - proc_layer_name: the lsass.exe process layer name - cryptdll_types: the types from cryptdll binary analysis - cryptdll_base: base address of cryptdll.dll inside of lsass.exe - cryptdll_size: size of the VAD - Returns: - A list of csystem instances - """ - - csystems = [] - - cryptdll_end = cryptdll_base + cryptdll_size - - proc_layer = self.context.layers[proc_layer_name] - - ecrypt_size = cryptdll_types.get_type("_KERB_ECRYPT").size - - # scan for potential instances of RC4 HMAC - # the signature is based on the type being 0x17 - # and the block size member being 1 in all test samples - for address in proc_layer.scan( - self.context, - scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), - sections=[(cryptdll_base, cryptdll_size)], - ): - # this occurs across page boundaries - if not proc_layer.is_valid(address, ecrypt_size): - continue - - kerb = cryptdll_types.object("_KERB_ECRYPT", offset=address, absolute=True) - - # ensure the Encrypt and Finish pointers are inside the VAD - # these are not manipulated in the attack - if (cryptdll_base < kerb.Encrypt < cryptdll_end) and ( - cryptdll_base < kerb.Finish < cryptdll_end - ): - csystems.append(kerb) - - return csystems - - def _generator(self, procs): - """ - Finds instances of the RC4 HMAC CSystem structure - - Returns whether the instances are hooked as well as the function handler addresses - - Args: - procs: the process list filtered to lsass.exe instances - """ - kernel = self.context.modules[self.config["kernel"]] - - if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name): - vollog.info("This plugin only supports 64bit Windows memory samples") - return None - - lsass_proc, proc_layer_name = self._find_lsass_proc(procs) - if not lsass_proc: - vollog.info( - "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed." - ) - return None - - cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc) - if not cryptdll_base: - vollog.info( - "Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed." - ) - return None - - # the custom type information from binary analysis - cryptdll_types = self._get_cryptdll_types( - self.context, self.config, self.config_path, proc_layer_name, cryptdll_base - ) - - # attempt to find the array and symbols directly from the PDB - csystems, rc4HmacInitialize, rc4HmacDecrypt = self._find_csystems_with_symbols( - proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size - ) - - # if we can't find cSystems through the PDB then - # we fall back to export analysis and scanning - # we keep the address of the rc4 functions from the PDB - # though as its our only source to get them - if csystems is None: - fallback_sources = [ - self._find_csystems_with_export, - self._find_csystems_with_scanning, - ] - - for source in fallback_sources: - csystems = source( - proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size - ) - - if csystems is not None: - break - - if csystems is None: - vollog.info( - "Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed." - ) - return None - - for csystem in csystems: - if not self.context.layers[proc_layer_name].is_valid( - csystem.vol.offset, csystem.vol.size - ): - continue - - # filter for RC4 HMAC - if csystem.EncryptionType != 0x17: - continue - - # use the specific symbols if present, otherwise use the vad start and size - if rc4HmacInitialize and rc4HmacDecrypt: - skeleton_key_present = self._check_for_skeleton_key_symbols( - csystem, rc4HmacInitialize, rc4HmacDecrypt - ) - else: - skeleton_key_present = self._check_for_skeleton_key_vad( - 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), - ) - - def _lsass_proc_filter(self, proc): - """ - Used to filter to only lsass.exe processes - - There should only be one of these, but malware can/does make lsass.exe - named processes to blend in or uses lsass.exe as a process hollowing target - """ - process_name = utility.array_to_string(proc.ImageFileName) - - return process_name != "lsass.exe" - - def run(self): - kernel = self.context.modules[self.config["kernel"]] - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Skeleton Key Found", bool), - ("rc4HmacInitialize", format_hints.Hex), - ("rc4HmacDecrypt", format_hints.Hex), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=self._lsass_proc_filter, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/ssdt.py b/volatility3/framework/plugins/windows/ssdt.py index 1fcb6cc91..b4fa39950 100644 --- a/volatility3/framework/plugins/windows/ssdt.py +++ b/volatility3/framework/plugins/windows/ssdt.py @@ -19,7 +19,9 @@ class SSDT(plugins.PluginInterface): """Lists the system call table.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + + # 2.0.0 - changed the signature of `build_module_collection` + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -29,8 +31,8 @@ class SSDT(plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(2, 0, 0) + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) ), ] @@ -38,23 +40,23 @@ class SSDT(plugins.PluginInterface): def build_module_collection( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> contexts.ModuleCollection: """Builds a collection of modules. Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + kernel_module_name: Name of the module for the kernel Returns: A Module collection of available modules based on `Modules.list_modules` """ - mods = modules.Modules.list_modules(context, layer_name, symbol_table) + mods = modules.Modules.list_modules(context, kernel_module_name) context_modules = [] + kernel = context.modules[kernel_module_name] + for mod in mods: try: module_name_with_ext = mod.BaseDllName.get_string() @@ -64,17 +66,13 @@ class SSDT(plugins.PluginInterface): module_name = os.path.splitext(module_name_with_ext)[0] - symbol_table_name = None - if module_name in constants.windows.KERNEL_MODULE_NAMES: - symbol_table_name = symbol_table - context_module = contexts.SizedModule.create( context=context, module_name=module_name, - layer_name=layer_name, + layer_name=kernel.layer_name, offset=mod.DllBase, size=mod.SizeOfImage, - symbol_table_name=symbol_table_name, + symbol_table_name=kernel.symbol_table_name, ) context_modules.append(context_module) @@ -84,15 +82,13 @@ class SSDT(plugins.PluginInterface): def _generator(self) -> Iterator[Tuple[int, Tuple[int, int, Any, Any]]]: kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name collection = self.build_module_collection( - self.context, layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], ) - kvo = self.context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = self.context.module( - kernel.symbol_table_name, layer_name=layer_name, offset=kvo - ) + ntkrnlmp = kernel + kvo = kernel.offset # this is just one way to enumerate the native (NT) service table. # to do the same thing for the Win32K service table, we would need Win32K.sys symbol support @@ -105,7 +101,7 @@ class SSDT(plugins.PluginInterface): # on 64-bit systems the indexes are also 32-bits but they're offsets from the # base address of the table and can be negative, so we need a signed data type is_kernel_64 = symbols.symbol_table_is_64bit( - self.context, kernel.symbol_table_name + context=self.context, symbol_table_name=kernel.symbol_table_name ) if is_kernel_64: array_subtype = "long" diff --git a/volatility3/framework/plugins/windows/strings.py b/volatility3/framework/plugins/windows/strings.py index 0eaa65884..9ea4ffed0 100644 --- a/volatility3/framework/plugins/windows/strings.py +++ b/volatility3/framework/plugins/windows/strings.py @@ -18,8 +18,11 @@ vollog = logging.getLogger(__name__) class Strings(interfaces.plugins.PluginInterface): """Reads output from the strings command and indicates which process(es) each string belongs to.""" - _version = (1, 2, 0) _required_framework_version = (2, 0, 0) + + # 2.0.0 - change signature of `generate_mapping` + _version = (2, 0, 0) + strings_pattern = re.compile(rb"^(?:\W*)([0-9]+)(?:\W*)(\w[\w\W]+)\n?") @classmethod @@ -30,8 +33,8 @@ class Strings(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ListRequirement( name="pid", @@ -68,12 +71,10 @@ class Strings(interfaces.plugins.PluginInterface): except ValueError: vollog.error(f"Line in unrecognized format: line {count}") line = strings_fp.readline() - kernel = self.context.modules[self.config["kernel"]] revmap = self.generate_mapping( - self.context, - kernel.layer_name, - kernel.symbol_table_name, + context=self.context, + kernel_module_name=self.config["kernel"], progress_callback=self._progress_callback, pid_list=self.config["pid"], ) @@ -122,8 +123,7 @@ class Strings(interfaces.plugins.PluginInterface): def generate_mapping( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, progress_callback: constants.ProgressCallback = None, pid_list: Optional[List[int]] = None, ) -> Dict[int, Set[Tuple[str, int]]]: @@ -132,8 +132,7 @@ class Strings(interfaces.plugins.PluginInterface): Args: context: the context for the method to run against - layer_name: the layer to map against the string lines - symbol_table: the name of the symbol table for the provided layer + kernel_module_name: the name of the module forthe kernel progress_callback: an optional callable to display progress pid_list: a lit of process IDs to consider when generating the reverse map @@ -142,7 +141,9 @@ class Strings(interfaces.plugins.PluginInterface): """ filter = pslist.PsList.create_pid_filter(pid_list) - layer = context.layers[layer_name] + kernel = context.modules[kernel_module_name] + + layer = context.layers[kernel.layer_name] reverse_map: Dict[int, Set[Tuple[str, int]]] = dict() if isinstance(layer, intel.Intel): # We don't care about errors, we just wanted chunks that map correctly @@ -161,7 +162,7 @@ class Strings(interfaces.plugins.PluginInterface): # TODO: Include kernel modules for process in pslist.PsList.list_processes( - context, layer_name, symbol_table + context=context, kernel_module_name=kernel_module_name ): if not filter(process): proc_id = "Unknown" @@ -170,9 +171,7 @@ class Strings(interfaces.plugins.PluginInterface): proc_layer_name = process.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) continue @@ -181,7 +180,7 @@ class Strings(interfaces.plugins.PluginInterface): for mapval in proc_layer.mapping( 0x0, proc_layer.maximum_address, ignore_errors=True ): - mapped_offset, _, offset, mapped_size, maplayer = mapval + mapped_offset, _, offset, mapped_size, _maplayer = mapval for val in range( mapped_offset, mapped_offset + mapped_size, 0x1000 ): diff --git a/volatility3/framework/plugins/windows/suspended_threads.py b/volatility3/framework/plugins/windows/suspended_threads.py new file mode 100644 index 000000000..82d44a6d7 --- /dev/null +++ b/volatility3/framework/plugins/windows/suspended_threads.py @@ -0,0 +1,148 @@ +import logging + +from typing import Dict +import functools + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +import volatility3.plugins.windows.pslist as pslist +import volatility3.plugins.windows.threads as threads +import volatility3.plugins.windows.pe_symbols as pe_symbols + +from volatility3.framework.objects import utility + +vollog = logging.getLogger(__name__) + + +class SuspendedThreads(interfaces.plugins.PluginInterface): + """Enumerates suspended threads.""" + + _required_framework_version = (2, 13, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="threads", component=threads.Threads, version=(3, 0, 0) + ), + ] + + def _generator(self): + """ + The goal of this plugin is to report on threads that are suspended + + Legitimate programs can start threads suspended but then will later resume them + + Subsets of malware techniques, such as EDR evasion and process hollowing, + create suspended threads and do not resume them. These are the threads that this + plugin is designed to catch. + + See the whitepaper from our DEF CON 2024 presentation for more details: + + https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + """ + vads_cache: Dict[int, pe_symbols.PESymbols.ranges_type] = {} + + proc_modules = None + + # walk the threads of each process checking for suspended threads + for proc in pslist.PsList.list_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ): + for thread in threads.Threads.list_threads( + self.context, self.config["kernel"], proc + ): + try: + # we only care if the thread is suspended + if thread.Tcb.SuspendCount == 0: + continue + + # 4 == terminated + if thread.Tcb.State == 4: + continue + + owner_proc = thread.owning_process() + owner_proc_pid = thread.Cid.UniqueProcess + owner_proc_name = utility.array_to_string(owner_proc.ImageFileName) + thread_tid = thread.Cid.UniqueThread + thread_start_addr = thread.StartAddress + thread_win32_addr = thread.Win32StartAddress + except exceptions.InvalidAddressException: + continue + + # Nothing useful to report if a process doesn't have VADs.. Also a sign of smear/terminated + vads = pe_symbols.PESymbols.get_vads_for_process_cache( + vads_cache, owner_proc + ) + if not vads: + continue + + # Only compute this if needed as its expensive and 99.9% of samples + # will not have suspended threads + if not proc_modules: + proc_modules = pe_symbols.PESymbols.get_process_modules( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_modules=None, + ) + + path_and_symbol = functools.partial( + pe_symbols.PESymbols.path_and_symbol_for_address, + self.context, + self.config_path, + proc_modules, + ) + + start_file, start_sym = path_and_symbol(vads, thread_start_addr) + win32_file, win32_sym = path_and_symbol(vads, thread_win32_addr) + + # the only false positive found in mass scanning of samples + if start_file and start_file.endswith("\\WorkFoldersShell.dll"): + continue + + if win32_file and win32_file.endswith("\\WorkFoldersShell.dll"): + continue + + yield ( + 0, + ( + owner_proc_name, + owner_proc_pid, + thread_tid, + start_file or renderers.NotAvailableValue(), + start_sym or renderers.NotAvailableValue(), + format_hints.Hex(thread_start_addr), + win32_file or renderers.NotAvailableValue(), + win32_sym or renderers.NotAvailableValue(), + format_hints.Hex(thread_win32_addr), + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("TID", int), + ("StartFile", str), + ("StartSymbol", str), + ("StartAddress", format_hints.Hex), + ("Win32StartFile", str), + ("Win32StartSymbol", str), + ("Win32StartAddress", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index 4bfb6baa5..068bdccaf 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -1,214 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List, Dict, Tuple, Generator -from volatility3.framework import renderers, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import suspicious_threads vollog = logging.getLogger(__name__) -class SuspiciousThreads(interfaces.plugins.PluginInterface): - """Lists suspicious userland process threads""" +class SuspiciousThreads( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=suspicious_threads.SuspiciousThreads, + removal_date="2026-06-07", +): + """Lists suspicious userland process threads (deprecated).""" _required_framework_version = (2, 4, 0) _version = (2, 0, 1) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, - ), - requirements.PluginRequirement( - name="threads", plugin=threads.Threads, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - def _get_ranges( - self, - kernel: interfaces.context.ModuleInterface, - all_ranges: Dict[int, List[Tuple[int, int, str, str]]], - proc, - ) -> Tuple[int, int, str, str]: - """ - Maintains a hash table so each process' VADs - are only enumerated once per plugin run - """ - key = proc.vol.offset - - if key not in all_ranges: - all_ranges[key] = [] - - for vad in proc.get_vad_root().traverse(): - fn = vad.get_file_name() - if not isinstance(fn, str) or not fn: - fn = None - - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, kernel.layer_name, kernel.symbol_table_name - ), - vadinfo.winnt_protections, - ) - - all_ranges[key].append( - (vad.get_start(), vad.get_end(), protection_string, fn) - ) - - return all_ranges[key] - - def _get_range( - self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int - ) -> Tuple[int, str, str]: - """ - Walks a process' VADs looking for the one - containing `address` - - Returns its base address, protection string, and mapped file, if any - """ - for start, end, protection_string, fn in ranges: - if start <= address < end: - return start, protection_string, fn - - return None, None, None - - def _check_thread_address( - self, exe_path: str, ranges, thread_address: int - ) -> Generator[Tuple[str, str], None, None]: - vad_base, prot, vad_path = self._get_range(ranges, thread_address) - - # threads outside of a VAD means either smear from this thread or this process' VAD tree - if vad_base is None: - return - - if vad_path is None: - # set this so checks after report the non file backed region in the path column - vad_path = "" - - yield ( - vad_path, - f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", - ) - - # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions - if prot != "PAGE_EXECUTE_WRITECOPY": - yield ( - vad_path, - f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", - ) - - # check for process hollowing type techniques that mapped in a second, malicious exe file - if ( - exe_path - and vad_path.lower().endswith(".exe") - and (vad_path.lower() != exe_path.lower()) - ): - yield ( - vad_path, - "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process exectuable", - ) - - def _enumerate_processes( - self, kernel: interfaces.context.ModuleInterface, all_ranges - ): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - for proc in pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - filter_func=filter_func, - ): - ranges = self._get_ranges(kernel, all_ranges, proc) - - # smeared vads or process is terminating - if len(all_ranges[proc.vol.offset]) < 5: - continue - - pid = proc.UniqueProcessId - proc_name = utility.array_to_string(proc.ImageFileName) - - _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) - if not isinstance(exe_path, str): - exe_path = None - - yield proc, pid, proc_name, exe_path, ranges - - def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - all_ranges = {} - - for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( - kernel, all_ranges - ): - # processes often create multiple threads at the same address - # there is no benefit to checking the same address more than once per process - checked = set() - - for thread in threads.Threads.list_threads(kernel, proc): - # do not process if a thread is exited or terminated (4 = Terminated) - if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: - continue - - # bail if accessing the threads members causes a page fault - info = thrdscan.ThrdScan.gather_thread_info(thread) - if not info: - continue - - _, _, tid, start_address, _, _ = info - - addresses = [ - (start_address, "Start"), - (thread.Win32StartAddress, "Win32Start"), - ] - - for address, context in addresses: - if address in checked: - continue - checked.add(address) - - 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, - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Process", str), - ("PID", int), - ("TID", int), - ("Context", str), - ("Address", format_hints.Hex), - ("VAD Path", str), - ("Note", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index 84d06a695..24bc53e49 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -1,103 +1,26 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -# This module attempts to locate skeleton-key like function hooks. -# It does this by locating the CSystems array through a variety of methods, -# and then validating the entry for RC4 HMAC (0x17 / 23) -# -# For a thorough walkthrough on how the R&D was performed to develop this plugin, -# please see our blogpost here: -# -# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html - import logging - -from volatility3.framework import symbols, interfaces -from volatility3.framework.configuration import requirements -from volatility3.plugins.windows import svclist, svcscan -from volatility3.framework.symbols.windows import versions +from volatility3.framework import deprecation +from volatility3.plugins.windows.malware import svcdiff +from volatility3.plugins.windows import svcscan vollog = logging.getLogger(__name__) -class SvcDiff(svcscan.SvcScan): - """Compares services found through list walking versus scanning to find rootkits""" - - _required_framework_version = (2, 4, 0) +class SvcDiff( + svcscan.SvcScan, + deprecation.PluginRenameClass, + replacement_class=svcdiff.SvcDiff, + removal_date="2026-06-07", +): + """Compares services found through list walking versus scanning to find rootkits (deprecated).""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._enumeration_method = self.service_diff - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="svclist", component=svclist.SvcList, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="svcscan", component=svcscan.SvcScan, version=(3, 0, 0) - ), - ] + _required_framework_version = (2, 4, 0) - @classmethod - def service_diff( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - service_table_name: str, - service_binary_dll_map, - filter_func, - ): - """ - On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list - and scan for services then report differences - """ - if not symbols.symbol_table_is_64bit( - context, symbol_table - ) or not versions.is_win10_15063_or_later( - context=context, symbol_table=symbol_table - ): - vollog.warning( - "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" - ) - return - - from_scan = set() - from_list = set() - records = {} - - # collect unique service names from scanning - for service in svcscan.SvcScan.service_scan( - context, - layer_name, - symbol_table, - service_table_name, - service_binary_dll_map, - filter_func, - ): - from_scan.add(service[6]) - records[service[6]] = service - - # collect services from listing walking - for service in svclist.SvcList.service_list( - context, - layer_name, - symbol_table, - service_table_name, - service_binary_dll_map, - filter_func, - ): - from_list.add(service[6]) - - # report services found from scanning but not list walking - for hidden_service in from_scan - from_list: - yield records[hidden_service] + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/svclist.py b/volatility3/framework/plugins/windows/svclist.py index a59581063..963b7fc71 100644 --- a/volatility3/framework/plugins/windows/svclist.py +++ b/volatility3/framework/plugins/windows/svclist.py @@ -18,7 +18,10 @@ vollog = logging.getLogger(__name__) class SvcList(svcscan.SvcScan): """Lists services contained with the services.exe doubly linked list of services""" - _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + # 2.0.0 - service_list signature changed + _version = (2, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -28,20 +31,28 @@ class SvcList(svcscan.SvcScan): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.PluginRequirement( - name="svcscan", plugin=svcscan.SvcScan, version=(3, 0, 0) + requirements.VersionRequirement( + name="svcscan", component=svcscan.SvcScan, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.ModuleRequirement( name="kernel", description="Windows kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), ] @classmethod def _get_exe_range(cls, proc) -> Optional[Tuple[int, int]]: """ - Returns a tuple of starting,ending address for + Returns a tuple of starting address and size of the VAD containing services.exe """ @@ -59,16 +70,17 @@ class SvcList(svcscan.SvcScan): def service_list( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, service_table_name: str, service_binary_dll_map, filter_func, ): + kernel = context.modules[kernel_module_name] + if not symbols.symbol_table_is_64bit( - context, symbol_table + context=context, symbol_table_name=kernel.symbol_table_name ) or not versions.is_win10_15063_or_later( - context=context, symbol_table=symbol_table + context=context, symbol_table=kernel.symbol_table_name ): vollog.warning( "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" @@ -77,21 +89,18 @@ class SvcList(svcscan.SvcScan): for proc in pslist.PsList.list_processes( context=context, - layer_name=layer_name, - symbol_table=symbol_table, + kernel_module_name=kernel_module_name, filter_func=filter_func, ): try: - layer_name = proc.add_process_layer() + proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException: vollog.warning( - "Unable to access memory of services.exe running with PID: {}".format( - proc.UniqueProcessId - ) + f"Unable to access memory of services.exe running with PID: {proc.UniqueProcessId}" ) continue - layer = context.layers[layer_name] + proc_layer = context.layers[proc_layer_name] exe_range = cls._get_exe_range(proc) if not exe_range: @@ -100,16 +109,15 @@ class SvcList(svcscan.SvcScan): ) continue - for offset in layer.scan( + for offset in proc_layer.scan( context=context, scanner=scanners.BytesScanner(needle=b"Sc27"), sections=exe_range, ): - for record in cls.enumerate_vista_or_later_header( + yield from cls.enumerate_vista_or_later_header( context, service_table_name, service_binary_dll_map, - layer_name, + proc_layer_name, offset, - ): - yield record + ) diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index ca390561f..1bfb98fda 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -4,7 +4,7 @@ import logging import os -from typing import Dict, List, NamedTuple, Optional, Tuple, Union, cast +from typing import Dict, List, NamedTuple, Optional, Tuple, Union, cast, Callable from volatility3.framework import ( constants, @@ -15,31 +15,27 @@ from volatility3.framework import ( symbols, ) from volatility3.framework.configuration import requirements -from volatility3.framework.layers import scanners +from volatility3.framework.layers import scanners, registry from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import versions from volatility3.framework.symbols.windows.extensions import services as services_types -from volatility3.plugins.windows import poolscanner, pslist +from volatility3.plugins.windows import pslist from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) -ServiceBinaryInfo = NamedTuple( - "ServiceBinaryInfo", - [ - ("dll", Union[str, interfaces.renderers.BaseAbsentValue]), - ("binary", Union[str, interfaces.renderers.BaseAbsentValue]), - ], -) +class ServiceBinaryInfo(NamedTuple): + dll: Union[str, interfaces.renderers.BaseAbsentValue] + binary: Union[str, interfaces.renderers.BaseAbsentValue] class SvcScan(interfaces.plugins.PluginInterface): """Scans for windows services.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 1) + _version = (4, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -54,19 +50,22 @@ class SvcScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), ] - @staticmethod + @classmethod def get_record_tuple( + cls, service_record: interfaces.objects.ObjectInterface, binary_info: ServiceBinaryInfo, ): @@ -109,7 +108,7 @@ class SvcScan(interfaces.plugins.PluginInterface): @staticmethod def _create_service_table( context: interfaces.context.ContextInterface, - symbol_table: str, + symbol_table_name: str, config_path: str, ) -> str: """Constructs a symbol table containing the symbols for services @@ -123,15 +122,17 @@ class SvcScan(interfaces.plugins.PluginInterface): Returns: A symbol table containing the symbols necessary for services """ - native_types = context.symbol_space[symbol_table].natives - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) + native_types = context.symbol_space[symbol_table_name].natives + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=symbol_table_name + ) try: symbol_filename = next( filename for version_check, for_64bit, filename in SvcScan._win_version_file_map if is_64bit == for_64bit - and version_check(context=context, symbol_table=symbol_table) + and version_check(context=context, symbol_table=symbol_table_name) ) except StopIteration: raise NotImplementedError("This version of Windows is not supported!") @@ -147,15 +148,14 @@ class SvcScan(interfaces.plugins.PluginInterface): @staticmethod def _get_service_key( - context, config_path: str, layer_name: str, symbol_table: str + 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( config_path, "hivelist" ), - layer_name=layer_name, - symbol_table=symbol_table, + kernel_module_name=kernel_module_name, filter_string="machine\\system", ): # Get ControlSet\Services. @@ -163,12 +163,20 @@ class SvcScan(interfaces.plugins.PluginInterface): return cast( objects.StructType, hive.get_key(r"CurrentControlSet\Services") ) - except (KeyError, exceptions.InvalidAddressException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryException, + ): try: return cast( objects.StructType, hive.get_key(r"ControlSet001\Services") ) - except (KeyError, exceptions.InvalidAddressException): + except ( + KeyError, + exceptions.InvalidAddressException, + registry.RegistryException, + ): vollog.log( constants.LOGLEVEL_VVVV, "Could not retrieve any control set from SYSTEM hive", @@ -273,18 +281,19 @@ class SvcScan(interfaces.plugins.PluginInterface): def service_scan( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, service_table_name: str, service_binary_dll_map, filter_func, ): + kernel = context.modules[kernel_module_name] + relative_tag_offset = context.symbol_space.get_type( service_table_name + constants.BANG + "_SERVICE_RECORD" ).relative_child_offset("Tag") is_vista_or_later = versions.is_vista_or_later( - context=context, symbol_table=symbol_table + context=context, symbol_table=kernel.symbol_table_name ) if is_vista_or_later: @@ -295,9 +304,8 @@ class SvcScan(interfaces.plugins.PluginInterface): seen = [] for task in pslist.PsList.list_processes( - context=context, - layer_name=layer_name, - symbol_table=symbol_table, + context, + kernel_module_name=kernel_module_name, filter_func=filter_func, ): proc_id = "Unknown" @@ -306,13 +314,11 @@ class SvcScan(interfaces.plugins.PluginInterface): proc_layer_name = task.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) continue - layer = context.layers[proc_layer_name] + process_layer = context.layers[proc_layer_name] # get process sections for scanning sections = [] @@ -321,7 +327,7 @@ class SvcScan(interfaces.plugins.PluginInterface): if vad.get_size(): sections.append((base, vad.get_size())) - for offset in layer.scan( + for offset in process_layer.scan( context=context, scanner=scanners.BytesScanner(needle=service_tag), sections=sections, @@ -357,18 +363,20 @@ class SvcScan(interfaces.plugins.PluginInterface): yield service_record @classmethod - def get_prereq_info(cls, context, config_path, layer_name: str, symbol_table: str): + def get_prereq_info( + cls, context, config_path: str, kernel_module_name: str + ) -> Tuple[str, Dict, Callable]: """ Data structures and information needed to analyze service information """ + kernel = context.modules[kernel_module_name] + service_table_name = cls._create_service_table( - context, symbol_table, config_path + context, kernel.symbol_table_name, config_path ) - services_key = cls._get_service_key( - context, config_path, layer_name, symbol_table - ) + services_key = cls._get_service_key(context, config_path, kernel_module_name) service_binary_dll_map = ( cls._get_service_binary_map(services_key) @@ -381,16 +389,13 @@ class SvcScan(interfaces.plugins.PluginInterface): return service_table_name, service_binary_dll_map, filter_func def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - service_table_name, service_binary_dll_map, filter_func = self.get_prereq_info( - self.context, self.config_path, kernel.layer_name, kernel.symbol_table_name + self.context, self.config_path, self.config["kernel"] ) for record in self._enumeration_method( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], service_table_name, service_binary_dll_map, filter_func, diff --git a/volatility3/framework/plugins/windows/symlinkscan.py b/volatility3/framework/plugins/windows/symlinkscan.py index 89fdf142e..cdcb5d3d3 100644 --- a/volatility3/framework/plugins/windows/symlinkscan.py +++ b/volatility3/framework/plugins/windows/symlinkscan.py @@ -17,6 +17,8 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa _required_framework_version = (2, 0, 0) + _version = (2, 0, 0) + @classmethod def get_requirements(cls): return [ @@ -25,14 +27,21 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa description="Windows kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) + ), ] @classmethod def scan_symlinks( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for links using the poolscanner module and constraints. @@ -45,22 +54,20 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa A list of symlink objects found by scanning memory for the Symlink pool signatures """ + kernel = context.modules[kernel_module_name] + constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Sym\xe2", b"Symb"] + kernel.symbol_table_name, [b"Sym\xe2", b"Symb"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, kernel_module_name, constraints ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - for link in self.scan_symlinks( - self.context, kernel.layer_name, kernel.symbol_table_name - ): + for link in self.scan_symlinks(self.context, self.config["kernel"]): try: from_name = link.get_link_name() except (ValueError, exceptions.InvalidAddressException): diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index b812a15ff..1588f292e 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -1,15 +1,17 @@ ## ## plugin for testing addition of threads scan support to poolscanner.py ## -import logging import datetime -from typing import Callable, Iterable +import logging +from typing import Callable, Dict, NamedTuple, Optional, Union, Tuple, Iterator -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements +from volatility3.framework.constants import windows as windows_constants from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import poolscanner +from volatility3.framework.symbols.windows import extensions as win_extensions from volatility3.plugins import timeliner +from volatility3.plugins.windows import pe_symbols, poolscanner vollog = logging.getLogger(__name__) @@ -19,7 +21,18 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (1, 1, 0) + _version = (2, 1, 0) + + class ThreadInfo(NamedTuple): + offset: int + pid: objects.Pointer + tid: objects.Pointer + start_addr: objects.Pointer + start_path: Optional[str] + win32_start_addr: objects.Pointer + win32_start_path: Optional[str] + create_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + exit_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -33,8 +46,16 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), ), ] @@ -43,7 +64,7 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) cls, context: interfaces.context.ContextInterface, module_name: str, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterator[win_extensions.ETHREAD]: """Scans for threads using the poolscanner module and constraints. Args: @@ -54,54 +75,112 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) A list of _ETHREAD objects found by scanning memory for the "Thre" / "Thr\\xE5" pool signatures """ - module = context.modules[module_name] - layer_name = module.layer_name - symbol_table = module.symbol_table_name + kernel = context.modules[module_name] constraints = poolscanner.PoolScanner.builtin_constraints( - symbol_table, [b"Thr\xe5", b"Thre"] + kernel.symbol_table_name, [b"Thr\xe5", b"Thre"] ) for result in poolscanner.PoolScanner.generate_pool_scan( - context, layer_name, symbol_table, constraints + context, module_name, constraints ): _constraint, mem_object, _header = result yield mem_object @classmethod - def gather_thread_info(cls, ethread): + def gather_thread_info( + cls, + ethread: win_extensions.ETHREAD, + vads_cache: Optional[Dict[int, pe_symbols.ranges_type]] = None, + ) -> Optional[ThreadInfo]: try: thread_offset = ethread.vol.offset owner_proc_pid = ethread.Cid.UniqueProcess thread_tid = ethread.Cid.UniqueThread thread_start_addr = ethread.StartAddress + thread_win32start_addr = ethread.Win32StartAddress thread_create_time = ( ethread.get_create_time() ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object thread_exit_time = ( ethread.get_exit_time() ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object + + owner_proc = None + if vads_cache is not None: + owner_proc = ethread.owning_process() except exceptions.InvalidAddressException: - vollog.debug("Thread invalid address {:#x}".format(ethread.vol.offset)) + vollog.debug(f"Thread invalid address {ethread.vol.offset:#x}") return None - return ( - format_hints.Hex(thread_offset), + # Filter junk PIDs + if ( + ethread.Cid.UniqueProcess > windows_constants.MAX_PID + or ethread.Cid.UniqueProcess == 0 + or ethread.Cid.UniqueProcess % 4 != 0 + ): + return None + + # Get VAD mappings for valid non-system (PID 4) processes + if ( + owner_proc + and owner_proc.is_valid() + and owner_proc.UniqueProcessId != 4 + and vads_cache is not None + ): + vads = pe_symbols.PESymbols.get_vads_for_process_cache( + vads_cache, owner_proc + ) + + start_path = ( + pe_symbols.PESymbols.filepath_for_address(vads, thread_start_addr) + if vads + else None + ) + win32start_path = ( + pe_symbols.PESymbols.filepath_for_address(vads, thread_win32start_addr) + if vads + else None + ) + else: + start_path = None + win32start_path = None + + return cls.ThreadInfo( + thread_offset, owner_proc_pid, thread_tid, - format_hints.Hex(thread_start_addr), + thread_start_addr, + start_path, + thread_win32start_addr, + win32start_path, thread_create_time, thread_exit_time, ) - def _generator(self, filter_func: Callable): + def _generator(self, filter_func: Callable) -> Iterator[Tuple[int, Tuple]]: kernel_name = self.config["kernel"] + vads_cache: Dict[int, pe_symbols.ranges_type] = {} + for ethread in self.implementation(self.context, kernel_name): - info = self.gather_thread_info(ethread) + info = self.gather_thread_info(ethread, vads_cache) if info: - yield (0, 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, + ), + ) def generate_timeline(self): filt_func = self.filter_func(self.config) @@ -114,6 +193,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) row_dict["PID"], row_dict["TID"], row_dict["StartAddress"], + row_dict["StartPath"], + row_dict["Win32StartAddress"], + row_dict["Win32StartPath"], row_dict["CreateTime"], row_dict["ExitTime"], ) = row_data @@ -147,6 +229,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) ("PID", int), ("TID", int), ("StartAddress", format_hints.Hex), + ("StartPath", str), + ("Win32StartAddress", format_hints.Hex), + ("Win32StartPath", str), ("CreateTime", datetime.datetime), ("ExitTime", datetime.datetime), ], diff --git a/volatility3/framework/plugins/windows/threads.py b/volatility3/framework/plugins/windows/threads.py index 98a3169a5..d040fa990 100644 --- a/volatility3/framework/plugins/windows/threads.py +++ b/volatility3/framework/plugins/windows/threads.py @@ -3,7 +3,7 @@ # import logging -from typing import Callable, Iterable, List, Generator +from typing import Iterable, List, Generator from volatility3.framework import interfaces, constants from volatility3.framework.configuration import requirements @@ -16,7 +16,7 @@ class Threads(thrdscan.ThrdScan): """Lists process threads""" _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + _version = (3, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -31,20 +31,20 @@ class Threads(thrdscan.ThrdScan): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="thrdscan", plugin=thrdscan.ThrdScan, version=(1, 1, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), ] @classmethod def list_threads( - cls, kernel, proc: interfaces.objects.ObjectInterface + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + proc: interfaces.objects.ObjectInterface, ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Threads of a specific process. @@ -54,6 +54,8 @@ class Threads(thrdscan.ThrdScan): Returns: A list of threads based on the process and filtered based on the filter function """ + kernel = context.modules[kernel_module_name] + seen = set() for thread in proc.ThreadListHead.to_list( f"{kernel.symbol_table_name}{constants.BANG}_ETHREAD", "ThreadListEntry" @@ -67,20 +69,14 @@ class Threads(thrdscan.ThrdScan): def list_process_threads( cls, context: interfaces.context.ContextInterface, - module_name: str, + kernel_module_name: str, ) -> Iterable[interfaces.objects.ObjectInterface]: """Runs through all processes and lists threads for each process""" - module = context.modules[module_name] - layer_name = module.layer_name - symbol_table_name = module.symbol_table_name - filter_func = pslist.PsList.create_pid_filter(context.config.get("pid", None)) for proc in pslist.PsList.list_processes( context=context, - layer_name=layer_name, - symbol_table=symbol_table_name, + kernel_module_name=kernel_module_name, filter_func=filter_func, ): - for thread in cls.list_threads(module, proc): - yield thread + yield from cls.list_threads(context, kernel_module_name, proc) diff --git a/volatility3/framework/plugins/windows/timers.py b/volatility3/framework/plugins/windows/timers.py index d49c28784..c7364d5a5 100644 --- a/volatility3/framework/plugins/windows/timers.py +++ b/volatility3/framework/plugins/windows/timers.py @@ -11,10 +11,11 @@ from volatility3.framework import ( interfaces, constants, symbols, + exceptions, ) from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols.windows import versions +from volatility3.framework.symbols.windows import versions, extensions from volatility3.plugins.windows import ssdt, kpcrs vollog = logging.getLogger(__name__) @@ -24,7 +25,7 @@ class Timers(interfaces.plugins.PluginInterface): """Print kernel timers and associated module DPCs""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -34,11 +35,11 @@ class Timers(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="kpcrs", plugin=kpcrs.KPCRs, version=(1, 0, 0) + requirements.VersionRequirement( + name="kpcrs", component=kpcrs.KPCRs, version=(2, 0, 0) ), ] @@ -47,16 +48,12 @@ class Timers(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - layer_name: str, - symbol_table: str, - ) -> Iterable[Tuple[str, int, str]]: + ) -> Iterable[extensions.KTIMER]: """Lists all kernel timers. Args: context: The context to retrieve required elements (layers, symbol tables) from kernel_module_name: The name of the kernel module on which to operate - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols Yields: A _KTIMER entry @@ -64,19 +61,19 @@ class Timers(interfaces.plugins.PluginInterface): kernel = context.modules[kernel_module_name] if versions.is_windows_7( - context=context, symbol_table=symbol_table - ) or versions.is_windows_8_or_later(context=context, symbol_table=symbol_table): + context=context, symbol_table=kernel.symbol_table_name + ) or versions.is_windows_8_or_later( + context=context, symbol_table=kernel.symbol_table_name + ): # Starting with Windows 7, there is no more KiTimerTableListHead. The list is # at _KPCR.PrcbData.TimerTable.TimerEntries # See http://pastebin.com/FiRsGW3f - for kpcr in kpcrs.KPCRs.list_kpcrs( - context, kernel_module_name, layer_name, symbol_table - ): + for kpcr, _ in kpcrs.KPCRs.list_kpcrs(context, kernel_module_name): if hasattr(kpcr.Prcb.TimerTable, "TableState"): for timer_entries in kpcr.Prcb.TimerTable.TimerEntries: for timer_entry in timer_entries: for timer in timer_entry.Entry.to_list( - symbol_table + constants.BANG + "_KTIMER", + kernel.symbol_table_name + constants.BANG + "_KTIMER", "TimerListEntry", ): yield timer @@ -84,17 +81,19 @@ class Timers(interfaces.plugins.PluginInterface): else: for timer_entries in kpcr.Prcb.TimerTable.TimerEntries: for timer in timer_entries.Entry.to_list( - symbol_table + constants.BANG + "_KTIMER", + kernel.symbol_table_name + constants.BANG + "_KTIMER", "TimerListEntry", ): yield timer elif versions.is_xp_or_2003( - context=context, symbol_table=symbol_table - ) or versions.is_vista_or_later(context=context, symbol_table=symbol_table): - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) + context=context, symbol_table=kernel.symbol_table_name + ) or versions.is_vista_or_later( + context=context, symbol_table=kernel.symbol_table_name + ): + is_64bit = symbols.symbol_table_is_64bit(context, kernel.symbol_table_name) if is_64bit or versions.is_vista_or_later( - context=context, symbol_table=symbol_table + context=context, symbol_table=kernel.symbol_table_name ): # On XP x64, Windows 2003 SP1-SP2, and Vista SP0-SP2, KiTimerTableListHead # is an array of 512 _KTIMER_TABLE_ENTRY structs. @@ -112,7 +111,7 @@ class Timers(interfaces.plugins.PluginInterface): ) for table in timer_table_list_head: for timer in table.to_list( - symbol_table + constants.BANG + "_KTIMER", + kernel.symbol_table_name + constants.BANG + "_KTIMER", "TimerListEntry", ): yield timer @@ -121,19 +120,19 @@ class Timers(interfaces.plugins.PluginInterface): raise NotImplementedError("This version of Windows is not supported!") def _generator(self) -> Iterator[Tuple]: - kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name - symbol_table = kernel.symbol_table_name - collection = ssdt.SSDT.build_module_collection( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, + kernel_module_name=self.config["kernel"], ) + # FIXME - the list_timers API is gross. Fix after GUI merge for timer in self.list_timers( - self.context, self.config["kernel"], layer_name, symbol_table + self.context, + self.config["kernel"], ): if not timer.valid_type(): continue + try: dpc = timer.get_dpc() if dpc == 0: @@ -141,7 +140,10 @@ class Timers(interfaces.plugins.PluginInterface): if dpc.DeferredRoutine == 0: continue deferred_routine = dpc.DeferredRoutine - except Exception as e: + except exceptions.InvalidAddressException as exc: + vollog.debug( + f"Failed to get _KTIMER.Dpc due to {exc.__class__.__name__}" + ) continue module_symbols = list( diff --git a/volatility3/framework/plugins/windows/truecrypt.py b/volatility3/framework/plugins/windows/truecrypt.py index 7fd26cb4e..0478e37a5 100644 --- a/volatility3/framework/plugins/windows/truecrypt.py +++ b/volatility3/framework/plugins/windows/truecrypt.py @@ -2,21 +2,20 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Iterable, Generator, List, Tuple +import logging +from typing import Generator, Iterable, List, Tuple -from volatility3.framework import constants, interfaces, renderers +from volatility3.framework import constants, interfaces, objects, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces.configuration import RequirementInterface -from volatility3.framework.interfaces.objects import ObjectInterface -from volatility3.framework.objects import Bytes, DataFormatInfo, Integer, StructType -from volatility3.framework.objects.templates import ObjectTemplate +from volatility3.framework.interfaces import configuration from volatility3.framework.objects.utility import array_to_string from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe - from volatility3.plugins.windows import modules +vollog = logging.getLogger(__name__) + class Passphrase(interfaces.plugins.PluginInterface): """TrueCrypt Cached Passphrase Finder""" @@ -25,7 +24,7 @@ class Passphrase(interfaces.plugins.PluginInterface): _required_framework_version = (2, 5, 2) @classmethod - def get_requirements(cls) -> List[RequirementInterface]: + def get_requirements(cls) -> List[configuration.RequirementInterface]: return [ requirements.ModuleRequirement( "kernel", @@ -33,7 +32,7 @@ class Passphrase(interfaces.plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(2, 0, 0) + name="modules", component=modules.Modules, version=(3, 0, 0) ), requirements.IntRequirement( name="min-length", @@ -63,7 +62,7 @@ class Passphrase(interfaces.plugins.PluginInterface): layer_name, module_base, ) - data_section: StructType = next( + data_section: objects.StructType = next( sec for sec in dos_header.get_nt_header().get_sections() if array_to_string(sec.Name) == ".data" @@ -72,11 +71,11 @@ class Passphrase(interfaces.plugins.PluginInterface): size: int = data_section.Misc.VirtualSize # Looking at `Length` in TrueCrypt/Common/Password.h::Password struct DWORD_SIZE_BYTES: int = 4 - format = DataFormatInfo( + format = objects.DataFormatInfo( length=DWORD_SIZE_BYTES, byteorder="little", signed=True ) - int32 = ObjectTemplate( - Integer, pe_table_name + constants.BANG + "int", data_format=format + int32 = objects.templates.ObjectTemplate( + objects.Integer, pe_table_name + constants.BANG + "int", data_format=format ) count, not_aligned = divmod(size, DWORD_SIZE_BYTES) if not_aligned: @@ -95,7 +94,7 @@ class Passphrase(interfaces.plugins.PluginInterface): if not min_length <= length <= 64: continue offset = length.vol["offset"] + DWORD_SIZE_BYTES - passphrase: Bytes = self.context.object( + passphrase: objects.Bytes = self.context.object( pe_table_name + constants.BANG + "bytes", layer_name, offset, @@ -107,7 +106,7 @@ class Passphrase(interfaces.plugins.PluginInterface): continue # TrueCrypt/Common/Password.h::Password struct is padded with # 3 zero bytes to keep 64-byte alignment. - buf: Bytes = self.context.object( + buf: objects.Bytes = self.context.object( pe_table_name + constants.BANG + "bytes", layer_name, offset + length + 1, # +1 for '\0'-terminated password string @@ -120,14 +119,21 @@ class Passphrase(interfaces.plugins.PluginInterface): def _generator(self): kernel = self.context.modules[self.config["kernel"]] - mods: Iterable[ObjectInterface] = modules.Modules.list_modules( - self.context, kernel.layer_name, kernel.symbol_table_name - ) - truecrypt_module_base = next( - mod.DllBase - for mod in mods - if mod.BaseDllName.get_string().lower() == "truecrypt.sys" + mods: Iterable[interfaces.objects.ObjectInterface] = ( + modules.Modules.list_modules(self.context, self.config["kernel"]) ) + try: + truecrypt_module_base = next( + mod.DllBase + for mod in mods + if mod.BaseDllName.get_string().lower() == "truecrypt.sys" + ) + except StopIteration: + vollog.warning( + "Truecrypt module not found in the modules list. Unable to proceed." + ) + return + for offset, password in self.scan_module( truecrypt_module_base, kernel.layer_name ): diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 1a1e59940..3827bbe6e 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -1,208 +1,22 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 - -# Full details on the techniques used in these plugins to detect EDR-evading malware -# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation -# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf - +# import logging - -from typing import Dict, Tuple, List, Generator - -from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.plugins.windows import pslist, pe_symbols +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import ( + unhooked_system_calls as unhooked_syscalls, +) vollog = logging.getLogger(__name__) -class unhooked_system_calls(interfaces.plugins.PluginInterface): - """Looks for signs of Skeleton Key malware""" +class unhooked_system_calls( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=unhooked_syscalls.UnhookedSystemCalls, + removal_date="2026-06-07", +): + """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" _required_framework_version = (2, 4, 0) - - system_calls = { - "ntdll.dll": { - pe_symbols.wanted_names_identifier: [ - "NtCreateThread", - "NtProtectVirtualMemory", - "NtReadVirtualMemory", - "NtOpenProcess", - "NtWriteFile", - "NtQueryVirtualMemory", - "NtAllocateVirtualMemory", - "NtWorkerFactoryWorkerReady", - "NtAcceptConnectPort", - "NtAddDriverEntry", - "NtAdjustPrivilegesToken", - "NtAlpcCreatePort", - "NtClose", - "NtCreateFile", - "NtCreateMutant", - "NtOpenFile", - "NtOpenIoCompletion", - "NtOpenJobObject", - "NtOpenKey", - "NtOpenKeyEx", - "NtOpenThread", - "NtOpenThreadToken", - "NtOpenThreadTokenEx", - "NtWriteVirtualMemory", - "NtTraceEvent", - "NtTranslateFilePath", - "NtUmsThreadYield", - "NtUnloadDriver", - "NtUnloadKey", - "NtUnloadKey2", - "NtUnloadKeyEx", - "NtCreateKey", - "NtCreateSection", - "NtDeleteKey", - "NtDeleteValueKey", - "NtDuplicateObject", - "NtQueryValueKey", - "NtReplaceKey", - "NtRequestWaitReplyPort", - "NtRestoreKey", - "NtSetContextThread", - "NtSetSecurityObject", - "NtSetValueKey", - "NtSystemDebugControl", - "NtTerminateProcess", - ] - } - } - - # This data structure is used to track unique implementations of functions across processes - # The outer dictionary holds the module name (e.g., ntdll.dll) - # The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module - # The innermost dictionary holds the unique implementation (bytes) of a function across processes - # Each implementation is tracked along with the process(es) that host it - # For systems without malware, all functions should have the same implementation - # When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations - _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] - - @classmethod - def get_requirements(cls) -> List: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(2, 0, 0) - ), - requirements.PluginRequirement( - name="pe_symbols", plugin=pe_symbols.PESymbols, version=(1, 0, 0) - ), - ] - - def _gather_code_bytes( - self, - kernel: interfaces.context.ModuleInterface, - found_symbols: pe_symbols.found_symbols_type, - ) -> _code_bytes_type: - """ - Enumerates the desired DLLs and function implementations in each process - Groups based on unique implementations of each DLLs' functions - The purpose is to detect when a function has different implementations (code) - in different processes. - This very effectively detects code injection. - """ - code_bytes: unhooked_system_calls._code_bytes_type = {} - - procs = pslist.PsList.list_processes( - context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, - ) - - for proc in procs: - try: - proc_id = proc.UniqueProcessId - proc_name = utility.array_to_string(proc.ImageFileName) - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException: - continue - - for dll_name, functions in found_symbols.items(): - for func_name, func_addr in functions: - try: - fbytes = self.context.layers[proc_layer_name].read( - func_addr, 0x20 - ) - except exceptions.InvalidAddressException: - continue - - # see the definition of _code_bytes_type for details of this data structure - if dll_name not in code_bytes: - code_bytes[dll_name] = {} - - if func_name not in code_bytes[dll_name]: - code_bytes[dll_name][func_name] = {} - - if fbytes not in code_bytes[dll_name][func_name]: - code_bytes[dll_name][func_name][fbytes] = [] - - code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name)) - - return code_bytes - - def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: - kernel = self.context.modules[self.config["kernel"]] - - found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - unhooked_system_calls.system_calls, - ) - - # code_bytes[dll_name][func_name][func_bytes] - code_bytes = self._gather_code_bytes(kernel, found_symbols) - - # walk the functions that were evaluated - for functions in code_bytes.values(): - # cbb is the distinct groups of bytes (instructions) - # for this function across processes - for func_name, cbb in functions.items(): - # the dict key here is the raw instructions, which is not helpful to look at - # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) - cb = list(cbb.values()) - - # if all processes map to the same implementation, then no malware is present - if len(cb) == 1: - yield 0, (func_name, "", len(cb[0])) - else: - # if there are differing implementations then it means - # that malware has overwritten system call(s) in infected processes - # max_idx and small_idx find which implementation of a system call has the least processes - # as all observed malware and open source projects only infected a few targets, leaving the - # rest with the original EDR hooks in place - max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 - small_idx = (~max_idx) & 1 - - ps = [] - - # gather processes on small_idx since these are the malware infected ones - for pid, pname in cb[small_idx]: - ps.append("{:d}:{}".format(pid, pname)) - - proc_names = ", ".join(ps) - - yield 0, (func_name, proc_names, len(cb[max_idx])) - - def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid( - [ - ("Function", str), - ("Distinct Implementations", str), - ("Total Implementations", int), - ], - self._generator(), - ) + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/unloadedmodules.py b/volatility3/framework/plugins/windows/unloadedmodules.py index 01e575818..692f2c4a4 100644 --- a/volatility3/framework/plugins/windows/unloadedmodules.py +++ b/volatility3/framework/plugins/windows/unloadedmodules.py @@ -4,7 +4,7 @@ import logging import datetime -from typing import List, Iterable +from typing import List, Generator, Tuple from volatility3.framework import constants from volatility3.framework import interfaces, symbols, exceptions @@ -14,6 +14,7 @@ from volatility3.framework.interfaces import configuration from volatility3.framework.renderers import format_hints, conversion from volatility3.framework.symbols import intermed from volatility3.plugins import timeliner +from volatility3.plugins.windows import modules vollog = logging.getLogger(__name__) @@ -22,7 +23,7 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt """Lists the unloaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -32,10 +33,19 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt description="Windows kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="timeliner", + component=timeliner.TimeLinerInterface, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), ] - @staticmethod + @classmethod def create_unloadedmodules_table( + cls, context: interfaces.context.ContextInterface, symbol_table: str, config_path: str, @@ -51,7 +61,9 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt The name of the constructed unloaded modules table """ native_types = context.symbol_space[symbol_table].natives - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=symbol_table + ) table_mapping = {"nt_symbols": symbol_table} if is_64bit: @@ -72,10 +84,9 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt def list_unloadedmodules( cls, context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, + kernel_module_name: str, unloadedmodule_table_name: str, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Generator[Tuple[str, int, int, datetime.datetime], None, None]: """Lists all the unloaded modules in the primary layer. Args: @@ -87,15 +98,17 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt A list of Unloaded Modules as retrieved from MmUnloadedDrivers """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + ntkrnlmp = context.modules[kernel_module_name] + unloadedmodules_offset = ntkrnlmp.get_symbol("MmUnloadedDrivers").address unloadedmodules = ntkrnlmp.object( object_type="pointer", offset=unloadedmodules_offset, subtype="array", ) - is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) + is_64bit = symbols.symbol_table_is_64bit( + context=context, symbol_table_name=ntkrnlmp.symbol_table_name + ) if is_64bit: unloaded_count_type = "unsigned long long" @@ -107,43 +120,86 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt object_type=unloaded_count_type, offset=last_unloadedmodule_offset ) + # Bring down to default when smear present. Some samples had this completely broken + if unloaded_count > 1024: + vollog.warning( + f"Smeared array count found {unloaded_count}. Defaulting to 1024 elements." + ) + unloaded_count = 1024 + unloadedmodules_array = context.object( object_type=unloadedmodule_table_name + constants.BANG + "_UNLOADED_DRIVERS", - layer_name=layer_name, + layer_name=ntkrnlmp.layer_name, offset=unloadedmodules, ) unloadedmodules_array.UnloadedDrivers.count = unloaded_count - for mod in unloadedmodules_array.UnloadedDrivers: - yield mod + kernel_space_start = modules.Modules.get_kernel_space_start( + context, kernel_module_name + ) + + address_mask = context.layers[ntkrnlmp.layer_name].address_mask + + for driver in unloadedmodules_array.UnloadedDrivers: + # Mass testing led to dozens of samples backtracing on this plugin when + # accessing members of modules coming out this list + # Given how often temporary drivers load and unload on Win10+, I + # assume the chance for smear is very high + try: + start_address = driver.StartAddress & address_mask + end_address = driver.EndAddress & address_mask + current_time = driver.CurrentTime + driver_name = driver.Name.String + except exceptions.InvalidAddressException: + continue + + if ( + current_time > 1024 + and start_address > kernel_space_start + and start_address & 0xFFF == 0x0 + and end_address & 0xFFF == 0x0 + and end_address > kernel_space_start + ): + yield driver_name, start_address, end_address, current_time def _generator(self): kernel = self.context.modules[self.config["kernel"]] + if not kernel.has_symbol("MmUnloadedDrivers"): + vollog.error( + "The symbol table for this sample is missing the `MmUnloadedDrivers` symbol. Cannot proceed." + ) + return + + if not kernel.has_symbol("MmLastUnloadedDriver"): + vollog.error( + "The symbol table for this sample is missing the `MmLastUnloadededDriver` symbol. Cannot proceed." + ) + return + unloadedmodule_table_name = self.create_unloadedmodules_table( self.context, kernel.symbol_table_name, self.config_path ) - for mod in self.list_unloadedmodules( + for ( + driver_name, + start_address, + end_address, + current_time, + ) in self.list_unloadedmodules( self.context, - kernel.layer_name, - kernel.symbol_table_name, + self.config["kernel"], unloadedmodule_table_name, ): - try: - name = mod.Name.String - except exceptions.InvalidAddressException: - name = renderers.UnreadableValue() - yield ( 0, ( - name, - format_hints.Hex(mod.StartAddress), - format_hints.Hex(mod.EndAddress), - conversion.wintime_to_datetime(mod.CurrentTime), + driver_name, + format_hints.Hex(start_address), + format_hints.Hex(end_address), + conversion.wintime_to_datetime(current_time), ), ) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 2c6ed4daf..22d42505f 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -34,7 +34,7 @@ class VadInfo(interfaces.plugins.PluginInterface): """Lists process memory ranges.""" _required_framework_version = (2, 4, 0) - _version = (2, 0, 0) + _version = (2, 0, 1) MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb def __init__(self, *args, **kwargs): @@ -63,8 +63,8 @@ class VadInfo(interfaces.plugins.PluginInterface): element_type=int, optional=True, ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.BooleanRequirement( name="dump", @@ -99,7 +99,11 @@ class VadInfo(interfaces.plugins.PluginInterface): symbol_table: The name of the table containing the kernel symbols """ - kvo = context.layers[layer_name].config["kernel_virtual_offset"] + kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None) + if not kvo: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) addr = ntkrnlmp.get_symbol("MmProtectToValue").address values = ntkrnlmp.object( @@ -169,9 +173,7 @@ class VadInfo(interfaces.plugins.PluginInterface): proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) return None @@ -271,8 +273,6 @@ class VadInfo(interfaces.plugins.PluginInterface): ) def run(self) -> renderers.TreeGrid: - kernel = self.context.modules[self.config["kernel"]] - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) return renderers.TreeGrid( @@ -293,8 +293,7 @@ class VadInfo(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py new file mode 100644 index 000000000..6a3cc394f --- /dev/null +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -0,0 +1,135 @@ +# 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 +# + +import logging +import re +from typing import List + +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins, configuration +from volatility3.framework.layers import scanners +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + + +class VadRegExScan(plugins.PluginInterface): + """Scans all virtual memory areas for tasks using RegEx.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + MAXSIZE_DEFAULT = 128 + + @classmethod + def get_requirements(cls) -> List[configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + 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", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + requirements.StringRequirement( + name="pattern", description="RegEx pattern", optional=False + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size in bytes for displayed context", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), + ] + + def _generator(self, regex_pattern, procs): + regex_pattern = bytes(regex_pattern, "UTF-8") + 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: + continue + + # get the proc_layer object from the context + proc_layer = self.context.layers[proc_layer_name] + + # get process sections for scanning + sections = [] + for vad in proc.get_vad_root().traverse(): + base = vad.get_start() + if vad.get_size(): + sections.append((base, vad.get_size())) + + for offset in proc_layer.scan( + context=self.context, + scanner=scanners.RegExScanner(regex_pattern), + sections=sections, + progress_callback=self._progress_callback, + ): + result_data = proc_layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) + + # reapply the regex in order to extract just the match + regex_result = re.match(regex_pattern, result_data) + + if regex_result: + # the match is within the results_data (e.g. it fits within MAXSIZE_DEFAULT) + # extract just the match itself + regex_match = regex_result.group(0) + text_result = str(regex_match, encoding="UTF-8", errors="replace") + bytes_result = regex_match + else: + # the match is not with the results_data (e.g. it doesn't fit within MAXSIZE_DEFAULT) + text_result = str(result_data, encoding="UTF-8", errors="replace") + bytes_result = result_data + + proc_id = proc.UniqueProcessId + process_name = proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ) + yield ( + 0, + ( + proc_id, + process_name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + procs = pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("Text", str), + ("Hex", bytes), + ], + self._generator(self.config.get("pattern"), procs), + ) diff --git a/volatility3/framework/plugins/windows/vadwalk.py b/volatility3/framework/plugins/windows/vadwalk.py index 930388b3a..38b5d197e 100644 --- a/volatility3/framework/plugins/windows/vadwalk.py +++ b/volatility3/framework/plugins/windows/vadwalk.py @@ -28,11 +28,11 @@ class VadWalk(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="vadinfo", plugin=vadinfo.VadInfo, version=(2, 0, 0) + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -67,7 +67,6 @@ class VadWalk(interfaces.plugins.PluginInterface): ) def run(self) -> renderers.TreeGrid: - kernel = self.context.modules[self.config["kernel"]] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) return renderers.TreeGrid( @@ -85,8 +84,7 @@ class VadWalk(interfaces.plugins.PluginInterface): self._generator( pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ) ), diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index efcc70d07..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 @@ -17,8 +18,8 @@ vollog = logging.getLogger(__name__) class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" - _required_framework_version = (2, 4, 0) - _version = (1, 1, 1) + _required_framework_version = (2, 22, 0) + _version = (1, 1, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -29,11 +30,14 @@ class VadYaraScan(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -50,8 +54,6 @@ class VadYaraScan(interfaces.plugins.PluginInterface): return yarascan_requirements + vadyarascan_requirements def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - rules = yarascan.YaraScan.process_yara_options(dict(self.config)) filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) @@ -60,58 +62,67 @@ class VadYaraScan(interfaces.plugins.PluginInterface): for task in pslist.PsList.list_processes( context=self.context, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], filter_func=filter_func, ): layer_name = task.add_process_layer() layer = self.context.layers[layer_name] + + max_vad_size = 0 + vad_maps_to_scan = [] + for start, size in self.get_vad_maps(task): if size > sanity_check: vollog.debug( f"VAD at 0x{start:x} over sanity-check size, not scanning" ) continue + max_vad_size = max(max_vad_size, size) + vad_maps_to_scan.append((start, size)) - data = layer.read(start, size, True) - if not yarascan.YaraScan._yara_x: - for match in rules.match(data=data): - if yarascan.YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield 0, ( - format_hints.Hex(offset + start), - task.UniqueProcessId, - match.rule, - name, - value, - ) - else: - for match in rules.scan(data).matching_rules: - for match_string in match.patterns: - for instance in match_string.matches: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - f"{match.namespace}.{match.identifier}", - match_string.identifier, - data[ - instance.offset : instance.offset - + instance.length - ], - ) + if not vad_maps_to_scan: + vollog.warning( + f"No VADs were found for task {task.UniqueProcessId}, not scanning" + ) + continue - @staticmethod + scanner = yarascan.YaraScanner(rules=rules) + scanner.chunk_size = max_vad_size + + # scan the VAD data (in one contiguous block) with the yarascanner + for start, size in vad_maps_to_scan: + for offset, rule_name, name, value in scanner( + layer.read(start, size, pad=True), start + ): + layer_data = renderers.LayerData( + context=self.context, + offset=offset, + layer_name=layer.name, + length=len(value), + ) + 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 def get_vad_maps( + cls, task: interfaces.objects.ObjectInterface, ) -> Iterable[Tuple[int, int]]: """Creates a map of start/end addresses within a virtual address @@ -132,9 +143,14 @@ 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", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 4a06ed0c9..d2722418b 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -42,11 +42,16 @@ class VerInfo(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) ), - requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(2, 0, 0) + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="page_start_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), ), requirements.BooleanRequirement( name="extensive", @@ -176,7 +181,12 @@ class VerInfo(interfaces.plugins.PluginInterface): (major, minor, product, build) = self.get_version_information( self._context, pe_table_name, session_layer_name, mod.DllBase ) - except (exceptions.InvalidAddressException, TypeError, AttributeError): + except ( + exceptions.InvalidAddressException, + ValueError, + TypeError, + AttributeError, + ): (major, minor, product, build) = [renderers.UnreadableValue()] * 4 if ( not isinstance(BaseDllName, renderers.UnreadableValue) @@ -212,9 +222,7 @@ class VerInfo(interfaces.plugins.PluginInterface): proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException as excp: vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name - ) + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" ) continue @@ -255,19 +263,15 @@ class VerInfo(interfaces.plugins.PluginInterface): ) def run(self): - kernel = self.context.modules[self.config["kernel"]] - procs = pslist.PsList.list_processes( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, kernel_module_name=self.config["kernel"] ) - mods = modules.Modules.list_modules( - self.context, kernel.layer_name, kernel.symbol_table_name - ) + mods = modules.Modules.list_modules(self.context, self.config["kernel"]) # populate the session layers for kernel modules session_layers = modules.Modules.get_session_layers( - self.context, kernel.layer_name, kernel.symbol_table_name + context=self.context, kernel_module_name=self.config["kernel"] ) return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/windows/virtmap.py b/volatility3/framework/plugins/windows/virtmap.py index 3f3f270e2..f37d5790a 100644 --- a/volatility3/framework/plugins/windows/virtmap.py +++ b/volatility3/framework/plugins/windows/virtmap.py @@ -17,6 +17,7 @@ class VirtMap(interfaces.plugins.PluginInterface): """Lists virtual mapped sections.""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -138,8 +139,7 @@ class VirtMap(interfaces.plugins.PluginInterface): mapping = cls.determine_map(module) for entry in mapping: if "Unused" not in entry: - for value in mapping[entry]: - yield value + yield from mapping[entry] def run(self): kernel = self.context.modules[self.config["kernel"]] @@ -148,7 +148,7 @@ class VirtMap(interfaces.plugins.PluginInterface): module = self.context.module( kernel.symbol_table_name, layer_name=layer.name, - offset=layer.config["kernel_virtual_offset"], + offset=kernel.offset, ) return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/windows/windows.py b/volatility3/framework/plugins/windows/windows.py new file mode 100644 index 000000000..c98869665 --- /dev/null +++ b/volatility3/framework/plugins/windows/windows.py @@ -0,0 +1,142 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Iterable + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.objects import utility +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import windowstations + +vollog = logging.getLogger(__name__) + + +class Windows(interfaces.plugins.PluginInterface): + """Enumerates the Windows of Desktop instances""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="windowstations", + component=windowstations.WindowStations, + version=(1, 0, 0), + ), + ] + + @classmethod + def list_windows( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """ + Enumerates the desktops of each window station + For each found, enumerates its windows within the desktop + """ + kernel = context.modules[kernel_module_name] + + for ( + winsta, + station_name, + session_id, + ) in windowstations.WindowStations.scan_window_stations( + context, config_path, kernel_module_name + ): + # for each window station, walk its list of desktops + for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name): + try: + top_window = desktop.pDeskInfo.spwnd + except exceptions.InvalidAddressException: + vollog.debug( + f"Desktop with name {desktop_name} in window station {station_name} has a broken window pointer." + ) + continue + + for window, window_name in desktop.windows(top_window): + yield station_name, desktop_name, window, window_name + + def _generator(self): + kernel_name = self.config["kernel"] + + # call the implementation for finding windows and gather attributes + for station_name, desktop_name, window, window_name in self.list_windows( + self.context, self.config_path, kernel_name + ): + # We need a valid process and session id for the window to display it + process = window.get_process() + process_name = None + if process: + try: + process_name = utility.array_to_string(process.ImageFileName) + process_pid = process.UniqueProcessId + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read name and pid of the process for window {window.vol.offset:#x}" + ) + + if process_name is None: + vollog.debug( + f"Invalid process reference for the process hosting window {window.vol.offset:#x}" + ) + continue + + sess_id = window.get_session_id() + if sess_id is None: + vollog.debug( + f"Unable to read session id of the process for window {window.vol.offset:#x} in process {process_name}" + ) + continue + + # procedures can be empty, but if set, should be a valid pointer + window_proc = window.get_window_procedure() + if window_proc is None: + window_proc = renderers.NotAvailableValue() + elif window_proc == 0 or window_proc > 0x1000: + window_proc = format_hints.Hex(window_proc) + else: + vollog.debug( + f"Invalid window procedure {window_proc} for the window {window.vol.offset:#x}" + ) + 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, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Station", str), + ("Session", int), + ("Desktop", str), + ("Window", str), + ("Procedure", format_hints.Hex), + ("Process", str), + ("PID", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/windowstations.py b/volatility3/framework/plugins/windows/windowstations.py new file mode 100644 index 000000000..1f95b0531 --- /dev/null +++ b/volatility3/framework/plugins/windows/windowstations.py @@ -0,0 +1,246 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +import os +from typing import List, Tuple, Iterator, Generator, Dict + +from volatility3.framework import interfaces, renderers, symbols, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows import versions +from volatility3.plugins.windows import poolscanner, modules +from volatility3.framework.symbols.windows.extensions import gui + +vollog = logging.getLogger(__name__) + + +class WindowStations(interfaces.plugins.PluginInterface): + """Scans for top level Windows Stations""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + # These checks must be completed from newest -> oldest OS version. + _win_version_file_map: List[Tuple[versions.OsDistinguisher, str]] = [ + (versions.is_win10_19577_or_later, "gui-win10-19577-x64"), + (versions.is_win10_19041_or_later, "gui-win10-19041-x64"), + (versions.is_win10_18362_or_later, "gui-win10-18362-x64"), + (versions.is_win10_17763_or_later, "gui-win10-17763-x64"), + (versions.is_win10_17134_or_later, "gui-win10-17134-x64"), + (versions.is_win10_16299_or_later, "gui-win10-16299-x64"), + (versions.is_win10_15063_or_later, "gui-win10-15063-x64"), + (versions.is_win10_10586_or_later, "gui-win10-10586-x64"), + (versions.is_windows_8_or_later, "gui-win8-x64"), + (versions.is_windows_7_sp1, "gui-win7sp1-x64"), + (versions.is_windows_7_sp0, "gui-win7sp0-x64"), + ] + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="GUIExtensions", component=gui.GUIExtensions, version=(1, 0, 0) + ), + ] + + @staticmethod + def create_gui_table( + context: interfaces.context.ContextInterface, + symbol_table: str, + config_path: str, + ) -> str: + """Creates a symbol table for windows GUI types + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + symbol_table: The name of an existing symbol table containing the kernel symbols + config_path: The configuration path within the context of the symbol table to create + + Returns: + The name of the constructed GUI table + """ + + if not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=symbol_table + ): + raise NotImplementedError( + "This plugin only supports x64 versions of Windows" + ) + + table_mapping = {"nt_symbols": symbol_table} + + try: + symbol_filename = next( + filename + for version_check, filename in WindowStations._win_version_file_map + if version_check(context=context, symbol_table=symbol_table) + ) + except StopIteration: + raise NotImplementedError("This version of Windows is not supported!") + + vollog.debug(f"Using GUI table {symbol_filename}") + + return intermed.IntermediateSymbolTable.create( + context=context, + config_path=config_path, + sub_path=os.path.join("windows", "gui"), + filename=symbol_filename, + class_types=gui.GUIExtensions.class_types, + table_mapping=table_mapping, + ) + + @classmethod + def get_session_map( + cls, + context: interfaces.context.ContextInterface, + module_name: str, + gui_table_name: str, + ) -> Dict[int, interfaces.context.ModuleInterface]: + """ + Walks each session layer and returns a dictionary that + maps session identifiers to a module in the session's layer + """ + session_map = modules.Modules.get_session_layers_map(context, module_name) + + for session_id, session_layer in session_map.items(): + session_module = context.module( + gui_table_name, + layer_name=session_layer, + offset=context.modules[module_name].offset, + ) + session_map[session_id] = session_module + + return session_map + + @classmethod + def scan_gui_object( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + object_tag: bytes, + object_type: str, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """ + An API that generically scans for GUI (win32*.sys) objects allocated in the pools (which is nearly all of them) + + This function scans within the kernel space for the tags and then uses `get_session_map` to instantiate objects + in their correct session address space. + + Args: + context: + config_path: + kernel_module_name: + object_tag: The 4 byte pool header tag to search for + object_type: The data structure of the GUI object within the pool + """ + + kernel = context.modules[kernel_module_name] + + gui_table_name = cls.create_gui_table( + context, kernel.symbol_table_name, config_path + ) + + constraints = poolscanner.PoolScanner.gui_poolscanner_constraints( + gui_table_name, [object_tag] + ) + + session_map = cls.get_session_map(context, kernel_module_name, gui_table_name) + + for result in poolscanner.PoolScanner.generate_pool_scan_extended( + context=context, + kernel_module_name=kernel_module_name, + object_symbol_table_name=gui_table_name, + constraints=constraints, + ): + _constraint, mem_object, _header = result + + # enforce that objects are in a valid session + # this prevents smear and also ensures future pointer + # dereferences are performed in the correct address space (layer) + try: + session_id = mem_object.get_session_id() + except exceptions.InvalidAddressException: + continue + + if session_id is not None: + session_module = session_map.get(session_id, None) + if session_module: + # create the object its own address space (per-session) + yield session_module.object( + object_type=object_type, + offset=mem_object.vol.offset, + absolute=True, + ) + + @classmethod + def scan_window_stations( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_module_name: str, + ) -> Iterator[Tuple["gui.tagWINDOWSTATION", str, int]]: + """ + Scans for window stations through `scan_gui_object` + Yields each window station along with its name and session_id + """ + + seen = set() + + kernel = context.modules[kernel_module_name] + + for scanned_winsta in cls.scan_gui_object( + context, config_path, kernel_module_name, b"Wind", "tagWINDOWSTATION" + ): + # walk the list of each station found through scanning + for winsta in scanned_winsta.traverse(): + if winsta.vol.offset in seen: + continue + seen.add(winsta.vol.offset) + + # stations need to have a name and be in a session + name, session_id = winsta.get_info(kernel.symbol_table_name) + if name and session_id is not None: + yield winsta, name, session_id + + def _generator(self): + """ + A wrapper around `scan_window_stations` + """ + for winsta, name, session_id in self.scan_window_stations( + self.context, self.config_path, self.config["kernel"] + ): + yield ( + 0, + ( + format_hints.Hex(winsta.vol.offset), + name, + session_id, + ), + ) + + # Volatility 2 reported whether the station is interactive or not, but I could not determine if its algorithm + # is currently valid. I also did not see where the old code paths still checked the same bit mask + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Name", str), + ("SessionId", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 310bbd072..910ded109 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -31,13 +31,13 @@ except ImportError: except ImportError: vollog.info( - "Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available" + "Neither yara-x nor yara-python (>3.8.0) module was found, plugin (and dependent plugins) not available" ) raise class YaraScanner(interfaces.layers.ScannerInterface): - _version = (2, 1, 0) + _version = (2, 1, 1) # yara.Rules isn't exposed, so we can't type this properly def __init__(self, rules) -> None: @@ -79,23 +79,23 @@ class YaraScanner(interfaces.layers.ScannerInterface): for offset, name, value in match.strings: yield (offset + data_offset, match.rule, name, value) - @staticmethod - def get_rule(rule): + @classmethod + def get_rule(cls, rule): if USE_YARA_X: return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") return yara.compile( sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} ) - @staticmethod - def from_compiled_file(filepath): + @classmethod + def from_compiled_file(cls, filepath): with resources.ResourceAccessor().open(filepath, "rb") as fp: if USE_YARA_X: return yara_x.Rules.deserialize_from(file=fp) return yara.load(file=fp) - @staticmethod - def from_file(filepath): + @classmethod + def from_file(cls, filepath): with resources.ResourceAccessor().open(filepath, "rb") as fp: if USE_YARA_X: return yara_x.compile(fp.read().decode()) @@ -105,8 +105,8 @@ class YaraScanner(interfaces.layers.ScannerInterface): class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" - _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _required_framework_version = (2, 22, 0) + _version = (2, 0, 1) _yara_x = USE_YARA_X @classmethod @@ -118,7 +118,12 @@ class YaraScan(plugins.PluginInterface): name="primary", description="Memory layer for the kernel", architectures=["Intel32", "Intel64"], - ) + ), + requirements.VersionRequirement( + name="yarascanner", + component=YaraScanner, + version=(2, 1, 1), + ), ] @classmethod @@ -177,7 +182,7 @@ class YaraScan(plugins.PluginInterface): rule = config["yara_string"] if rule[0] not in ["{", "/"]: rule = f'"{rule}"' - if config.get("case", False): + if config.get("insensitive", False): rule += " nocase" if config.get("wide", False): rule += " wide ascii" @@ -201,7 +206,13 @@ class YaraScan(plugins.PluginInterface): for offset, rule_name, name, value in layer.scan( context=self.context, scanner=YaraScanner(rules=rules) ): - yield 0, (format_hints.Hex(offset), rule_name, name, value) + layer_data = renderers.LayerData( + context=self.context, + offset=offset, + layer_name=layer.name, + length=len(value), + ) + yield 0, (format_hints.Hex(offset), rule_name, name, layer_data) def run(self): return renderers.TreeGrid( @@ -209,7 +220,7 @@ class YaraScan(plugins.PluginInterface): ("Offset", format_hints.Hex), ("Rule", str), ("Component", str), - ("Value", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 43bb59a21..899c19196 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -6,8 +6,10 @@ Renderers display the unified output format in some manner (be it text or file or graphical output """ + import collections import collections.abc +import dataclasses import datetime import logging from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, Union @@ -22,16 +24,28 @@ class UnreadableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because the data cannot be read.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + class UnparsableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because the data cannot be interpreted correctly.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + class NotApplicableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because they don't make sense for this node.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "N/A" + class NotAvailableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which cannot be provided now (but might in @@ -45,6 +59,70 @@ class NotAvailableValue(interfaces.renderers.BaseAbsentValue): in preference, and only if neither fits should this be used. """ + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "N/A" + + +########## +### Basic Types + + +class Disassembly(interfaces.renderers.BasicType): + """A class to indicate that the bytes provided should be disassembled + (based on the architecture)""" + + possible_architectures = ["intel", "intel64", "arm", "arm64"] + + def __init__( + self, data: bytes, offset: int = 0, architecture: str = "intel64" + ) -> None: + self.data = data + self.architecture = None + if architecture in self.possible_architectures: + self.architecture = architecture + if not isinstance(offset, int): + raise TypeError("Offset must be an integer type") + self.offset = offset + + def __str__(self) -> str: + """Fallback method of rendering""" + return str(self.data) + + +@dataclasses.dataclass +class LayerData(interfaces.renderers.BasicType): + """Layer data + + This requires the context to be passed in, in case plugins want to use multiple contexts + and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins + """ + + context: "interfaces.context.ContextInterface" + layer_name: str + offset: int + length: int + no_surrounding: bool = False + + @staticmethod + def from_object( + object: "interfaces.objects.ObjectInterface", + size: Optional[int] = None, + no_surrounding: bool = True, + ): + return LayerData( + context=object._context, + layer_name=object.vol.layer_name, + offset=object.vol.offset, + length=size or object.vol.size, + no_surrounding=no_surrounding, + ) + + def __str__(self) -> str: + """Fallback method of rendering""" + data = self.context.layers[self.layer_name].read(self.offset, self.length, True) + return str(data) + class TreeNode(interfaces.renderers.TreeNode): """Class representing a particular node in a tree grid.""" @@ -83,14 +161,11 @@ class TreeNode(interfaces.renderers.TreeNode): raise TypeError( "Values must be a list of objects made up of simple types and number the same as the columns" ) - for index in range(len(self._treegrid.columns)): - column = self._treegrid.columns[index] + for index, column in enumerate(self._treegrid.columns): val = values[index] if not isinstance(val, (column.type, interfaces.renderers.BaseAbsentValue)): raise TypeError( - "Values item with index {} is the wrong type for column {} (got {} but expected {})".format( - index, column.name, type(val), column.type - ) + f"Values item with index {index} is the wrong type for column {column.name} (got {type(val)} but expected {column.type})" ) # TODO: Consider how to deal with timezone naive/aware datetimes (and alert plugin uses to be precise) # if isinstance(val, datetime.datetime): @@ -189,9 +264,7 @@ class TreeGrid(interfaces.renderers.TreeGrid): is_simple_type = issubclass(column_type, self.base_types) if not is_simple_type: raise TypeError( - "Column {}'s type is not a simple type: {}".format( - name, column_type.__class__.__name__ - ) + f"Column {name}'s type is not a simple type: {column_type.__class__.__name__}" ) converted_columns.append(interfaces.renderers.Column(name, column_type)) self.RowStructure = RowStructureConstructor( @@ -218,7 +291,7 @@ class TreeGrid(interfaces.renderers.TreeGrid): def populate( self, - function: interfaces.renderers.VisitorSignature = None, + function: Optional[interfaces.renderers.VisitorSignature] = None, initial_accumulator: Any = None, fail_on_errors: bool = True, ) -> Optional[Exception]: @@ -417,8 +490,7 @@ class ColumnSortKey(interfaces.renderers.ColumnSortKey): _index = None self._type = None self.ascending = ascending - for i in range(len(treegrid.columns)): - column = treegrid.columns[i] + for i, column in enumerate(treegrid.columns): if column.name.lower() == column_name.lower(): _index = i self._type = column.type @@ -434,10 +506,10 @@ class ColumnSortKey(interfaces.renderers.ColumnSortKey): value = datetime.datetime.min elif self._type in [int, float]: value = -1 - elif self._type == bool: + elif self._type is bool: value = False elif self._type in [str, renderers.Disassembly]: value = "-" - elif self._type == bytes: + elif self._type is bytes: value = b"" return value diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index e48684b31..f848b2dad 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -18,7 +18,7 @@ def wintime_to_datetime( unix_time = wintime // 10000000 if unix_time == 0: return renderers.NotApplicableValue() - unix_time = unix_time - 11644473600 + unix_time -= 11644473600 try: return datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc) # Windows sometimes throws OSErrors rather than ValueError/OverflowError when it can't convert a value @@ -71,7 +71,7 @@ def round(addr: int, align: int, up: bool = False) -> int: Args: addr: the address align: the alignment value - up: Whether to round up or not + up: whether to round up or not Returns: The aligned address @@ -122,11 +122,12 @@ def convert_port(port_as_integer): def convert_network_four_tuple(family, four_tuple): - """Converts the connection four_tuple: (source ip, source port, dest ip, - dest port) + """Converts the connection four_tuple: + + (source ip, source port, dest ip, dest port) into their string equivalents. IP addresses are expected as a tuple - of unsigned shorts Ports are converted to proper endianness as well + of unsigned shorts. Ports are converted to proper endianness as well. """ if family == socket.AF_INET: diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index 194e38099..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 @@ -70,15 +71,21 @@ class MultiTypeData(bytes): ) -BinOrAbsent = lambda x: ( - Bin(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x -) -HexOrAbsent = lambda x: ( - Hex(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x -) -HexBytesOrAbsent = lambda x: ( - HexBytes(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x -) -MultiTypeDataOrAbsent = lambda x: ( - MultiTypeData(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x -) +def BinOrAbsent(x): + return Bin(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x + + +def HexOrAbsent(x): + return Hex(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x + + +def HexBytesOrAbsent(x): + return HexBytes(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x + + +def MultiTypeDataOrAbsent(x): + return ( + MultiTypeData(x) + if not isinstance(x, interfaces.renderers.BaseAbsentValue) + else x + ) diff --git a/volatility3/framework/symbols/__init__.py b/volatility3/framework/symbols/__init__.py index a8753bd4d..a050298ba 100644 --- a/volatility3/framework/symbols/__init__.py +++ b/volatility3/framework/symbols/__init__.py @@ -53,10 +53,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): self._resolved: Dict[str, interfaces.objects.Template] = {} self._resolved_symbols: Dict[str, interfaces.objects.Template] = {} - def clear_symbol_cache(self, table_name: str = None) -> None: + def clear_symbol_cache(self, table_name: Optional[str] = None) -> None: """Clears the symbol cache for the specified table name. If no table name is specified, the caches of all symbol tables are cleared.""" - table_list: List[interfaces.symbols.BaseSymbolTableInterface] = list() + table_list: List[interfaces.symbols.BaseSymbolTableInterface] = [] if table_name is None: table_list = list(self._dict.values()) else: @@ -81,7 +81,7 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): yield table + constants.BANG + symbol_name def get_symbols_by_location( - self, offset: int, size: int = 0, table_name: str = None + self, offset: int, size: int = 0, table_name: Optional[str] = None ) -> Iterable[str]: """Returns all symbols that exist at a specific relative address.""" table_list: Iterable[interfaces.symbols.BaseSymbolTableInterface] = ( @@ -128,7 +128,7 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): self, producer: str, validator: Callable[[Optional[Tuple], Optional[datetime.datetime]], bool], - tables: List[str] = None, + tables: Optional[List[str]] = None, ) -> bool: """Verifies the producer metadata and version of tables @@ -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/generic/__init__.py b/volatility3/framework/symbols/generic/__init__.py index 9d6da5aa4..7dd00fa75 100644 --- a/volatility3/framework/symbols/generic/__init__.py +++ b/volatility3/framework/symbols/generic/__init__.py @@ -4,7 +4,7 @@ import random import string -from typing import Union +from typing import Optional, Union from volatility3.framework import objects, interfaces @@ -14,8 +14,8 @@ class GenericIntelProcess(objects.StructType): self, context: interfaces.context.ContextInterface, dtb: Union[int, interfaces.objects.ObjectInterface], - config_prefix: str = None, - preferred_name: str = None, + config_prefix: Optional[str] = None, + preferred_name: Optional[str] = None, ) -> str: """Constructs a new layer based on the process's DirectoryTableBase.""" diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 751f88e39..001f817bd 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -86,7 +86,7 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): config_path: str, name: str, isf_url: str, - native_types: interfaces.symbols.NativeTableInterface = None, + native_types: Optional[interfaces.symbols.NativeTableInterface] = None, table_mapping: Optional[Dict[str, str]] = None, validate: bool = True, class_types: Optional[ @@ -101,7 +101,7 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): Args: context: The volatility context for the symbol table config_path: The configuration path for the symbol table - name: The name for the symbol table (this is used in symbols e.g. table!symbol ) + name: The name for the symbol table (this is used in symbols e.g. table!symbol) isf_url: The URL pointing to the ISF file location native_types: The NativeSymbolTable that contains the native types for this symbol table table_mapping: A dictionary linking names referenced in the file with symbol tables in the context @@ -111,7 +111,7 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): """ # Check there are no obvious errors # Open the file and test the version - self._versions = dict([(x.version, x) for x in class_subclasses(ISFormatTable)]) + self._versions = dict((x.version, x) for x in class_subclasses(ISFormatTable)) with resources.ResourceAccessor().open(isf_url) as fp: reader = codecs.getreader("utf-8") json_object = json.load(reader(fp)) # type: ignore @@ -166,12 +166,12 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): format. An interface version such as Major.Minor.Patch means that Major - of the provider must be equal to that of the consumer, and the + of the provider must be equal to that of the consumer, and the provider (the JSON in this instance) must have a greater minor - (indicating that only additive changes have been made) than + (indicating that only additive changes have been made) than the consumer (in this case, the file reader). """ - major, minor, patch = [int(x) for x in version.split(".")] + major, minor, patch = (int(x) for x in version.split(".")) supported_versions = [x for x in versions if x[0] == major and x[1] >= minor] if not supported_versions: raise ValueError( @@ -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( @@ -319,7 +322,7 @@ class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass=ABCMeta): config_path: str, name: str, json_object: Any, - native_types: interfaces.symbols.NativeTableInterface = None, + native_types: Optional[interfaces.symbols.NativeTableInterface] = None, table_mapping: Optional[Dict[str, str]] = None, ) -> None: self._json_object = json_object @@ -411,18 +414,27 @@ class Version1Format(ISFormatTable): @property def symbols(self) -> Iterable[str]: - """Returns an iterator of the symbol names.""" - return list(self._json_object.get("symbols", {})) + """Returns an iterable (KeysView) of the available symbol names.""" + return self._json_object.get("symbols", {}).keys() @property - def enumerations(self) -> Iterable[str]: - """Returns an iterator of the available enumerations.""" - return list(self._json_object.get("enums", {})) + def enumerations(self) -> Iterable[Any]: + """Returns an iterable (KeysView) of the available enumerations.""" + return self._json_object.get("enums", {}).keys() @property def types(self) -> Iterable[str]: - """Returns an iterator of the symbol type names.""" - return list(self._json_object.get("user_types", {})) + list(self.natives.types) + """Returns an iterable (KeysView) of the available symbol type names.""" + # We use ** instead of + # `set(self._json_object.get("user_types", {}).keys()).union(self.natives.types)` + # because converting user_types dict to a set is costly. + # It is more efficient to convert the (very small) self.natives.types set to a dict. + # FIXME: On Python3.8 support drop, merge the two dicts using the merge operator: + # (self._json_object.get("user_types", {}) | dict.fromkeys(self.natives.types)).keys() + return { + **self._json_object.get("user_types", {}), + **dict.fromkeys(self.natives.types), + }.keys() def get_type_class(self, name: str) -> Type[interfaces.objects.ObjectInterface]: return self._overrides.get(name, objects.AggregateType) @@ -738,10 +750,17 @@ class Version6Format(Version5Format): @property def metadata(self) -> Optional[interfaces.symbols.MetadataInterface]: """Returns a MetadataInterface object.""" - if self._json_object.get("metadata", {}).get("windows"): - return metadata.WindowsMetadata(self._json_object["metadata"]["windows"]) - if self._json_object.get("metadata", {}).get("linux"): - return metadata.LinuxMetadata(self._json_object["metadata"]["linux"]) + if "metadata" not in self._json_object: + return None + + json_metadata = self._json_object["metadata"] + if "windows" in json_metadata: + return metadata.WindowsMetadata(json_metadata["windows"]) + if "linux" in json_metadata: + return metadata.LinuxMetadata(json_metadata["linux"]) + if "mac" in json_metadata: + return metadata.MacMetadata(json_metadata["mac"]) + return None @@ -798,7 +817,12 @@ class Version8Format(Version7Format): type_definition = self._json_object["user_types"].get(type_name) if type_definition is None: # Fall back to the natives table - return self.natives.get_type(self.name + constants.BANG + type_name) + if type_name in self.natives.types: + return self.natives.get_type(self.name + constants.BANG + type_name) + else: + raise exceptions.SymbolError( + type_name, self.name, f"Unknown symbol: {type_name}" + ) members = self._process_fields(type_definition["fields"]) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 3289775b6..7560da3ec 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,16 +1,31 @@ # 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 math -import contextlib -from abc import ABC, abstractmethod -from typing import Iterator, List, Tuple, Optional, Union +import math +import string +import contextlib +import functools +import logging +from abc import ABC, abstractmethod +from typing import List, Tuple, Optional, Union, Dict, Generator, Iterator + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework -from volatility3.framework import constants, exceptions, interfaces, objects +from volatility3.framework import ( + constants, + exceptions, + deprecation, + interfaces, + objects, +) from volatility3.framework.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions +from volatility3.framework.layers import scanners +from volatility3.framework.constants import linux as linux_constants + +vollog = logging.getLogger(__name__) class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): @@ -37,12 +52,16 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("idr", extensions.IDR) self.set_type_class("address_space", extensions.address_space) self.set_type_class("page", extensions.page) + # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) self.optional_set_type_class("bpf_prog_aux", extensions.bpf_prog_aux) self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) + self.optional_set_type_class("scatterlist", extensions.scatterlist) + self.optional_set_type_class("module_sect_attr", extensions.module_sect_attr) + self.optional_set_type_class("bin_attribute", extensions.bin_attribute) # kernels >= 4.18 self.optional_set_type_class("timespec64", extensions.timespec64) @@ -57,27 +76,34 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.optional_set_type_class("rb_root", extensions.rb_root) # Network - self.set_type_class("net", extensions.net) - self.set_type_class("socket", extensions.socket) - self.set_type_class("sock", extensions.sock) - self.set_type_class("inet_sock", extensions.inet_sock) - self.set_type_class("unix_sock", extensions.unix_sock) + # FIXME: Deprecate all of this once the framework hits version 3 + self.set_type_class("net", extensions.network.net) + self.set_type_class("socket", extensions.network.socket) + self.set_type_class("sock", extensions.network.sock) + self.set_type_class("inet_sock", extensions.network.inet_sock) + self.set_type_class("unix_sock", extensions.network.unix_sock) + # Might not exist in older kernels or the current symbols - self.optional_set_type_class("netlink_sock", extensions.netlink_sock) - self.optional_set_type_class("vsock_sock", extensions.vsock_sock) - self.optional_set_type_class("packet_sock", extensions.packet_sock) - self.optional_set_type_class("bt_sock", extensions.bt_sock) - self.optional_set_type_class("xdp_sock", extensions.xdp_sock) + self.optional_set_type_class("netlink_sock", extensions.network.netlink_sock) + self.optional_set_type_class("vsock_sock", extensions.network.vsock_sock) + self.optional_set_type_class("packet_sock", extensions.network.packet_sock) + self.optional_set_type_class("bt_sock", extensions.network.bt_sock) + self.optional_set_type_class("xdp_sock", extensions.network.xdp_sock) # Only found in 6.1+ kernels self.optional_set_type_class("maple_tree", extensions.maple_tree) + self.optional_set_type_class("latch_tree_root", extensions.latch_tree_root) + self.optional_set_type_class("kernel_symbol", extensions.kernel_symbol) + class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 1, 1) + _version = (2, 4, 0) _required_framework_version = (2, 0, 0) + deleted = "(deleted)" + smear = "" framework.require_interface_version(*_required_framework_version) @@ -106,8 +132,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): Args: task (task_struct): A reference task mnt (vfsmount or mount): A mounted filesystem or a mount point. - - kernels < 3.3.8 type is 'vfsmount' - - kernels >= 3.3.8 type is 'mount' + - kernels < 3.3 type is 'vfsmount' + - kernels >= 3.3 type is 'mount' Returns: str: Pathname of the mount point relative to the task's root directory. @@ -129,14 +155,30 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): rdentry (dentry *): A pointer to the root dentry rmnt (vfsmount *): A pointer to the root vfsmount dentry (dentry *): A pointer to the dentry - vfsmnt (vfsmount *): A pointer to the vfsmount + vfsmnt (vfsmount/vfsmount *): A vfsmount object (kernels >= 3.3) or a + vfsmount pointer (kernels < 3.3) Returns: str: Pathname of the mount point or file """ + if not (rdentry and rdentry.is_readable() and rmnt and rmnt.is_readable()): + return "" + + if isinstance(vfsmnt, objects.Pointer) and not ( + vfsmnt and vfsmnt.is_readable() + ): + # vfsmnt can be the vfsmount object itself (>=3.3) or a vfsmount * (<3.3) + return "" + + inode = dentry.d_inode path_reversed = [] - while dentry != rdentry or not vfsmnt.is_equal(rmnt): + smeared = False + while ( + dentry + and dentry.is_readable() + and (dentry != rdentry or not vfsmnt.is_equal(rmnt)) + ): if dentry == vfsmnt.get_mnt_root() or dentry.is_root(): # Escaped? if dentry != vfsmnt.get_mnt_root(): @@ -153,10 +195,23 @@ 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: + smeared = True path_reversed.append(dname.strip("/")) dentry = parent path = "/" + "/".join(reversed(path_reversed)) + if smeared: + # 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"{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 @@ -244,7 +299,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): ns_ops = ns_common.ops pre_name = utility.pointer_to_string(ns_ops.name, 255) - except IndexError: + except (exceptions.SymbolError, IndexError): pre_name = "" else: pre_name = f" {sym}" @@ -254,7 +309,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 @@ -293,7 +348,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) @@ -306,17 +361,18 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): context: interfaces.context.ContextInterface, symbol_table: str, task: interfaces.objects.ObjectInterface, + files_only: bool = False, ): - # task.files can be null - if not (task.files and task.files.is_readable()): - return None + try: + files = task.files + fd_table = files.get_fds() + if fd_table == 0: + return None - fd_table = task.files.get_fds() - if fd_table == 0: + max_fds = files.get_max_fds() + except exceptions.InvalidAddressException: return None - max_fds = task.files.get_max_fds() - # corruption check if max_fds > 500000: return None @@ -329,11 +385,17 @@ 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 @classmethod + @deprecation.method_being_removed( + removal_date="2025-09-25", + message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", + ) def mask_mods_list( cls, context: interfaces.context.ContextInterface, @@ -341,20 +403,17 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): mods: Iterator[interfaces.objects.ObjectInterface], ) -> List[Tuple[str, int, int]]: """ + DEPRECATED: use "volatility3.framework.symbols.linux.utilities.modules.Modules.mask_mods_list" instead. + A helper function to mask the starting and end address of kernel modules """ - mask = context.layers[layer_name].address_mask - - return [ - ( - utility.array_to_string(mod.name), - mod.get_module_base() & mask, - (mod.get_module_base() & mask) + mod.get_core_size(), - ) - for mod in mods - ] + return linux_utilities_modules.Modules.mask_mods_list(context, layer_name, mods) @classmethod + @deprecation.method_being_removed( + removal_date="2025-09-25", + message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", + ) def generate_kernel_handler_info( cls, context: interfaces.context.ContextInterface, @@ -362,6 +421,8 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): mods_list: Iterator[interfaces.objects.ObjectInterface], ) -> List[Tuple[str, int, int]]: """ + This method is being deprecated. Use `linux_utilities_modules.Modules.run_module_scanners` to map kernel pointers to modules") + A helper function that gets the beginning and end address of the kernel module """ @@ -377,51 +438,85 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return [ (constants.linux.KERNEL_NAME, start_addr, end_addr) - ] + LinuxUtilities.mask_mods_list(context, kernel.layer_name, mods_list) + ] + linux_utilities_modules.Modules.mask_mods_list( + context, kernel.layer_name, mods_list + ) @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.lookup_module_address, + removal_date="2025-09-25", + replacement_version=(2, 0, 0), + ) def lookup_module_address( cls, kernel_module: interfaces.context.ModuleInterface, handlers: List[Tuple[str, int, int]], target_address: int, - ): + ) -> Tuple[str, str]: """ + DEPRECATED: use "volatility3.framework.symbols.linux.utilities.modules.Modules.lookup_module_address" instead. + Searches between the start and end address of the kernel module using target_address. Returns the module and symbol name of the address provided. """ - - mod_name = "UNKNOWN" - symbol_name = "N/A" - - for name, start, end in handlers: - if start <= target_address <= end: - mod_name = name - if name == constants.linux.KERNEL_NAME: - symbols = list( - kernel_module.get_symbols_by_absolute_location(target_address) - ) - - if len(symbols): - symbol_name = ( - symbols[0].split(constants.BANG)[1] - if constants.BANG in symbols[0] - else symbols[0] - ) - - break - - return mod_name, symbol_name + return linux_utilities_modules.Modules.lookup_module_address( + kernel_module.context, kernel_module.name, handlers, target_address + ) @classmethod - def walk_internal_list(cls, vmlinux, struct_name, list_member, list_start): + def walk_internal_list( + cls, + vmlinux: interfaces.context.ModuleInterface, + struct_name: str, + list_member: str, + list_start: interfaces.objects.ObjectInterface, + max_count: int = 4096, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """ + An API that provides generic, smear-resistant enumeration of embedded lists + + Args: + vmlinux: + struct_name: name of the structure of the list elements + list_member: name of the list_member holding the internal list + list_start: Starting (head) member of the list + max_count: Optional maximum amount of list elements that will be yielded + + Returns: + Instances of `struct_name` + """ + + count = 0 + seen = set() + while list_start: + if list_start.vol.offset in seen: + vollog.debug( + "walk_internal_list: Repeat entry found. Stopping enumeration" + ) + break + seen.add(list_start.vol.offset) + + if not (list_start and list_start.is_readable()): + break + list_struct = vmlinux.object( - object_type=struct_name, offset=list_start.vol.offset + object_type=struct_name, offset=list_start.vol.offset, absolute=True ) + yield list_struct + list_start = getattr(list_struct, list_member) + if count == max_count: + vollog.debug( + f"walk_internal_list: Breaking list enumeration at maximum allowed count of {count}" + ) + break + + count += 1 + @classmethod def container_of( cls, @@ -431,7 +526,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): vmlinux: interfaces.context.ModuleInterface, ) -> Optional[interfaces.objects.ObjectInterface]: """Cast a member of a structure out to the containing structure. - It mimicks the Linux kernel macro container_of() see include/linux.kernel.h + It mimics the Linux kernel macro container_of() see include/linux.kernel.h Args: addr: The pointer to the member. @@ -449,6 +544,10 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): type_dec = vmlinux.get_type(type_name) member_offset = type_dec.relative_child_offset(member_name) container_addr = addr - member_offset + layer = vmlinux.context.layers[vmlinux.layer_name] + if not layer.is_valid(container_addr): + return None + return vmlinux.object( object_type=type_name, offset=container_addr, absolute=True ) @@ -483,6 +582,22 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return kernel + @classmethod + def convert_fourcc_code(cls, code: int) -> str: + """Convert a fourcc integer back to its fourcc string representation. + + Args: + code: the numerical representation of the fourcc + + Returns: + The fourcc code string. + """ + + code_bytes_length = (code.bit_length() + 7) // 8 + return "".join( + [chr((code >> (i * 8)) & 0xFF) for i in range(code_bytes_length)] + ) + class IDStorage(ABC): """Abstraction to support both XArray and RadixTree""" @@ -586,7 +701,7 @@ class IDStorage(ABC): raise NotImplementedError @abstractmethod - def get_head_node(self, tree) -> int: + def get_head_node(self, tree) -> Optional[int]: """Returns a pointer to the tree's head""" raise NotImplementedError @@ -596,7 +711,7 @@ class IDStorage(ABC): raise NotImplementedError def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface: - """Instanciates a tree node from its pointer + """Instantiates a tree node from its pointer Args: nodep: Pointer to the XArray/RadixTree node @@ -619,7 +734,11 @@ class IDStorage(ABC): node = self.nodep_to_node(nodep) node_slots = node.slots for off in range(self.CHUNK_SIZE): - slot = node_slots[off] + try: + slot = node_slots[off] + except exceptions.InvalidAddressException: + continue + if slot == 0: continue @@ -629,8 +748,7 @@ class IDStorage(ABC): if self.is_valid_node(nodep): yield nodep else: - for child_node in self._iter_node(nodep, height - 1): - yield child_node + yield from self._iter_node(nodep, height - 1) def get_entries(self, root: interfaces.objects.ObjectInterface) -> Iterator[int]: """Walks the tree data structure @@ -644,7 +762,7 @@ class IDStorage(ABC): height = self.get_tree_height(root.vol.offset) nodep = self.get_head_node(root) - if not nodep: + if not (nodep and nodep.is_readable()): return # Keep the internal flag before untagging it @@ -659,8 +777,7 @@ class IDStorage(ABC): if self.is_valid_node(nodep): yield nodep else: - for child_node in self._iter_node(nodep, height): - yield child_node + yield from self._iter_node(nodep, height) class XArray(IDStorage): @@ -680,10 +797,13 @@ class XArray(IDStorage): def get_node_height(self, nodep) -> int: node = self.nodep_to_node(nodep) - return (node.shift / self.CHUNK_SHIFT) + 1 + return (node.shift // self.CHUNK_SHIFT) + 1 - def get_head_node(self, tree) -> int: - return tree.xa_head + def get_head_node(self, tree) -> Optional[int]: + try: + return tree.xa_head + except exceptions.InvalidAddressException: + return None def node_is_internal(self, nodep) -> bool: return (nodep & self.XARRAY_TAG_MASK) == self.XARRAY_TAG_INTERNAL @@ -703,6 +823,7 @@ class RadixTree(IDStorage): RADIX_TREE_INTERNAL_NODE = 1 RADIX_TREE_EXCEPTIONAL_ENTRY = 2 RADIX_TREE_ENTRY_MASK = 3 + RADIX_TREE_MAP_SHIFT = 6 # CONFIG_BASE_FULL # Dynamic values. These will be initialized later RADIX_TREE_INDEX_BITS = None @@ -739,45 +860,62 @@ class RadixTree(IDStorage): def get_tree_height(self, treep) -> int: with contextlib.suppress(exceptions.SymbolError): if self.vmlinux.get_type("radix_tree_root").has_member("height"): - # kernels < 4.7.10 + # kernels < 4.7 d0891265bbc988dc91ed8580b38eb3dac128581b radix_tree_root = self.vmlinux.object( "radix_tree_root", offset=treep, absolute=True ) return radix_tree_root.height - # kernels >= 4.7.10 + # kernels >= 4.7 return 0 + @functools.cached_property + def _max_height_array(self): + if self.vmlinux.has_symbol("height_to_maxindex"): + # 2.6.24 26fb1589cb0aaec3a0b4418c54f30c1a2b1781f6 <= Kernels < 4.7 d0891265bbc988dc91ed8580b38eb3dac128581b + return self.vmlinux.object_from_symbol("height_to_maxindex") + elif self.vmlinux.has_symbol("height_to_maxnodes"): + # 4.8 c78c66d1ddfdbd2353f3fcfeba0268524537b096 <= kernels < 4.20 8cf2f98411e3a0865026a1061af637161b16d32b + return self.vmlinux.object_from_symbol("height_to_maxnodes") + + return None + def _radix_tree_maxindex(self, node, height) -> int: """Return the maximum key which can be store into a radix tree with this height.""" - if not self.vmlinux.has_symbol("height_to_maxindex"): - # Kernels >= 4.7 - return (self.CHUNK_SIZE << node.shift) - 1 + if self._max_height_array: + # 2.6.24 <= kernels <= 4.20 See _max_height_array() + return self._max_height_array[height] else: - # Kernels < 4.7 - height_to_maxindex_array = self.vmlinux.object_from_symbol( - "height_to_maxindex" - ) - maxindex = height_to_maxindex_array[height] - return maxindex + # Kernels >= 4.20 + return (self.CHUNK_SIZE << node.shift) - 1 def get_node_height(self, nodep) -> int: node = self.nodep_to_node(nodep) if hasattr(node, "shift"): # 4.7 <= Kernels < 4.20 - return (node.shift / self.CHUNK_SHIFT) + 1 + height = (node.shift // self.CHUNK_SHIFT) + 1 elif hasattr(node, "path"): # 3.15 <= Kernels < 4.7 - return node.path & self.RADIX_TREE_HEIGHT_MASK + height = node.path & self.RADIX_TREE_HEIGHT_MASK elif hasattr(node, "height"): # Kernels < 3.15 - return node.height + height = node.height else: raise exceptions.VolatilityException("Cannot find radix-tree node height") - def get_head_node(self, tree) -> int: - return tree.rnode + if self._max_height_array and not (0 <= height < self._max_height_array.count): + error_msg = f"Radix Tree node {node.vol.offset:#x} height {height} exceeds max height of {self._max_height_array.count}" + vollog.error(error_msg) + raise exceptions.LinuxPageCacheException(error_msg) + + return height + + def get_head_node(self, tree) -> Optional[int]: + try: + return tree.rnode + except exceptions.InvalidAddressException: + return None def node_is_internal(self, nodep) -> bool: return (nodep & self.RADIX_TREE_INTERNAL_NODE) != 0 @@ -788,17 +926,19 @@ class RadixTree(IDStorage): def untag_node(self, nodep) -> int: return nodep & (~self.RADIX_TREE_ENTRY_MASK) - def is_valid_node(self, nodep) -> bool: + def _is_exceptional_node(self, nodep) -> bool: # In kernels 4.20, exceptional nodes were removed and internal entries took their bitmask - if self.vmlinux.has_type("radix_tree_root"): - return ( - nodep & self.RADIX_TREE_ENTRY_MASK - ) != self.RADIX_TREE_EXCEPTIONAL_ENTRY + return ( + self.vmlinux.has_type("radix_tree_root") + and (nodep & self.RADIX_TREE_ENTRY_MASK) + == self.RADIX_TREE_EXCEPTIONAL_ENTRY + ) - return True + def is_valid_node(self, nodep) -> bool: + return not self._is_exceptional_node(nodep) -class PageCache(object): +class PageCache: """Linux Page Cache abstraction""" def __init__( @@ -824,11 +964,128 @@ class PageCache(object): Yields: Page objects """ - + layer = self.vmlinux.context.layers[self.vmlinux.layer_name] for page_addr in self._idstorage.get_entries(self._page_cache.i_pages): - if not page_addr: - continue + if not layer.is_valid(page_addr): + error_msg = f"Invalid cached page address at {page_addr:#x}, aborting" + vollog.error(error_msg) + raise exceptions.LinuxPageCacheException(error_msg) page = self.vmlinux.object("page", offset=page_addr, absolute=True) - if page: - yield page + if not page.is_valid(): + error_msg = f"Invalid cached page at {page_addr:#x}, aborting" + vollog.error(error_msg) + raise exceptions.LinuxPageCacheException(error_msg) + + yield page + + +class VMCoreInfo(interfaces.configuration.VersionableInterface): + _required_framework_version = (2, 11, 0) + + _version = (1, 0, 0) + + @classmethod + def _vmcoreinfo_data_to_dict( + cls, + vmcoreinfo_data, + ) -> Optional[Dict[str, str]]: + """Converts the input VMCoreInfo data buffer into a dictionary""" + + # Ensure the whole buffer is printable + printable_bytes_set = set(string.printable.encode()) + if not all(byte in printable_bytes_set for byte in vmcoreinfo_data): + # Abort, we are in the wrong place + return None + + vmcoreinfo_dict = dict() + for line in vmcoreinfo_data.decode().splitlines(): + if not line: + break + + key, value = line.split("=", 1) + vmcoreinfo_dict[key] = cls._parse_value(key, value) + + return vmcoreinfo_dict + + @classmethod + def _parse_value(cls, key, value): + if key.startswith("SYMBOL(") or key == "KERNELOFFSET": + return int(value, 16) + elif key.startswith(("NUMBER(", "LENGTH(", "SIZE(", "OFFSET(")): + return int(value, 0) + elif key == "PAGESIZE": + return int(value, 0) + + # Default, as string + return value + + @classmethod + def search_vmcoreinfo_elf_note( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + progress_callback: constants.ProgressCallback = None, + ) -> Iterator[Tuple[int, Dict[str, str]]]: + """Enumerates each VMCoreInfo ELF note table found in memory along with its offset. + + This approach is independent of any external ISF symbol or type, requiring only the + Elf64_Note found in 'elf.json', which is already included in the framework. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + layer_name: The layer within the context in which the module exists + progress_callback: A function that takes a percentage (and an optional description) that will be called periodically + + Yields: + Tuples with the VMCoreInfo ELF note offset and the VMCoreInfo table parsed in a dictionary. + """ + + elf_table_name = intermed.IntermediateSymbolTable.create( + context, "elf_symbol_table", "linux", "elf" + ) + module = context.module(elf_table_name, layer_name, 0) + layer = context.layers[layer_name] + + # Both Elf32_Note and Elf64_Note are of the same size + elf_note_size = context.symbol_space[elf_table_name].get_type("Elf64_Note").size + + for vmcoreinfo_offset in layer.scan( + scanner=scanners.BytesScanner(linux_constants.VMCOREINFO_MAGIC_ALIGNED), + context=context, + progress_callback=progress_callback, + ): + # vmcoreinfo_note kernels >= 2.6.24 fd59d231f81cb02870b9cf15f456a897f3669b4e + vmcoreinfo_elf_note_offset = vmcoreinfo_offset - elf_note_size + + # Elf32_Note and Elf64_Note are identical, so either can be used interchangeably here + elf_note = module.object( + object_type="Elf64_Note", + offset=vmcoreinfo_elf_note_offset, + absolute=True, + ) + + # Ensure that we are within an ELF note + if ( + elf_note.n_namesz != len(linux_constants.VMCOREINFO_MAGIC) + or elf_note.n_type != 0 + or elf_note.n_descsz == 0 + ): + continue + + vmcoreinfo_data_offset = vmcoreinfo_offset + len( + linux_constants.VMCOREINFO_MAGIC_ALIGNED + ) + + # Also, confirm this with the first tag, which has consistently been OSRELEASE + vmcoreinfo_data = layer.read(vmcoreinfo_data_offset, elf_note.n_descsz) + if not vmcoreinfo_data.startswith(linux_constants.OSRELEASE_TAG): + continue + + table = cls._vmcoreinfo_data_to_dict(vmcoreinfo_data) + if not table: + # Wrong VMCoreInfo note offset, keep trying + continue + + # A valid VMCoreInfo ELF note exists at 'vmcoreinfo_elf_note_offset' + yield vmcoreinfo_elf_note_offset, table diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 927f767e2..0a6e05951 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -9,25 +9,33 @@ import functools import binascii import stat import datetime -import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict +import uuid +from typing import ( + Generator, + Iterable, + Iterator, + Optional, + Tuple, + List, + Union, + Dict, + Callable, +) from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion from volatility3.framework.constants import linux as linux_constants -from volatility3.framework.layers import linear +from volatility3.framework.layers import linear, intel from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed from volatility3.framework.symbols.linux.extensions import elf - vollog = logging.getLogger(__name__) # Keep these in a basic module, to prevent import cycles when symbol providers require them 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 @@ -90,13 +98,13 @@ class module(generic.GenericIntelProcess): return self.mem[module_mem_index] - def _get_mem_size(self, mod_mem_type_name): + def _get_mem_size(self, mod_mem_type_name) -> int: return self._get_mem_type(mod_mem_type_name).size - def _get_mem_base(self, mod_mem_type_name): + def _get_mem_base(self, mod_mem_type_name) -> int: return self._get_mem_type(mod_mem_type_name).base - def get_module_base(self): + def get_module_base(self) -> int: if self.has_member("mem"): # kernels 6.4+ return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): @@ -106,7 +114,7 @@ class module(generic.GenericIntelProcess): raise AttributeError("Unable to get module base") - def get_init_size(self): + def get_init_size(self) -> int: if self.has_member("mem"): # kernels 6.4+ return ( self._get_mem_size("MOD_INIT_TEXT") @@ -120,7 +128,7 @@ class module(generic.GenericIntelProcess): raise AttributeError("Unable to determine .init section size of module") - def get_core_size(self): + def get_core_size(self) -> int: if self.has_member("mem"): # kernels 6.4+ return ( self._get_mem_size("MOD_TEXT") @@ -135,7 +143,7 @@ class module(generic.GenericIntelProcess): raise AttributeError("Unable to determine core size of module") - def get_core_text_size(self): + def get_core_text_size(self) -> int: if self.has_member("mem"): # kernels 6.4+ return self._get_mem_size("MOD_TEXT") elif self.has_member("core_layout"): @@ -145,7 +153,7 @@ class module(generic.GenericIntelProcess): raise AttributeError("Unable to determine core text size of module") - def get_module_core(self): + def get_module_core(self) -> objects.Pointer: if self.has_member("mem"): # kernels 6.4+ return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): @@ -154,7 +162,7 @@ class module(generic.GenericIntelProcess): return self.module_core raise AttributeError("Unable to get module core") - def get_module_init(self): + def get_module_init(self) -> objects.Pointer: if self.has_member("mem"): # kernels 6.4+ return self._get_mem_base("MOD_INIT_TEXT") elif self.has_member("init_layout"): @@ -163,45 +171,76 @@ class module(generic.GenericIntelProcess): return self.module_init raise AttributeError("Unable to get module init") - def get_name(self): + def get_name(self) -> Optional[str]: """Get the name of the module as a string""" - return utility.array_to_string(self.name) + try: + return utility.array_to_string(self.name) + except exceptions.InvalidAddressException: + return None - def _get_sect_count(self, grp): - """Try to determine the number of valid sections""" - arr = self._context.object( - self.get_symbol_table_name() + constants.BANG + "array", - layer_name=self.vol.layer_name, - offset=grp.attrs, - subtype=self._context.symbol_space.get_type( - self.get_symbol_table_name() + constants.BANG + "pointer" - ), - count=25, - ) + def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: + """Try to determine the number of valid sections. Support for kernels > 6.14-rc1. - idx = 0 - while arr[idx]: - idx = idx + 1 - return idx + Resources: + - https://github.com/torvalds/linux/commit/d8959b947a8dfab1047c6fd5e982808f65717bfe + - https://github.com/torvalds/linux/commit/e0349c46cb4fbbb507fa34476bd70f9c82bad359 + """ - def get_sections(self): - """Get sections of the module""" - if self.sect_attrs.has_member("nsections"): - num_sects = self.sect_attrs.nsections + if grp.has_member("bin_attrs"): + arr_offset_ptr = grp.bin_attrs + arr_subtype = "bin_attribute" else: - num_sects = self._get_sect_count(self.sect_attrs.grp) + arr_offset_ptr = grp.attrs + arr_subtype = "attribute" + + if not arr_offset_ptr.is_readable(): + vollog.log( + constants.LOGLEVEL_V, + f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}", + ) + return 0 + + # We chose 100 as an arbitrary guard value to prevent + # looping forever in extreme cases, and because 100 is not expected + # to be a valid number of sections. If that still happens, + # Vol3 module processing will indicate that it is missing information + # with the following message: + # "Unable to reconstruct the ELF for module struct at" + # See PR #1773 for more information. + bin_attrs_list = utility.dynamically_sized_array_of_pointers( + context=self._context, + array=arr_offset_ptr.dereference(), + subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype, + iterator_guard_value=100, + ) + return len(bin_attrs_list) + + @functools.cached_property + def number_of_sections(self) -> int: + # Dropped in 6.14-rc1: d8959b947a8dfab1047c6fd5e982808f65717bfe + if self.sect_attrs.has_member("nsections"): + return self.sect_attrs.nsections + + return self._get_sect_count(self.sect_attrs.grp) + + def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Get a list of section attributes for the given module.""" + if self.number_of_sections == 0: + vollog.debug( + f"Invalid number of sections ({self.number_of_sections}) for module at offset {self.vol.offset:#x}" + ) + return [] + + symbol_table_name = self.get_symbol_table_name() arr = self._context.object( - self.get_symbol_table_name() + constants.BANG + "array", + symbol_table_name + constants.BANG + "array", layer_name=self.vol.layer_name, offset=self.sect_attrs.attrs.vol.offset, - subtype=self._context.symbol_space.get_type( - self.get_symbol_table_name() + constants.BANG + "module_sect_attr" - ), - count=num_sects, + subtype=self.sect_attrs.attrs.vol.subtype, + count=self.number_of_sections, ) - for attr in arr: - yield attr + yield from arr def get_elf_table_name(self): elf_table_name = intermed.IntermediateSymbolTable.create( @@ -214,66 +253,106 @@ class module(generic.GenericIntelProcess): ) return elf_table_name - def get_symbols(self): - """Get symbols of the module + def get_symbols(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Get ELF symbol objects for this module""" - Yields: - A symbol object - """ + if not self.section_strtab or self.num_symtab < 1: + return None - if not hasattr(self, "_elf_table_name"): - self._elf_table_name = self.get_elf_table_name() - if symbols.symbol_table_is_64bit(self._context, self.get_symbol_table_name()): - prefix = "Elf64_" - else: - prefix = "Elf32_" - syms = self._context.object( - self.get_symbol_table_name() + constants.BANG + "array", + elf_table_name = self.get_elf_table_name() + symbol_table_name = self.get_symbol_table_name() + + is_64bit = symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ) + sym_name = "Elf64_Sym" if is_64bit else "Elf32_Sym" + sym_type = self._context.symbol_space.get_type( + elf_table_name + constants.BANG + sym_name + ) + elf_syms = self._context.object( + symbol_table_name + constants.BANG + "array", layer_name=self.vol.layer_name, offset=self.section_symtab, - subtype=self._context.symbol_space.get_type( - self._elf_table_name + constants.BANG + prefix + "Sym" - ), - count=self.num_symtab + 1, + subtype=sym_type, + count=self.num_symtab, ) - if self.section_strtab: - for sym in syms: - yield sym + for elf_sym_obj in elf_syms: + # Prepare the symbol object for methods like get_name() + elf_sym_obj.cached_strtab = self.section_strtab + yield elf_sym_obj - def get_symbols_names_and_addresses(self) -> Iterable[Tuple[str, int]]: + def get_symbols_names_and_addresses( + self, max_symbols: int = 4096 + ) -> Iterable[Tuple[str, int]]: """Get names and addresses for each symbol of the module Yields: A tuple for each symbol containing the symbol name and its corresponding value """ + layer = self._context.layers[self.vol.layer_name] + for iteration_counter, elf_sym_obj in enumerate(self.get_symbols()): + if iteration_counter > max_symbols: + vollog.debug( + f"Hit maximum symbols ({max_symbols}) for ELF at {self.vol.offset:#x} in layer {self.vol.layer_name}" + ) + return - for sym in self.get_symbols(): - sym_arr = self._context.object( - self.get_symbol_table_name() + constants.BANG + "array", - layer_name=self.vol.native_layer_name, - offset=self.section_strtab + sym.st_name, - ) - try: - sym_name = utility.array_to_string( - sym_arr, 512 - ) # 512 is the value of KSYM_NAME_LEN kernel constant - except exceptions.InvalidAddressException: + sym_name = elf_sym_obj.get_name() + if not sym_name: continue - if sym_name != "": - # Normalize sym.st_value offset, which is an address pointing to the symbol value - mask = self._context.layers[self.vol.layer_name].address_mask - sym_address = sym.st_value & mask - yield (sym_name, sym_address) - def get_symbol(self, wanted_sym_name): - """Get symbol value for a given symbol name""" + # Normalize sym.st_value offset, which is an address pointing to the symbol value + sym_address = elf_sym_obj.st_value & layer.address_mask + yield (sym_name, sym_address) + + @functools.lru_cache + def get_module_address_boundaries(self) -> Optional[Tuple[int, int]]: + """Return the module address boundaries based on its symbol addresses""" + + if not self.section_strtab or self.num_symtab < 1: + return None + + elf_table_name = self.get_elf_table_name() + symbol_table_name = self.get_symbol_table_name() + + is_64bit = symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ) + sym_name = "Elf64_Sym" if is_64bit else "Elf32_Sym" + sym_type = self._context.symbol_space.get_type( + elf_table_name + constants.BANG + sym_name + ) + elf_syms = self._context.object( + symbol_table_name + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=self.section_symtab, + subtype=sym_type, + count=self.num_symtab, + ) + # They should be sorted, but just in case + elf_syms_sorted = sorted(elf_syms, key=lambda x: x.st_value) + + layer = self._context.layers[self.vol.layer_name] + + # The first elf_sym is null + first_symbol = elf_syms_sorted[1] + last_symbol = elf_syms_sorted[-1] + minimum_address = first_symbol.st_value & layer.address_mask + maximum_address = ( + last_symbol.st_value & layer.address_mask + last_symbol.st_size + ) + + return minimum_address, maximum_address + + def get_symbol(self, wanted_sym_name) -> Optional[int]: + """Get symbol address for a given symbol name""" for sym_name, sym_address in self.get_symbols_names_and_addresses(): if wanted_sym_name == sym_name: return sym_address return None - def get_symbol_by_address(self, wanted_sym_address): + def get_symbol_by_address(self, wanted_sym_address) -> Optional[str]: """Get symbol name for a given symbol address""" for sym_name, sym_address in self.get_symbols_names_and_addresses(): if wanted_sym_address == sym_address: @@ -282,35 +361,141 @@ class module(generic.GenericIntelProcess): return None @property - def section_symtab(self): - if self.has_member("kallsyms"): - return self.kallsyms.symtab - elif self.has_member("symtab"): - return self.symtab + def section_symtab(self) -> Optional[interfaces.objects.ObjectInterface]: + try: + if self.has_member("kallsyms"): + return self.kallsyms.symtab + elif self.has_member("symtab"): + return self.symtab + except exceptions.InvalidAddressException: + vollog.debug( + f"Page fault encountered when accessing symtab of ELF at {self.vol.offset:#x} in {self.vol.layer_name}" + ) + return None + raise AttributeError("Unable to get symtab") @property - def num_symtab(self): - if self.has_member("kallsyms"): - return int(self.kallsyms.num_symtab) - elif self.has_member("num_symtab"): - return int(self.member("num_symtab")) + def num_symtab(self) -> Optional[int]: + try: + if self.has_member("kallsyms"): + return int(self.kallsyms.num_symtab) + elif self.has_member("num_symtab"): + return int(self.member("num_symtab")) + except exceptions.InvalidAddressException: + vollog.debug( + f"Page fault encountered when accessing num_symtab of ELF at {self.vol.offset:#x} in {self.vol.layer_name}" + ) + return None + raise AttributeError("Unable to determine number of symbols") @property - def section_strtab(self): - # Newer kernels - if self.has_member("kallsyms"): - return self.kallsyms.strtab - # Older kernels - elif self.has_member("strtab"): - return self.strtab + def section_strtab(self) -> Optional[interfaces.objects.ObjectInterface]: + try: + # Newer kernels + if self.has_member("kallsyms"): + return self.kallsyms.strtab + # Older kernels + elif self.has_member("strtab"): + return self.strtab + except exceptions.InvalidAddressException: + vollog.debug( + f"Page fault encountered when accessing strtab of ELF at {self.vol.offset:#x} in {self.vol.layer_name}" + ) + return None + raise AttributeError("Unable to get strtab") + @property + def section_typetab(self) -> Optional[interfaces.objects.ObjectInterface]: + try: + if self.has_member("kallsyms") and self.kallsyms.has_member("typetab"): + # kernels >= 4.5 8244062ef1e54502ef55f54cced659913f244c3e: kallsyms was added + # kernels >= 5.2 1c7651f43777cdd59c1aaa82c87324d3e7438c7b: types have its own array + return self.kallsyms.typetab + except exceptions.InvalidAddressException: + vollog.debug( + f"Page fault encountered when accessing typetab of ELF at {self.vol.offset:#x} in {self.vol.layer_name}" + ) + return None + + raise AttributeError("Unable to get typetab section, it needs a kernel >= 5.2") + + def get_symbol_type( + self, symbol: interfaces.objects.ObjectInterface, symbol_index: int + ) -> Optional[str]: + """Determines the type of a given ELF symbol. + + Args: + symbol: The ELF symbol object (elf_sym) + symbol_index: The index of the symbol within the type table + + Returns: + A single-character string representing the symbol type + """ + try: + if self.has_member("kallsyms") and self.kallsyms.has_member("typetab"): + # kernels >= 5.2 1c7651f43777cdd59c1aaa82c87324d3e7438c7b types have its own array + layer = self._context.layers[self.vol.layer_name] + sym_type = layer.read(self.section_typetab + symbol_index, 1) + sym_type = sym_type.decode("utf-8", errors="ignore") + else: + # kernels < 5.2 the type was stored in the st_info + sym_type = chr(symbol.st_info) + except exceptions.InvalidAddressException: + vollog.debug( + f"Page fault encountered when accessing symbol type of index {symbol_index} of ELF at {self.vol.offset:#x} in {self.vol.layer_name}" + ) + return None + + return sym_type + class task_struct(generic.GenericIntelProcess): + def is_valid(self) -> bool: + layer = self._context.layers[self.vol.layer_name] + # Make sure the entire task content is readable + if not layer.is_valid(self.vol.offset, self.vol.size): + return False + + if self.pid < 0 or self.tgid < 0: + return False + + if self.has_member("signal") and not ( + self.signal and self.signal.is_readable() + ): + return False + + if self.has_member("nsproxy") and not ( + self.nsproxy and self.nsproxy.is_readable() + ): + return False + + if self.has_member("real_parent") and not ( + self.real_parent and self.real_parent.is_readable() + ): + return False + + if ( + self.has_member("active_mm") + and self.active_mm + and not self.active_mm.is_readable() + ): + return False + + if self.mm: + if not self.mm.is_readable(): + return False + + if self.mm != self.active_mm: + return False + + return True + + @functools.lru_cache def add_process_layer( - self, config_prefix: str = None, preferred_name: str = None + self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None ) -> Optional[str]: """Constructs a new layer based on the process's DTB. @@ -326,9 +511,11 @@ class task_struct(generic.GenericIntelProcess): raise TypeError( "Parent layer is not a translation layer, unable to construct process layer" ) - dtb, layer_name = parent_layer.translate(pgd) - if not dtb: + try: + dtb, layer_name = parent_layer.translate(pgd) + except exceptions.InvalidAddressException: return None + if preferred_name is None: preferred_name = self.vol.layer_name + f"_Process{self.pid}" # Add the constructed layer and return the name @@ -336,6 +523,19 @@ class task_struct(generic.GenericIntelProcess): self._context, dtb, config_prefix, preferred_name ) + def get_address_space_layer( + self, + ) -> Optional[interfaces.layers.TranslationLayerInterface]: + """Returns the task layer for this task's address space.""" + + task_layer_name = ( + self.vol.layer_name if self.is_kernel_thread else self.add_process_layer() + ) + if not task_layer_name: + return None + + return self._context.layers[task_layer_name] + def get_process_memory_sections( self, heap_only: bool = False ) -> Generator[Tuple[int, int], None, None]: @@ -401,6 +601,8 @@ class task_struct(generic.GenericIntelProcess): tasks_iterable = self._get_tasks_iterable() threads_seen = set([self.vol.offset]) for task in tasks_iterable: + if not task.is_valid(): + continue if task.vol.offset not in threads_seen: threads_seen.add(task.vol.offset) yield task @@ -444,6 +646,15 @@ class task_struct(generic.GenericIntelProcess): else None ) + @property + def state(self): + if self.has_member("__state"): + return self.member("__state") + elif self.has_member("state"): + return self.member("state") + else: + raise AttributeError("Unsupported task_struct: Cannot find state") + def _get_task_start_time(self) -> datetime.timedelta: """Returns the task's monotonic start_time as a timedelta. @@ -586,7 +797,9 @@ class task_struct(generic.GenericIntelProcess): raise exceptions.VolatilityException("Unsupported") - def get_boottime(self, root_time_namespace: bool = True) -> datetime.datetime: + def get_boottime( + self, root_time_namespace: bool = True + ) -> Optional[datetime.datetime]: """Returns the boot time in UTC as a datetime. Args: @@ -610,7 +823,7 @@ class task_struct(generic.GenericIntelProcess): return boottime.to_datetime() - def get_create_time(self) -> datetime.datetime: + def get_create_time(self) -> Optional[datetime.datetime]: """Retrieves the task's start time from its time namespace. Args: context: The context to retrieve required elements (layers, symbol tables) from @@ -626,6 +839,8 @@ class task_struct(generic.GenericIntelProcess): # The kernel exports only tv_sec to procfs, see kernel's show_stat(). # This means user-space tools, like those in the procps package (e.g., ps, top, etc.), # only use the boot time seconds to compute dates relatives to this. + if boottime is None: + return None boottime = boottime.replace(microsecond=0) task_start_time_timedelta = self._get_task_start_time() @@ -635,6 +850,20 @@ class task_struct(generic.GenericIntelProcess): # root time namespace, not within the task's own time namespace return boottime + task_start_time_timedelta + def get_parent_pid(self) -> int: + """Returns the parent process ID (PPID) + + This method replicates the Linux kernel's `getppid` syscall behavior. + Avoid using `task.parent`; instead, use this function for accurate results. + """ + + if self.real_parent and self.real_parent.is_readable(): + ppid = self.real_parent.tgid + else: + ppid = 0 + + return ppid + class fs_struct(objects.StructType): def get_root_dentry(self): @@ -688,7 +917,7 @@ class maple_tree(objects.StructType): expected_maple_tree_depth, seen=None, current_depth=1, - ): + ) -> Optional[int]: """Recursively parse Maple Tree Nodes and yield all non empty slots""" # Create seen set if it does not exist, e.g. on the first call into this recursive function. This @@ -737,7 +966,8 @@ class maple_tree(objects.StructType): node_parent_pointer = node_parent_mte & ~(self.MAPLE_NODE_POINTER_MASK) # verify that the node_parent_pointer correctly points to the parent - assert node_parent_pointer == parent + if node_parent_pointer != parent: + return None # create a node object node = self._context.object( @@ -776,14 +1006,13 @@ class maple_tree(objects.StructType): current_depth + 1, ) else: - # unkown maple node type + # unknown maple node type raise AttributeError( - f"Unkown Maple Tree node type {node_type} at offset {hex(pointer)}." + f"Unknown Maple Tree node type {node_type} at offset {hex(pointer)}." ) 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]: """ @@ -797,23 +1026,30 @@ class mm_struct(objects.StructType): def _get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """Returns an iterator for the mmap list member of an mm_struct. Use this only if required, get_vma_iter() will choose the correct _get_maple_tree_iter() or - _get_mmap_iter() automatically as required.""" + _get_mmap_iter() automatically as required. + + Yields: + vm_area_struct objects + """ if not self.has_member("mmap"): raise AttributeError( "_get_mmap_iter called on mm_struct where no mmap member exists." ) - if not self.mmap: + vma_pointer = self.mmap + if not (vma_pointer and vma_pointer.is_readable()): return None - yield self.mmap + vma_object = vma_pointer.dereference() + yield vma_object - seen = {self.mmap.vol.offset} - link = self.mmap.vm_next + seen = {vma_pointer} + vma_pointer = vma_pointer.vm_next - while link != 0 and link.vol.offset not in seen: - yield link - seen.add(link.vol.offset) - link = link.vm_next + while vma_pointer and vma_pointer.is_readable() and vma_pointer not in seen: + vma_object = vma_pointer.dereference() + yield vma_object + seen.add(vma_pointer) + vma_pointer = vma_pointer.vm_next # TODO: As of version 3.0.0 this method should be removed def get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: @@ -828,7 +1064,11 @@ class mm_struct(objects.StructType): def _get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """Returns an iterator for the mm_mt member of an mm_struct. Use this only if required, get_vma_iter() will choose the correct _get_maple_tree_iter() or - get_mmap_iter() automatically as required.""" + get_mmap_iter() automatically as required. + + Yields: + vm_area_struct objects + """ if not self.has_member("mm_mt"): raise AttributeError( @@ -836,24 +1076,51 @@ class mm_struct(objects.StructType): ) symbol_table_name = self.get_symbol_table_name() for vma_pointer in self.mm_mt.get_slot_iter(): - # convert pointer to vm_area_struct and yield - vma = self._context.object( - symbol_table_name + constants.BANG + "vm_area_struct", - layer_name=self.vol.native_layer_name, - offset=vma_pointer, - ) - yield vma + try: + vma_object = vma_pointer.dereference().cast( + symbol_table_name + constants.BANG + "vm_area_struct" + ) + except exceptions.InvalidAddressException: + continue - def get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: - """Returns an iterator for the VMAs in an mm_struct. Automatically choosing the mmap or mm_mt as required.""" + # The slots will hold values related to their slot if they are invalid + # Before this check, this function was returning objects on the first page of memory... + if vma_object.vol.offset < 0x1000: + continue + + yield vma_object + + def _do_get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns an iterator for the VMAs in an mm_struct. + Automatically choosing the mmap or mm_mt as required. + + Yields: + vm_area_struct objects + """ if self.has_member("mmap"): + # kernels < 6.1 yield from self._get_mmap_iter() elif self.has_member("mm_mt"): + # kernels >= 6.1 d4af56c5c7c6781ca6ca8075e2cf5bc119ed33d1 yield from self._get_maple_tree_iter() else: raise AttributeError("Unable to find mmap or mm_mt in mm_struct") + def get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns an iterator for the VMAs in an mm_struct. + Automatically choosing the mmap or mm_mt as required. + + Yields: + vm_area_struct objects + """ + for vma in self._do_get_vma_iter(): + if not vma.is_valid(): + vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}") + continue + + yield vma + class super_block(objects.StructType): # include/linux/kdev_t.h @@ -882,14 +1149,28 @@ class super_block(objects.StructType): SB_LAZYTIME: "lazytime", } - @property + @functools.cached_property def major(self) -> int: return self.s_dev >> self.MINORBITS - @property + @functools.cached_property def minor(self) -> int: return self.s_dev & ((1 << self.MINORBITS) - 1) + @functools.cached_property + def uuid(self) -> str: + if not self.has_member("s_uuid"): + raise AttributeError( + "super_block struct does not support s_uuid direct attribute access, probably indicating a kernel version < 2.6.39-rc1." + ) + + if self.s_uuid.has_member("b"): + uuid_as_ints = self.s_uuid.b + else: + uuid_as_ints = self.s_uuid + + return str(uuid.UUID(bytes=bytes(uuid_as_ints))) + def get_flags_access(self) -> str: return "ro" if self.s_flags & self.SB_RDONLY else "rw" @@ -974,6 +1255,39 @@ class vm_area_struct(objects.StructType): retval = retval + "-" return retval + def is_valid(self) -> bool: + """Validate a VMA struct to prevent processing smeared entries.""" + try: + start = self.vm_start + end = self.vm_end + self.get_protection() + except exceptions.InvalidAddressException: + return False + + layer = self._context.layers[self.vol.layer_name] + length = end - start + if ( + (start > end) + or (start == 0 and length == 0) + or (length % layer.page_size != 0) + ): + return False + + if self.vm_file != 0: + try: + inode = self.vm_file.get_inode() + except exceptions.InvalidAddressException: + return False + + # Verify that a file-backed VMA's page offset + # is not greater than the size of the file's inode. + # Check only inode sizes greater than 0 to account for + # special devices (e.g. "/dev/dri/card0") and prevent false negatives. + if inode.i_size > 0 and self.get_page_offset() > inode.i_size: + return False + + return True + # only parse the rwx bits def get_protection(self) -> str: return self._parse_flags(self.vm_flags & 0b1111, vm_area_struct.perm_flags) @@ -988,7 +1302,7 @@ class vm_area_struct(objects.StructType): parent_layer = self._context.layers[self.vol.layer_name] return self.vm_pgoff << parent_layer.page_shift - def get_name(self, context, task): + def _do_get_name(self, context, task) -> str: if self.vm_file != 0: fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file) elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk: @@ -1004,6 +1318,48 @@ class vm_area_struct(objects.StructType): fname = "Anonymous Mapping" return fname + def get_name(self, context, task) -> Optional[str]: + try: + return self._do_get_name(context, task) + 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 @@ -1019,7 +1375,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 @@ -1042,7 +1398,7 @@ class qstr(objects.StructType): else: str_length = 255 try: - ret = objects.utility.pointer_to_string(self.name, str_length) + ret = utility.pointer_to_string(self.name, str_length) except (exceptions.InvalidAddressException, ValueError): ret = "" return ret @@ -1107,16 +1463,23 @@ class dentry(objects.StructType): walk_member = "d_sib" list_head_member = self.d_children elif self.has_member("d_child") and self.has_member("d_subdirs"): - # 2.5.0 <= kernels < 6.8 + # 3.19.0 <= kernels < 6.8 walk_member = "d_child" list_head_member = self.d_subdirs + elif self.has_member("d_u") and self.has_member("d_subdirs"): + # kernels < 3.19 + + # Actually, 'd_u.d_child' but to_list() doesn't support something like that. + # Since, it's an union, everything is at the same offset than 'd_u'. + walk_member = "d_u" + list_head_member = self.d_subdirs else: raise exceptions.VolatilityException("Unsupported dentry type") dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry" yield from list_head_member.to_list(dentry_type_name, walk_member) - def get_inode(self) -> interfaces.objects.ObjectInterface: + def get_inode(self) -> Optional[interfaces.objects.ObjectInterface]: """Returns the inode associated with this dentry""" inode_ptr = self.d_inode @@ -1131,21 +1494,17 @@ class struct_file(objects.StructType): """Returns a pointer to the dentry associated with this file""" if self.has_member("f_path"): return self.f_path.dentry - elif self.has_member("f_dentry"): - return self.f_dentry - else: - raise AttributeError("Unable to find file -> dentry") + + raise AttributeError("Unable to find file -> dentry") def get_vfsmnt(self) -> interfaces.objects.ObjectInterface: """Returns the fs (vfsmount) where this file is mounted""" if self.has_member("f_path"): return self.f_path.mnt - elif self.has_member("f_vfsmnt"): - return self.f_vfsmnt - else: - raise AttributeError("Unable to find file -> vfs mount") - def get_inode(self) -> interfaces.objects.ObjectInterface: + raise AttributeError("Unable to find file -> vfs mount") + + def get_inode(self) -> Optional[interfaces.objects.ObjectInterface]: """Returns an inode associated with this file""" inode_ptr = None @@ -1188,35 +1547,43 @@ class list_head(objects.StructType, collections.abc.Iterable): Objects of the type specified via the "symbol_type" argument. """ - layer = layer or self.vol.layer_name + layer_name = layer or self.vol.layer_name + + trans_layer = self._context.layers[layer_name] + if not trans_layer.is_valid(self.vol.offset): + return None relative_offset = self._context.symbol_space.get_type( symbol_type ).relative_child_offset(member) - direction = "prev" - if forward: - direction = "next" - try: - link = getattr(self, direction).dereference() - except exceptions.InvalidAddressException: + direction = "next" if forward else "prev" + + link_ptr = getattr(self, direction) + if not (link_ptr and link_ptr.is_readable()): return None + link = link_ptr.dereference() + if not sentinel: - yield self._context.object( - symbol_type, layer, offset=self.vol.offset - relative_offset - ) + obj_offset = self.vol.offset - relative_offset + if not trans_layer.is_valid(obj_offset): + return None + + yield self._context.object(symbol_type, layer_name, offset=obj_offset) + seen = {self.vol.offset} while link.vol.offset not in seen: - obj = self._context.object( - symbol_type, layer, offset=link.vol.offset - relative_offset - ) - yield obj + obj_offset = link.vol.offset - relative_offset + if not trans_layer.is_valid(obj_offset): + return None + + yield self._context.object(symbol_type, layer_name, offset=obj_offset) seen.add(link.vol.offset) - try: - link = getattr(link, direction).dereference() - except exceptions.InvalidAddressException: + link_ptr = getattr(link, direction) + if not (link_ptr and link_ptr.is_readable()): break + link = link_ptr.dereference() def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) @@ -1373,9 +1740,9 @@ class mount(objects.StructType): A dentry pointer """ vfsmnt = self.get_vfsmnt_current() - dentry = vfsmnt.mnt_root + dentry_pointer = vfsmnt.mnt_root - return dentry + return dentry_pointer def get_dentry_parent(self): """Returns the parent root of the mounted tree @@ -1462,7 +1829,7 @@ class mount(objects.StructType): def next_peer(self): table_name = self.vol.type_name.split(constants.BANG)[0] - mount_struct = "{0}{1}mount".format(table_name, constants.BANG) + mount_struct = f"{table_name}{constants.BANG}mount" offset = self._context.symbol_space.get_type( mount_struct ).relative_child_offset("mnt_share") @@ -1483,16 +1850,16 @@ class vfsmount(objects.StructType): ) def _is_kernel_prior_to_struct_mount(self) -> bool: - """Helper to distinguish between kernels prior to version 3.3.8 that - lacked the 'mount' structure and later versions that have it. + """Helper to distinguish between kernels prior to version 3.3 which lacked the + 'mount' struct, versus later versions that include it. + See 7d6fec45a5131918b51dcd76da52f2ec86a85be6. - The 'mnt_parent' member was moved from struct 'vfsmount' to struct - 'mount' when the latter was introduced. - - Alternatively, vmlinux.has_type('mount') can be used here but it is faster. + # Following that commit, also in kernel version 3.3 (3376f34fff5be9954fd9a9c4fd68f4a0a36d480e), + # the 'mnt_parent' member was relocated from the 'vfsmount' struct to the newly + # introduced 'mount' struct. Returns: - bool: 'True' if the kernel + 'True' if the kernel lacks the 'mount' struct, typically indicating kernel < 3.3. """ return self.has_member("mnt_parent") @@ -1500,22 +1867,21 @@ class vfsmount(objects.StructType): def is_equal(self, vfsmount_ptr) -> bool: """Helper to make sure it is comparing two pointers to 'vfsmount'. - Depending on the kernel version, the calling object (self) could be - a 'vfsmount \\*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust - in the framework "auto" dereferencing ability to assure that when we - reach this point 'self' will be a 'vfsmount' already and self.vol.offset + Depending on the kernel version, see 3376f34fff5be9954fd9a9c4fd68f4a0a36d480e, + the calling object (self) could be a 'vfsmount \\*' (<3.3) or a 'vfsmount' (>=3.3). + This way we trust in the framework "auto" dereferencing ability to assure that + when we reach this point 'self' will be a 'vfsmount' already and self.vol.offset a 'vfsmount \\*' and not a 'vfsmount \\*\\*'. The argument must be a 'vfsmount \\*'. Typically, it's called from do_get_path(). Args: - vfsmount_ptr (vfsmount *): A pointer to a 'vfsmount' + vfsmount_ptr: A pointer to a 'vfsmount' Raises: exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \\*' Returns: - bool: 'True' if the given argument points to the the same 'vfsmount' - as 'self'. + 'True' if the given argument points to the same 'vfsmount' as 'self'. """ if isinstance(vfsmount_ptr, objects.Pointer): return self.vol.offset == vfsmount_ptr @@ -1524,13 +1890,14 @@ class vfsmount(objects.StructType): "Unexpected argument type. It has to be a 'vfsmount *'" ) - def _get_real_mnt(self): + def _get_real_mnt(self) -> interfaces.objects.ObjectInterface: """Gets the struct 'mount' containing this 'vfsmount'. - It should be only called from kernels >= 3.3.8 when 'struct mount' was introduced. + It should be only called from kernels >= 3.3 when 'struct mount' was introduced. + See 7d6fec45a5131918b51dcd76da52f2ec86a85be6 Returns: - mount: the struct 'mount' containing this 'vfsmount'. + The 'mount' object containing this 'vfsmount'. """ vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) return linux.LinuxUtilities.container_of( @@ -1549,8 +1916,8 @@ class vfsmount(objects.StructType): """Gets the parent fs (vfsmount) to where it's mounted on Returns: - For kernels < 3.3.8: A vfsmount pointer - For kernels >= 3.3.8: A vfsmount object + For kernels < 3.3: A vfsmount pointer + For kernels >= 3.3: A vfsmount object """ if self._is_kernel_prior_to_struct_mount(): return self.get_mnt_parent() @@ -1583,8 +1950,8 @@ class vfsmount(objects.StructType): """Gets the mnt_parent member. Returns: - For kernels < 3.3.8: A vfsmount pointer - For kernels >= 3.3.8: A mount pointer + For kernels < 3.3: A vfsmount pointer + For kernels >= 3.3: A mount pointer """ if self._is_kernel_prior_to_struct_mount(): return self.mnt_parent @@ -1655,15 +2022,17 @@ class kobject(objects.StructType): class mnt_namespace(objects.StructType): def get_inode(self): if self.has_member("proc_inum"): + # 98f842e675f96ffac96e6c50315790912b2812be 3.8 <= kernels < 3.19 return self.proc_inum elif self.has_member("ns") and self.ns.has_member("inum"): + # kernels >= 3.19 435d5f4bb2ccba3b791d9ef61d2590e30b8e806e return self.ns.inum else: raise AttributeError("Unable to find mnt_namespace inode") def get_mount_points( self, - ) -> Iterator[interfaces.objects.ObjectInterface]: + ) -> Iterator[Optional[interfaces.objects.ObjectInterface]]: """Yields the mount points for this mount namespace. Yields: @@ -1689,8 +2058,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: @@ -1699,281 +2069,11 @@ class mnt_namespace(objects.StructType): ) -class net(objects.StructType): - def get_inode(self): - if self.has_member("proc_inum"): - # 3.8.13 <= kernel < 3.19.8 - return self.proc_inum - elif self.has_member("ns") and self.ns.has_member("inum"): - # kernel >= 3.19.8 - return self.ns.inum - else: - # kernel < 3.8.13 - raise AttributeError("Unable to find net_namespace inode") - - -class socket(objects.StructType): - def _get_vol_kernel(self): - symbol_table_arr = self.vol.type_name.split("!", 1) - symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None - - module_names = list( - self._context.modules.get_modules_by_symbol_tables(symbol_table) - ) - if not module_names: - raise ValueError(f"No module using the symbol table {symbol_table}") - kernel_module_name = module_names[0] - kernel = self._context.modules[kernel_module_name] - return kernel - - def get_inode(self): - try: - kernel = self._get_vol_kernel() - except ValueError: - return 0 - socket_alloc = linux.LinuxUtilities.container_of( - self.vol.offset, "socket_alloc", "socket", kernel - ) - vfs_inode = socket_alloc.vfs_inode - - return vfs_inode.i_ino - - def get_state(self): - socket_state_idx = self.state - if 0 <= socket_state_idx < len(linux_constants.SOCKET_STATES): - return linux_constants.SOCKET_STATES[socket_state_idx] - - -class sock(objects.StructType): - def get_family(self): - family_idx = self.__sk_common.skc_family - if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): - return linux_constants.SOCK_FAMILY[family_idx] - - def get_type(self): - return linux_constants.SOCK_TYPES.get(self.sk_type, "") - - def get_inode(self): - if not self.sk_socket: - return 0 - return self.sk_socket.get_inode() - - def get_protocol(self): - return None - - def get_state(self): - # Return the generic socket state - if self.has_member("sk"): - return self.sk.sk_socket.get_state() - return self.sk_socket.get_state() - - -class unix_sock(objects.StructType): - def get_name(self): - if not self.addr: - return None - sockaddr_un = self.addr.name.cast("sockaddr_un") - saddr = str(utility.array_to_string(sockaddr_un.sun_path)) - return saddr - - def get_protocol(self): - return None - - def get_state(self): - """Return a string representing the sock state.""" - - # Unix socket states reuse (a subset) of the inet_sock states contants - if self.sk.get_type() == "STREAM": - state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(linux_constants.TCP_STATES): - return linux_constants.TCP_STATES[state_idx] - else: - # Return the generic socket state - return self.sk.sk_socket.get_state() - - def get_inode(self): - return self.sk.get_inode() - - -class inet_sock(objects.StructType): - def get_family(self): - family_idx = self.sk.__sk_common.skc_family - if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): - return linux_constants.SOCK_FAMILY[family_idx] - - def get_protocol(self): - # If INET6 family and a proto is defined, we use that specific IPv6 protocol. - # Otherwise, we use the standard IP protocol. - protocol = linux_constants.IP_PROTOCOLS.get(self.sk.sk_protocol) - if self.get_family() == "AF_INET6": - protocol = linux_constants.IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) - return protocol - - def get_state(self): - """Return a string representing the sock state.""" - - if self.sk.get_type() == "STREAM": - state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(linux_constants.TCP_STATES): - return linux_constants.TCP_STATES[state_idx] - else: - # Return the generic socket state - return self.sk.sk_socket.get_state() - - def get_src_port(self): - sport_le = getattr(self, "sport", getattr(self, "inet_sport", None)) - if sport_le is not None: - return socket_module.htons(sport_le) - - def get_dst_port(self): - sk_common = self.sk.__sk_common - if hasattr(sk_common, "skc_portpair"): - dport_le = sk_common.skc_portpair & 0xFFFF - elif hasattr(self, "dport"): - dport_le = self.dport - elif hasattr(self, "inet_dport"): - dport_le = self.inet_dport - elif hasattr(sk_common, "skc_dport"): - dport_le = sk_common.skc_dport - else: - return None - return socket_module.htons(dport_le) - - def get_src_addr(self): - sk_common = self.sk.__sk_common - family = sk_common.skc_family - if family == socket_module.AF_INET: - addr_size = 4 - if hasattr(self, "rcv_saddr"): - saddr = self.rcv_saddr - elif hasattr(self, "inet_rcv_saddr"): - saddr = self.inet_rcv_saddr - else: - saddr = sk_common.skc_rcv_saddr - elif family == socket_module.AF_INET6: - addr_size = 16 - saddr = self.pinet6.saddr - else: - return None - parent_layer = self._context.layers[self.vol.layer_name] - try: - addr_bytes = parent_layer.read(saddr.vol.offset, addr_size) - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to read socket src address from {saddr.vol.offset:#x}" - ) - return None - return socket_module.inet_ntop(family, addr_bytes) - - def get_dst_addr(self): - sk_common = self.sk.__sk_common - family = sk_common.skc_family - if family == socket_module.AF_INET: - if hasattr(self, "daddr") and self.daddr: - daddr = self.daddr - elif hasattr(self, "inet_daddr") and self.inet_daddr: - daddr = self.inet_daddr - else: - daddr = sk_common.skc_daddr - addr_size = 4 - elif family == socket_module.AF_INET6: - if hasattr(self.pinet6, "daddr"): - daddr = self.pinet6.daddr - else: - daddr = sk_common.skc_v6_daddr - addr_size = 16 - else: - return None - parent_layer = self._context.layers[self.vol.layer_name] - try: - addr_bytes = parent_layer.read(daddr.vol.offset, addr_size) - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to read socket dst address from {daddr.vol.offset:#x}" - ) - return None - return socket_module.inet_ntop(family, addr_bytes) - - -class netlink_sock(objects.StructType): - def get_protocol(self): - protocol_idx = self.sk.sk_protocol - if 0 <= protocol_idx < len(linux_constants.NETLINK_PROTOCOLS): - return linux_constants.NETLINK_PROTOCOLS[protocol_idx] - - def get_state(self): - # Return the generic socket state - return self.sk.sk_socket.get_state() - - def get_portid(self): - if self.has_member("pid"): - # kernel < 3.7.10 - return self.pid - if self.has_member("portid"): - # kernel >= 3.7.10 - return self.portid - else: - raise AttributeError("Unable to find a source port id") - - def get_dst_portid(self): - if self.has_member("dst_pid"): - # kernel < 3.7.10 - return self.dst_pid - if self.has_member("dst_portid"): - # kernel >= 3.7.10 - return self.dst_portid - else: - raise AttributeError("Unable to find a destination port id") - - -class vsock_sock(objects.StructType): - def get_protocol(self): - # The protocol should always be 0 for vsocks - return None - - def get_state(self): - # Return the generic socket state - return self.sk.sk_socket.get_state() - - -class packet_sock(objects.StructType): - def get_protocol(self): - eth_proto = socket_module.htons(self.num) - if eth_proto == 0: - return None - elif eth_proto in linux_constants.ETH_PROTOCOLS: - return linux_constants.ETH_PROTOCOLS[eth_proto] - else: - return f"0x{eth_proto:x}" - - def get_state(self): - # Return the generic socket state - return self.sk.sk_socket.get_state() - - -class bt_sock(objects.StructType): - def get_protocol(self): - type_idx = self.sk.sk_protocol - if 0 <= type_idx < len(linux_constants.BLUETOOTH_PROTOCOLS): - return linux_constants.BLUETOOTH_PROTOCOLS[type_idx] - - def get_state(self): - state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(linux_constants.BLUETOOTH_STATES): - return linux_constants.BLUETOOTH_STATES[state_idx] - - -class xdp_sock(objects.StructType): - def get_protocol(self): - # The protocol should always be 0 for xdp_sock - return None - - def get_state(self): - # xdp_sock.state is an enum - return self.state.lookup() - - class bpf_prog(objects.StructType): + _BPF_PROG_CHUNK_SHIFT = 6 + _BPF_PROG_CHUNK_SIZE = 1 << _BPF_PROG_CHUNK_SHIFT + _BPF_PROG_CHUNK_MASK = ~(_BPF_PROG_CHUNK_SIZE - 1) + def get_type(self) -> Union[str, None]: """Returns a string with the eBPF program type""" @@ -2002,8 +2102,11 @@ class bpf_prog(objects.StructType): prog_tag_addr = self.tag.vol.offset prog_tag_size = self.tag.count - prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) + if not vmlinux_layer.is_valid(prog_tag_addr, prog_tag_size): + vollog.debug("Unable to read bpf tag string from 0x%x", prog_tag_addr) + return None + prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) prog_tag = binascii.hexlify(prog_tag_bytes).decode() return prog_tag @@ -2013,7 +2116,62 @@ class bpf_prog(objects.StructType): # 'prog_aux' was added in kernels 3.18 return None - return self.aux.get_name() + try: + return self.aux.get_name() + except exceptions.InvalidAddressException: + return None + + def bpf_jit_binary_hdr_address(self) -> int: + """Return the jitted BPF program start address + Based on bpf_jit_binary_hdr() + + Returns: + The BPF program address + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + # In 5.18 (33c9805860e584b194199cab1a1e81f4e6395408) <= kernels < 6.0 (1d5f82d9dd477d5c66e0214a68c3e4f308eadd6d) + # 'bpf_prog_aux' has a 'use_bpf_prog_pack' member + bpf_prog_aux_has_use_bpf_prog_pack = vmlinux.get_type( + "bpf_prog_aux" + ).has_member("use_bpf_prog_pack") + if bpf_prog_aux_has_use_bpf_prog_pack and self.aux.use_bpf_prog_pack: + long_mask = (1 << vmlinux_layer.bits_per_register) - 1 + addr_mask = self._BPF_PROG_CHUNK_MASK & long_mask + else: + addr_mask = vmlinux_layer.page_mask + + real_start = self.bpf_func + return real_start & addr_mask + + def get_address_region(self) -> Tuple[int, int]: + """Returns the start and end memory addresses of the BPF program. + Based on bpf_get_prog_addr_region() + + Returns: + A tuple with the addresses representing the memory range (start, end) of the BPF program. + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + # Based on bpf_get_prog_addr_region() + bpf_start_address = self.bpf_jit_binary_hdr_address() + + if vmlinux.has_type("bpf_binary_header"): + # kernels >= 3.11 314beb9bcabfd6b4542ccbced2402af2c6f6142a + bpf_binary_header = vmlinux.object( + object_type="bpf_binary_header", offset=bpf_start_address, absolute=True + ) + pages = bpf_binary_header.pages + else: + # kernels < 3.11 The first member is always the size + pages = vmlinux.object( + object_type="unsigned int", offset=bpf_start_address, absolute=True + ) + + bpf_end_address = bpf_start_address + pages * vmlinux_layer.page_size + + return bpf_start_address, bpf_end_address class bpf_prog_aux(objects.StructType): @@ -2023,11 +2181,13 @@ class bpf_prog_aux(objects.StructType): # 'name' was added in kernels 4.15 return None - if not self.name: + try: + if not self.name: + return None + return utility.array_to_string(self.name) + except exceptions.InvalidAddressException: return None - return utility.array_to_string(self.name) - class cred(objects.StructType): # struct cred was added in kernels 2.6.29 @@ -2060,13 +2220,40 @@ class cred(objects.StructType): return int(value) @property - def euid(self): + def uid(self) -> int: + """Returns the real user ID + + Returns: + The real user ID value + """ + return self._get_cred_int_value("uid") + + @property + def gid(self) -> int: + """Returns the real user ID + + Returns: + The real user ID value + """ + return self._get_cred_int_value("gid") + + @property + def euid(self) -> int: """Returns the effective user ID + Returns: + The effective user ID value + """ + return self._get_cred_int_value("euid") + + @property + def egid(self) -> int: + """Returns the effective group ID + Returns: int: the effective user ID value """ - return self._get_cred_int_value("euid") + return self._get_cred_int_value("egid") class kernel_cap_struct(objects.StructType): @@ -2196,7 +2383,7 @@ class kernel_cap_t(kernel_cap_struct): class Timespec64Abstract(abc.ABC): - """Abstract class to handle all required timespec64 operations, convertions and + """Abstract class to handle all required timespec64 operations, conversions and adjustments.""" @classmethod @@ -2292,7 +2479,7 @@ class Timespec64Abstract(abc.ABC): class Timespec64Concrete(Timespec64Abstract): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This is used to dynamically create timespec64-like objects, each with its own variables and the same methods as a timespec64 object extension. """ @@ -2303,7 +2490,7 @@ class Timespec64Concrete(Timespec64Abstract): class timespec64(Timespec64Abstract, objects.StructType): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This works as an extension of the timespec64 object while maintaining the same methods as a Timespec64Concrete object. """ @@ -2378,7 +2565,9 @@ class inode(objects.StructType): else: return None - def _time_member_to_datetime(self, member) -> datetime.datetime: + def _time_member_to_datetime( + self, member + ) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 # Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853 @@ -2441,7 +2630,12 @@ class inode(objects.StructType): """ if not self.i_size: return - elif not (self.i_mapping and self.i_mapping.nrpages > 0): + + if not ( + self.i_mapping + and self.i_mapping.is_readable() + and self.i_mapping.nrpages > 0 + ): return page_cache = linux.PageCache( @@ -2449,19 +2643,26 @@ class inode(objects.StructType): kernel_module_name="kernel", page_cache=self.i_mapping.dereference(), ) + yield from page_cache.get_cached_pages() - def get_contents(self): + def get_contents(self) -> Iterable[Tuple[int, bytes]]: """Get the inode cached pages from the page cache Yields: page_index (int): The page index in the Tree. File offset is page_index * PAGE_SIZE. - page_content (str): The page content + page_content (bytes): The page content """ for page_obj in self.get_pages(): + if page_obj.mapping != self.i_mapping: + vollog.warning( + f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page" + ) + continue page_index = int(page_obj.index) page_content = page_obj.get_content() - yield page_index, page_content + if page_content: + yield page_index, page_content class address_space(objects.StructType): @@ -2469,7 +2670,7 @@ class address_space(objects.StructType): def i_pages(self): """Returns the appropriate member containing the page cache tree""" if self.has_member("i_pages"): - # Kernel >= 4.17 + # Kernel >= 4.17 b93b016313b3ba8003c3b8bb71f569af91f19fc7 return self.member("i_pages") elif self.has_member("page_tree"): # Kernel < 4.17 @@ -2479,16 +2680,22 @@ class address_space(objects.StructType): class page(objects.StructType): - @property - @functools.lru_cache() + def is_valid(self) -> bool: + if self.mapping and not self.mapping.is_readable(): + return False + + if self.to_paddr() < 0: + return False + + return True + + @functools.cached_property def pageflags_enum(self) -> Dict: """Returns 'pageflags' enumeration key/values Returns: A dictionary with the pageflags enumeration key/values """ - # FIXME: It would be even better to use @functools.cached_property instead, - # however, this requires Python +3.8 try: pageflags_enum = self._context.symbol_space.get_enumeration( self.get_symbol_table_name() + constants.BANG + "pageflags" @@ -2502,24 +2709,12 @@ class page(objects.StructType): return pageflags_enum - def get_flags_list(self) -> List[str]: - """Returns a list of page flags + @functools.cached_property + def _intel_vmemmap_start(self) -> int: + """Determine the start of the struct page array, for Intel systems. Returns: - List of page flags - """ - flags = [] - for name, value in self.pageflags_enum.items(): - if self.flags & (1 << value) != 0: - flags.append(name) - - return flags - - def to_paddr(self) -> int: - """Converts a page's virtual address to its physical address using the current physical memory model. - - Returns: - int: page physical address + int: vmemmap_start address """ vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] @@ -2559,14 +2754,40 @@ class page(objects.StructType): "Something went wrong, we shouldn't be here" ) - page_type_size = vmlinux.get_type("page").size + return vmemmap_start + + def _intel_to_paddr(self) -> int: + """Converts a page's virtual address to its physical address using the current Intel memory model. + + Returns: + int: page physical address + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] pagec = vmlinux_layer.canonicalize(self.vol.offset) - pfn = (pagec - vmemmap_start) // page_type_size + pfn = (pagec - self._intel_vmemmap_start) // vmlinux.get_type("page").size page_paddr = pfn * vmlinux_layer.page_size return page_paddr - def get_content(self) -> Union[str, None]: + def to_paddr(self) -> int: + """Converts a page's virtual address to its physical address using the current CPU memory model. + + Returns: + int: page physical address + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + if isinstance(vmlinux_layer, intel.Intel): + page_paddr = self._intel_to_paddr() + else: + raise exceptions.LayerException( + f"Architecture {type(vmlinux_layer)} vmemmap_start calculation isn't currently supported." + ) + + return page_paddr + + def get_content(self) -> Union[bytes, None]: """Returns the page content Returns: @@ -2574,13 +2795,34 @@ class page(objects.StructType): """ vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] - physical_layer = vmlinux.context.layers["memory_layer"] + physical_layer_name = self._context.layers[self.vol.layer_name].config.get( + "memory_layer", self.vol.layer_name + ) + physical_layer = self._context.layers[physical_layer_name] page_paddr = self.to_paddr() if not page_paddr: return None - page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size) - return page_data + if not physical_layer.is_valid(page_paddr, length=vmlinux_layer.page_size): + vollog.debug( + "Unable to read page 0x%x content at 0x%x", self.vol.offset, page_paddr + ) + return None + + return physical_layer.read(page_paddr, vmlinux_layer.page_size) + + def get_flags_list(self) -> List[str]: + """Returns a list of page flags + + Returns: + List of page flags + """ + flags = [] + for name, value in self.pageflags_enum.items(): + if self.flags & (1 << value) != 0: + flags.append(name) + + return flags class IDR(objects.StructType): @@ -2604,7 +2846,7 @@ class IDR(objects.StructType): return (1 << bits) - 1 - def idr_find(self, idr_id: int) -> int: + def idr_find(self, idr_id: int) -> Optional[int]: """Finds an ID within the IDR data structure. Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11 Args: @@ -2616,7 +2858,7 @@ class IDR(objects.StructType): vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( - "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" + "Unsupported IDR implementation, it should be a very very old kernel, probably < 2.6" ) return None @@ -2658,8 +2900,7 @@ class IDR(objects.StructType): id_storage = linux.IDStorage.choose_id_storage( self._context, kernel_module_name="kernel" ) - for page_addr in id_storage.get_entries(root=self.idr_rt): - yield page_addr + yield from id_storage.get_entries(root=self.idr_rt) def get_entries(self) -> Iterable[int]: """Walks the IDR and yield a pointer associated with each element. @@ -2677,22 +2918,23 @@ class IDR(objects.StructType): # Kernels < 4.11 get_entries_func = self._old_kernel_get_entries - for page_addr in get_entries_func(): - yield page_addr + yield from get_entries_func() class rb_root(objects.StructType): - def _walk_nodes(self, root_node) -> Iterator[int]: + def _walk_nodes( + self, root_node: interfaces.objects.ObjectInterface + ) -> Iterator[int]: """Traverses the Red-Black tree from the root node and yields a pointer to each node in this tree. Args: - root_node: A Red-Black tree node from which to start descending + root_node: A Red-Black tree node pointer from which to start descending Yields: A pointer to every node descending from the specified root node """ - if not root_node: + if not (root_node and root_node.is_readable()): return yield root_node @@ -2707,3 +2949,326 @@ class rb_root(objects.StructType): """ yield from self._walk_nodes(root_node=self.rb_node) + + +class scatterlist(objects.StructType): + SG_CHAIN = 0x01 + SG_END = 0x02 + SG_PAGE_LINK_MASK = SG_CHAIN | SG_END + + def _sg_flags(self) -> int: + return self.page_link & self.SG_PAGE_LINK_MASK + + def _sg_is_chain(self) -> int: + return self._sg_flags() & self.SG_CHAIN + + def _sg_is_last(self) -> int: + return self._sg_flags() & self.SG_END + + def _sg_chain_ptr(self) -> int: + """Clears the last two bits basically.""" + return self.page_link & ~self.SG_PAGE_LINK_MASK + + def _sg_dma_len(self) -> int: + # Depends on CONFIG_NEED_SG_DMA_LENGTH + if self.has_member("dma_length"): + return self.dma_length + return self.length + + def _get_sg_max_single_alloc(self) -> int: + """Based on kernel's SG_MAX_SINGLE_ALLOC. + + Doc. from kernel source : + * Maximum number of entries that will be allocated in one piece, if + * a list larger than this is required then chaining will be utilized. + """ + return self._context.layers[self.vol.layer_name].page_size // self.vol.size + + def _sg_next(self) -> Optional[interfaces.objects.ObjectInterface]: + """Get the next scatterlist struct from the list. + Based on kernel's sg_next. + + Doc. from kernel source : + * Notes on SG table design. + * + * We use the unsigned long page_link field in the scatterlist struct to place + * the page pointer AND encode information about the sg table as well. The two + * lower bits are reserved for this information. + * + * If bit 0 is set, then the page_link contains a pointer to the next sg + * table list. Otherwise the next entry is at sg + 1. + * + * If bit 1 is set, then this sg entry is the last element in a list. + """ + if self._sg_is_last(): + return None + + if self._sg_is_chain(): + next_address = self._sg_chain_ptr() + else: + next_address = self.vol.offset + self.vol.size + + sg = self._context.object( + self.get_symbol_table_name() + constants.BANG + "scatterlist", + self.vol.layer_name, + next_address, + ) + return sg + + def for_each_sg(self) -> Optional[Iterator[interfaces.objects.ObjectInterface]]: + """Iterate over each struct in the scatterlist.""" + sg = self + sg_max_single_alloc = self._get_sg_max_single_alloc() + + # Empty scatterlists protection + if sg.page_link == 0 and sg._sg_dma_len() == 0 and sg.dma_address == 0: + return None + else: + # Yield itself first + yield sg + + entries_count = 1 + # entries_count <= sg_max_single_alloc should always be true if the + # scatterlists were correctly chained. + while entries_count <= sg_max_single_alloc: + sg = sg._sg_next() + if sg is None: + break + # Points to a new scatterlist + elif sg._sg_is_chain(): + entries_count = 0 + else: + entries_count += 1 + yield sg + + def get_content( + self, + ) -> Optional[Iterator[bytes]]: + """Traverse a scatterlist to gather content located at each + dma_address position. + + Returns: + An iterator of bytes + """ + # Either "physical" is layer-1 because this is a module layer, or "physical" is the current layer + physical_layer_name = self._context.layers[self.vol.layer_name].config.get( + "memory_layer", self.vol.layer_name + ) + physical_layer = self._context.layers[physical_layer_name] + for sg in self.for_each_sg(): + yield from physical_layer.read(sg.dma_address, sg._sg_dma_len()) + + +class latch_tree_root(objects.StructType): + """Latched RB-trees implementation""" + + @functools.cached_property + def _vmlinux(self): + return linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + + @functools.lru_cache + def _get_type_cached(self, name): + return self._vmlinux.get_type(name) + + def _get_lt_node_from_rb_node( + self, rb_node, index + ) -> Optional[interfaces.objects.ObjectInterface]: + """Gets the latch tree node from the RBTree node. + Based on __lt_from_rb() + """ + # Unfortunately, we cannot use our LinuxUtilities.container_of() here, since the + # member is indexed by the 'index' variable: + # ltn = container_of(node, struct latch_tree_node, node[idx]) + pointer_size = self._get_type_cached("pointer").size + type_dec = self._get_type_cached("latch_tree_node") + member_offset = type_dec.relative_child_offset("node") + index * pointer_size + container_addr = rb_node.vol.offset - member_offset + + return self._vmlinux.object( + object_type="latch_tree_node", offset=container_addr, absolute=True + ) + + def find( + self, key: int, comp_function: Callable + ) -> Optional[interfaces.objects.ObjectInterface]: + """Returns a pointer to the node matching key or None. + + Based on latch_tree_find() and __lt_find() + + Args: + key (int): Typically an address + comp_function: Callback comparison function to provide the order between the + search key and an element. It's works like the kernel's latch_tree_ops::comp + i.e.: comp_function(key, latch_tree_node) + + Returns: + latch_tree_node: A pointer to the node matching key or None. + """ + # latch_tree_root >= 4.2 ade3f510f93a5613b672febe88eff8ea7f1c63b7 + + # Use the lowest sequence bit as an index for picking which data copy to read + if self.seq.has_member("seqcount"): + # kernels >= 5.10 0c9794c8b6781eb7dad8e19b78c5d4557790597a + sequence = self.seq.seqcount.sequence + elif self.seq.has_member("sequence"): + # 4.2 <= kernel < 5.10 + sequence = self.seq.sequence + else: + raise AttributeError("Unsupported sequence type implementation") + + idx = sequence & 1 + + rb_node_ptr = self.tree[idx].rb_node + while rb_node_ptr and rb_node_ptr.is_readable(): + rb_node = rb_node_ptr.dereference() + lt_node = self._get_lt_node_from_rb_node(rb_node, idx) + c = comp_function(key, lt_node) + if c is None: + return None + elif c < 0: + rb_node_ptr = rb_node.rb_left + elif c > 0: + rb_node_ptr = rb_node.rb_right + else: + return lt_node + + return None + + +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 + return (self.vol.offset + off) & long_mask + + def _do_get_name(self) -> str: + if self.has_member("name_offset"): + # kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y + # See 7290d58095712a89f845e1bca05334796dd49ed2 + name_offset = self._offset_to_ptr(self.name_offset) + elif self.has_member("name"): + # kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n + name_offset = self.member("name") + else: + raise AttributeError("Unsupported kernel_symbol type implementation") + + return utility.pointer_to_string( + name_offset, linux_constants.KSYM_NAME_LEN, errors="ignore" + ) + + def get_name(self) -> Optional[str]: + try: + return self._do_get_name() + except exceptions.InvalidAddressException: + return None + + def _do_get_value(self) -> int: + if self.has_member("value_offset"): + # kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y + # See 7290d58095712a89f845e1bca05334796dd49ed2 + return self._offset_to_ptr(self.value_offset) + elif self.has_member("value"): + # kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n + return self.member("value") + + raise AttributeError("Unsupported kernel_symbol type implementation") + + def get_value(self) -> Optional[int]: + try: + return self._do_get_value() + except exceptions.InvalidAddressException: + return None + + def _do_get_namespace(self) -> str: + if self.has_member("namespace_offset"): + # kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y + # See 7290d58095712a89f845e1bca05334796dd49ed2 + namespace_offset = self._offset_to_ptr(self.namespace_offset) + elif self.has_member("namespace"): + # kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n + namespace_offset = self.member("namespace") + else: + raise AttributeError("Unsupported kernel_symbol type implementation") + + return utility.pointer_to_string( + namespace_offset, linux_constants.KSYM_NAME_LEN, errors="ignore" + ) + + def get_namespace(self) -> Optional[str]: + try: + return self._do_get_namespace() + except exceptions.InvalidAddressException: + return None + + +class module_sect_attr(objects.StructType): + def get_name(self) -> Optional[str]: + """ + Performs careful extraction of the section name + The `name` member has changed type and meaning over time + It also was present even in cases with `mattr` present, which + holds the name the kernel uses + """ + if hasattr(self, "battr"): + try: + return utility.pointer_to_string( + self.battr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + # if battr is present then its name attribute needs to be valid + vollog.debug(f"Invalid battr name for section at {self.vol.offset:#x}") + return None + + elif self.name.vol.type_name == "array": + try: + return utility.array_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + # specifically do not return here to give `mattr` a chance + vollog.debug(f"Invalid direct name for section at {self.vol.offset:#x}") + + elif self.name.vol.type_name == "pointer": + try: + return utility.pointer_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + # specifically do not return here to give `mattr` a chance + vollog.debug( + f"Invalid pointer name for section at {self.vol.offset:#x}" + ) + + # if everything else failed... + if hasattr(self, "mattr"): + try: + return utility.pointer_to_string( + self.mattr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unresolvable name for for section at {self.vol.offset:#x}" + ) + + return None + + +class bin_attribute(objects.StructType): + def get_name(self) -> Optional[str]: + """ + Performs extraction of the bin_attribute name + """ + try: + return utility.pointer_to_string( + self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + return None + + @property + def address(self) -> int: + """Equivalent to module_sect_attr.address: + - https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f + """ + return self.private diff --git a/volatility3/framework/symbols/linux/extensions/elf.py b/volatility3/framework/symbols/linux/extensions/elf.py index eadcbbae0..564439c64 100644 --- a/volatility3/framework/symbols/linux/extensions/elf.py +++ b/volatility3/framework/symbols/linux/extensions/elf.py @@ -2,15 +2,11 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Dict, Tuple import logging +from typing import Dict, Optional, Tuple -from volatility3.framework import constants -from volatility3.framework.constants.linux import ( - ELF_IDENT, - ELF_CLASS, -) -from volatility3.framework import objects, interfaces, exceptions +from volatility3.framework import constants, exceptions, interfaces, objects +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -63,13 +59,13 @@ class elf(objects.StructType): ei_class = self._context.object( symbol_table_name + constants.BANG + "unsigned char", layer_name=layer_name, - offset=object_info.offset + ELF_IDENT.EI_CLASS, + offset=object_info.offset + linux_constants.ELF_IDENT.EI_CLASS, ) - if ei_class == ELF_CLASS.ELFCLASS32: + if ei_class == linux_constants.ELF_CLASS.ELFCLASS32: self._type_prefix = "Elf32_" self._ei_class_size = 32 - elif ei_class == ELF_CLASS.ELFCLASS64: + elif ei_class == linux_constants.ELF_CLASS.ELFCLASS64: self._type_prefix = "Elf64_" self._ei_class_size = 64 else: @@ -316,6 +312,8 @@ class elf(objects.StructType): class elf_sym(objects.StructType): """An elf symbol entry""" + _MAX_NAME_LENGTH = linux_constants.KSYM_NAME_LEN + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._cached_strtab = None @@ -328,22 +326,25 @@ class elf_sym(objects.StructType): def cached_strtab(self, cached_strtab): self._cached_strtab = cached_strtab - def get_name(self): - addr = self._cached_strtab + self.st_name + def get_name(self) -> Optional[str]: + """Returns the symbol name""" - # Just get the first 255 characters, it should be enough for a symbol name - name_bytes = self._context.layers[self.vol.layer_name].read(addr, 255, pad=True) - - if name_bytes: - idx = name_bytes.find(b"\x00") - if idx != -1: - name_bytes = name_bytes[:idx] - return name_bytes.decode("utf-8", errors="ignore") - else: - # If we cannot read the name from the address space, - # we return None. + try: + addr = self._cached_strtab + self.st_name + except exceptions.InvalidAddressException: return None + layer = self._context.layers[self.vol.layer_name] + name_bytes = layer.read(addr, self._MAX_NAME_LENGTH, pad=True) + if not name_bytes: + return None + + idx = name_bytes.find(b"\x00") + if idx != -1: + name_bytes = name_bytes[:idx] + + return name_bytes.decode("utf-8", errors="replace") + class elf_phdr(objects.StructType): """An elf program header""" @@ -437,7 +438,7 @@ class elf_linkmap(objects.StructType): def get_name(self): try: buf = self._context.layers.read(self.vol.layer_name, self.l_name, 256) - except exceptions.PagedInvalidAddressException: + except exceptions.InvalidAddressException: # Protection against memory smear vollog.log( constants.LOGLEVEL_VVVV, @@ -448,7 +449,7 @@ class elf_linkmap(objects.StructType): idx = buf.find(b"\x00") if idx != -1: buf = buf[:idx] - return buf.decode() + return buf.decode("utf-8", errors="replace") class_types = { diff --git a/volatility3/framework/symbols/linux/extensions/network.py b/volatility3/framework/symbols/linux/extensions/network.py new file mode 100644 index 000000000..37fa5a41f --- /dev/null +++ b/volatility3/framework/symbols/linux/extensions/network.py @@ -0,0 +1,681 @@ +import logging +from typing import Dict, Generator, List, Optional, Union + +from volatility3.framework import objects, exceptions, renderers, interfaces, constants +from volatility3.framework.objects import utility +from volatility3.framework.constants import linux as linux_constants +from volatility3.framework.symbols import wrappers +from volatility3.framework.symbols import linux +from volatility3.framework.renderers import conversion +import socket as socket_module + + +vollog = logging.getLogger(__name__) + + +class net(objects.StructType): + def get_inode(self) -> int: + """Get the namespace id for this network namespace. + + Raises: + AttributeError: If it cannot find the network namespace id for the + current kernel. + + Returns: + int: the namespace id + """ + if self.has_member("proc_inum"): + # 3.8.13 <= kernel < 3.19.8 + return self.proc_inum + elif self.has_member("ns") and self.ns.has_member("inum"): + # kernel >= 3.19.8 + return self.ns.inum + else: + # kernel < 3.8.13 + raise AttributeError("Unable to find net_namespace inode") + + +class net_device(objects.StructType): + def get_device_name(self) -> str: + """Return the network device name + + Returns: + str: The network device name + """ + return utility.array_to_string(self.name) + + def _format_as_mac_address(self, hwaddr) -> str: + return ":".join([f"{x:02x}" for x in hwaddr[: self.addr_len]]) + + def get_mac_address(self) -> Optional[str]: + """Get the MAC address of this network interface. + + Returns: + str: the MAC address of this network interface. + """ + if self.has_member("perm_addr"): + null_mac_addr_bytes = b"\x00" * self.addr_len + null_mac_addr = self._format_as_mac_address(null_mac_addr_bytes) + mac_addr = self._format_as_mac_address(self.perm_addr) + if mac_addr != null_mac_addr: + return mac_addr + + parent_layer = self._context.layers[self.vol.layer_name] + try: + hwaddr = parent_layer.read(self.dev_addr, self.addr_len, pad=True) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read network interface mac address from {self.dev_addr:#x}" + ) + return None + + return self._format_as_mac_address(hwaddr) + + def _get_flag_choices(self) -> Dict[str, int]: + """Return the net_device flags as a list of strings""" + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + try: + # kernels >= 3.15 + net_device_flags_enum = vmlinux.get_enumeration("net_device_flags") + choices = net_device_flags_enum.choices + except exceptions.SymbolError: + # kernels < 3.15 + choices = linux_constants.NET_DEVICE_FLAGS + + return choices + + def _get_net_device_flag_value( + self, name + ) -> Union[int, interfaces.renderers.BaseAbsentValue]: + """Return the net_device flag value based on the flag name""" + return self._get_flag_choices().get(name, renderers.UnparsableValue()) + + def _get_netdev_state_t(self): + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + try: + # At least from kernels 2.6.30 + return vmlinux.get_enumeration("netdev_state_t") + except exceptions.SymbolError: + raise exceptions.VolatilityException( + "Unsupported kernel or wrong ISF. Cannot find 'netdev_state_t' enumeration" + ) + + def is_running(self) -> bool: + """Test if the network device has been brought up + Based on netif_running() + + Returns: + bool: True if the device is UP + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_START has been available since + # at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_START"]) != 0 + ) + + def is_carrier_ok(self) -> bool: + """Check if carrier is present on network device + Based on netif_carrier_ok() + + Returns: + bool: True if carrier present + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_NOCARRIER has been available + # since at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_NOCARRIER"]) + == 0 + ) + + def is_dormant(self) -> bool: + """Check if the network device is dormant + Based on netif_dormant(() + + Returns: + bool: True if the network device is dormant + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_DORMANT has been available + # since at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_DORMANT"]) != 0 + ) + + def is_operational(self) -> bool: + """Test if the carrier is operational + Based on netif_oper_up() + + Returns: + bool: True if the device is UP + """ + + return self.get_operational_state() in ("UP", "UNKNOWN") + + def get_flag_names(self) -> List[str]: + """Return the net_device flags as a list of strings. + This is the combination of flags exported through kernel APIs to userspace. + Based on dev_get_flags() + + Returns: + List[str]: A list of flag names + """ + choices = self._get_flag_choices() + clear_flags = choices.get("IFF_PROMISC", 0) + clear_flags |= choices.get("IFF_ALLMULTI", 0) + clear_flags |= choices.get("IFF_RUNNING", 0) + clear_flags |= choices.get("IFF_LOWER_UP", 0) + clear_flags |= choices.get("IFF_DORMANT", 0) + + clear_gflags = choices.get("IFF_PROMISC", 0) + clear_gflags |= choices.get("IFF_ALLMULTI)", 0) + + flags = (self.flags & ~clear_flags) | (self.gflags & ~clear_gflags) + + if self.is_running(): + if self.is_operational(): + flags |= choices.get("IFF_RUNNING", 0) + if self.is_carrier_ok(): + flags |= choices.get("IFF_LOWER_UP", 0) + if self.is_dormant(): + flags |= choices.get("IFF_DORMANT", 0) + + net_device_flags_enum_flags = wrappers.Flags(choices) + net_device_flags = net_device_flags_enum_flags(flags) + + # It's preferable to provide a deterministic list of items. i.e. for testing + return sorted(net_device_flags) + + @property + def promisc(self) -> bool: + """Return if this network interface is in promiscuous mode. + + Returns: + bool: True if this network interface is in promiscuous mode. Otherwise, False + """ + return self.flags & self._get_net_device_flag_value("IFF_PROMISC") != 0 + + def _do_get_net_namespace_id(self) -> int: + """Return the network namespace id for this network interface. + + Returns: + int: the network namespace id for this network interface + """ + nd_net = self.nd_net + if nd_net.has_member("net"): + # In kernel 4.1.52 the 'nd_net' member type was changed from + # 'struct net*' to 'possible_net_t' which has a 'struct net *net' member. + net_ns_id = nd_net.net.get_inode() + else: + # In kernels < 4.1.52 the 'nd_net'member type was 'struct net*' + net_ns_id = nd_net.get_inode() + + return net_ns_id + + def get_net_namespace_id(self) -> Optional[int]: + """Return the network namespace id for this network interface. + + Returns: + int: the network namespace id for this network interface + """ + try: + return self._do_get_net_namespace_id() + except exceptions.InvalidAddressException: + vollog.debug( + f"Encountered an invalid address exception when getting the namespace for {self.vol.offset:#x}" + ) + return None + + def get_operational_state(self) -> Union[str, interfaces.renderers.BaseAbsentValue]: + """Return the netwok device oprational state (RFC 2863) string + + Returns: + str: A string with the operational state + """ + try: + return linux_constants.IF_OPER_STATES(self.operstate).name + except ValueError: + vollog.warning(f"Invalid net_device operational state '{self.operstate}'") + return renderers.UnparsableValue() + + def get_qdisc_name(self) -> Optional[str]: + """Return the network device queuing discipline (qdisc) name + + Returns: + str: A string with the queuing discipline (qdisc) name + """ + try: + return utility.array_to_string(self.qdisc.ops.id) + except exceptions.InvalidAddressException: + vollog.debug(f"Unable to get qdisc name for {self.vol.offset:#x}") + return None + + def get_queue_length(self) -> int: + """Return the network device transmission queue length (qlen) + + Returns: + int: the network device transmission queue length (qlen) + """ + return self.tx_queue_len + + +class in_device(objects.StructType): + def get_addresses( + self, max_devices=128 + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Yield the IPv4 ifaddr addresses + + Yields: + in_ifaddr: An IPv4 ifaddr address + """ + seen = set() + + try: + cur = self.ifa_list + except exceptions.InvalidAddressException: + return + + while cur and cur.vol.offset: + if len(seen) > max_devices: + break + + if cur.vol.offset in seen: + break + seen.add(cur.vol.offset) + + yield cur + + try: + cur = cur.ifa_next + except exceptions.InvalidAddressException: + break + + +class inet6_dev(objects.StructType): + def get_addresses( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Yield the IPv6 ifaddr addresses + + Yields: + inet6_ifaddr: An IPv6 ifaddr address + """ + if not self.has_member( + "addr_list" + ) or not self.addr_list.vol.type_name.endswith(constants.BANG + "list_head"): + # kernels < 3.0 + # FIXME: struct inet6_ifaddr *addr_list; + vollog.warning( + "IPv6 is unsupported for this kernel. Check if the ISF contains the appropriate 'inet6_dev' type" + ) + return + + symbol_space = self._context.symbol_space + table_name = self.get_symbol_table_name() + inet6_ifaddr_symname = table_name + constants.BANG + "inet6_ifaddr" + if not symbol_space.has_type(inet6_ifaddr_symname) or not symbol_space.get_type( + inet6_ifaddr_symname + ).has_member("if_list"): + vollog.warning( + "IPv6 is unsupported for this kernel. Check if the ISF contains the appropriate 'inet6_ifaddr' type" + ) + return + + # 'if_list' member was added to 'inet6_ifaddr' type in kernels 3.0 + yield from self.addr_list.to_list(inet6_ifaddr_symname, "if_list") + + +class in_ifaddr(objects.StructType): + # Translation to text based on iproute2 package. See 'rtnl_rtscope_tab' in lib/rt_names.c + _rtnl_rtscope_tab = { + "RT_SCOPE_UNIVERSE": "global", + "RT_SCOPE_NOWHERE": "nowhere", + "RT_SCOPE_HOST": "host", + "RT_SCOPE_LINK": "link", + "RT_SCOPE_SITE": "site", + } + + def get_scope_type(self) -> str: + """Get the scope type for this IPv4 address + + Returns: + str: the IPv4 scope type. + """ + table_name = self.get_symbol_table_name() + rt_scope_enum = self._context.symbol_space.get_enumeration( + table_name + constants.BANG + "rt_scope_t" + ) + try: + rt_scope = rt_scope_enum.lookup(self.ifa_scope) + except ValueError: + return "unknown" + + return self._rtnl_rtscope_tab.get(rt_scope, "unknown") + + def get_address(self) -> str: + """Get an string with the IPv4 address + + Returns: + str: the IPv4 address + """ + return conversion.convert_ipv4(self.ifa_address) + + def get_prefix_len(self) -> int: + """Get the IPv4 address prefix len + + Returns: + int: the IPv4 address prefix len + """ + return self.ifa_prefixlen + + +class inet6_ifaddr(objects.StructType): + def get_scope_type(self) -> str: + """Get the scope type for this IPv6 address + + Returns: + str: the IPv6 scope type. + """ + if (self.scope & linux_constants.IFA_HOST) != 0: + return "host" + elif (self.scope & linux_constants.IFA_LINK) != 0: + return "link" + elif (self.scope & linux_constants.IFA_SITE) != 0: + return "site" + else: + return "global" + + def get_address(self) -> str: + """Get an string with the IPv6 address + + Returns: + str: the IPv6 address + """ + return conversion.convert_ipv6(self.addr.in6_u.u6_addr32) + + def get_prefix_len(self) -> int: + """Get the IPv6 address prefix len + + Returns: + int: the IPv6 address prefix len + """ + return self.prefix_len + + +class socket(objects.StructType): + def _get_vol_kernel(self) -> interfaces.context.ModuleInterface: + symbol_table_arr = self.vol.type_name.split("!", 1) + symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None + + if symbol_table is None: + raise ValueError(f"No module using the symbol table {symbol_table}") + + module_names = list( + self._context.modules.get_modules_by_symbol_tables(symbol_table) + ) + if not module_names: + raise ValueError(f"No module using the symbol table {symbol_table}") + kernel_module_name = module_names[0] + kernel = self._context.modules[kernel_module_name] + return kernel + + def get_inode(self) -> int: + try: + kernel = self._get_vol_kernel() + except ValueError: + return 0 + socket_alloc = linux.LinuxUtilities.container_of( + self.vol.offset, "socket_alloc", "socket", kernel + ) + if socket_alloc is None: + return 0 + vfs_inode = socket_alloc.vfs_inode + + return vfs_inode.i_ino + + def get_state(self) -> str: + socket_state_idx = self.state + if 0 <= socket_state_idx < len(linux_constants.SOCKET_STATES): + return linux_constants.SOCKET_STATES[socket_state_idx] + return "Unknown socket state" + + +class sock(objects.StructType): + def get_family(self) -> str: + family_idx = self.__sk_common.skc_family + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] + return "Unknown socket family" + + def get_type(self) -> str: + return linux_constants.SOCK_TYPES.get(self.sk_type, "") + + def get_inode(self) -> int: + if not self.sk_socket: + return 0 + return self.sk_socket.get_inode() + + def get_protocol(self) -> Optional[str]: + return None + + def get_state(self) -> str: + # Return the generic socket state + if self.has_member("sk"): + return self.sk.sk_socket.get_state() + return self.sk_socket.get_state() + + +class unix_sock(objects.StructType): + def get_name(self) -> Optional[str]: + if not self.addr: + return None + sockaddr_un = self.addr.name.cast("sockaddr_un") + saddr = str(utility.array_to_string(sockaddr_un.sun_path)) + return saddr + + def get_protocol(self) -> Optional[str]: + return None + + def get_state(self) -> str: + """Return a string representing the sock state.""" + + # Unix socket states reuse (a subset) of the inet_sock states contants + if self.sk.get_type() == "STREAM": + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] + else: + return "Unknown unix_sock stream state" + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_inode(self) -> int: + return self.sk.get_inode() + + +class inet_sock(objects.StructType): + def get_family(self) -> str: + family_idx = self.sk.__sk_common.skc_family + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] + return "Unknown inet_sock family" + + def get_protocol(self) -> Optional[str]: + # If INET6 family and a proto is defined, we use that specific IPv6 protocol. + # Otherwise, we use the standard IP protocol. + protocol = linux_constants.IP_PROTOCOLS.get(self.sk.sk_protocol) + if self.get_family() == "AF_INET6": + protocol = linux_constants.IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) + return protocol + + def get_state(self) -> str: + """Return a string representing the sock state.""" + + if self.sk.get_type() == "STREAM": + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] + else: + return "Unknown inet_sock stream state" + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_src_port(self) -> Optional[int]: + sport_le = getattr(self, "sport", getattr(self, "inet_sport", None)) + if sport_le is not None: + return socket_module.htons(sport_le) + return None + + def get_dst_port(self) -> Optional[int]: + sk_common = self.sk.__sk_common + if hasattr(sk_common, "skc_portpair"): + dport_le = sk_common.skc_portpair & 0xFFFF + elif hasattr(self, "dport"): + dport_le = self.dport + elif hasattr(self, "inet_dport"): + dport_le = self.inet_dport + elif hasattr(sk_common, "skc_dport"): + dport_le = sk_common.skc_dport + else: + return None + return socket_module.htons(dport_le) + + def get_src_addr(self) -> Optional[str]: + sk_common = self.sk.__sk_common + family = sk_common.skc_family + if family == socket_module.AF_INET: + addr_size = 4 + if hasattr(self, "rcv_saddr"): + saddr = self.rcv_saddr + elif hasattr(self, "inet_rcv_saddr"): + saddr = self.inet_rcv_saddr + else: + saddr = sk_common.skc_rcv_saddr + elif family == socket_module.AF_INET6: + addr_size = 16 + saddr = self.pinet6.saddr + else: + return None + parent_layer = self._context.layers[self.vol.layer_name] + try: + addr_bytes = parent_layer.read(saddr.vol.offset, addr_size) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read socket src address from {saddr.vol.offset:#x}" + ) + return None + return socket_module.inet_ntop(family, addr_bytes) + + def get_dst_addr(self) -> Optional[str]: + sk_common = self.sk.__sk_common + family = sk_common.skc_family + if family == socket_module.AF_INET: + if hasattr(self, "daddr") and self.daddr: + daddr = self.daddr + elif hasattr(self, "inet_daddr") and self.inet_daddr: + daddr = self.inet_daddr + else: + daddr = sk_common.skc_daddr + addr_size = 4 + elif family == socket_module.AF_INET6: + if hasattr(self.pinet6, "daddr"): + daddr = self.pinet6.daddr + else: + daddr = sk_common.skc_v6_daddr + addr_size = 16 + else: + return None + parent_layer = self._context.layers[self.vol.layer_name] + try: + addr_bytes = parent_layer.read(daddr.vol.offset, addr_size) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read socket dst address from {daddr.vol.offset:#x}" + ) + return None + return socket_module.inet_ntop(family, addr_bytes) + + +class netlink_sock(objects.StructType): + def get_protocol(self) -> str: + protocol_idx = self.sk.sk_protocol + if 0 <= protocol_idx < len(linux_constants.NETLINK_PROTOCOLS): + return linux_constants.NETLINK_PROTOCOLS[protocol_idx] + return "Unknown netlink_sock protocol" + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_portid(self) -> int: + if self.has_member("pid"): + # kernel < 3.7.10 + return self.pid + if self.has_member("portid"): + # kernel >= 3.7.10 + return self.portid + else: + raise AttributeError("Unable to find a source port id") + + def get_dst_portid(self) -> int: + if self.has_member("dst_pid"): + # kernel < 3.7.10 + return self.dst_pid + if self.has_member("dst_portid"): + # kernel >= 3.7.10 + return self.dst_portid + else: + raise AttributeError("Unable to find a destination port id") + + +class vsock_sock(objects.StructType): + def get_protocol(self): + # The protocol should always be 0 for vsocks + return None + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + +class packet_sock(objects.StructType): + def get_protocol(self) -> Optional[str]: + eth_proto = socket_module.htons(self.num) + if eth_proto == 0: + return None + elif eth_proto in linux_constants.ETH_PROTOCOLS: + return linux_constants.ETH_PROTOCOLS[eth_proto] + else: + return f"0x{eth_proto:x}" + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + +class bt_sock(objects.StructType): + def get_protocol(self) -> Optional[str]: + type_idx = self.sk.sk_protocol + if 0 <= type_idx < len(linux_constants.BLUETOOTH_PROTOCOLS): + return linux_constants.BLUETOOTH_PROTOCOLS[type_idx] + return None + + def get_state(self) -> Optional[str]: + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(linux_constants.BLUETOOTH_STATES): + return linux_constants.BLUETOOTH_STATES[state_idx] + return None + + +class xdp_sock(objects.StructType): + def get_protocol(self): + # The protocol should always be 0 for xdp_sock + return None + + def get_state(self): + # xdp_sock.state is an enum + return self.state.lookup() diff --git a/volatility3/framework/symbols/linux/kallsyms.py b/volatility3/framework/symbols/linux/kallsyms.py new file mode 100644 index 000000000..be026fc4e --- /dev/null +++ b/volatility3/framework/symbols/linux/kallsyms.py @@ -0,0 +1,1754 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import dataclasses +import functools +import logging +from typing import Iterator, List, Optional, Tuple + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.constants import linux as linux_constants +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +@dataclasses.dataclass +class KASConfig: + """Kallsyms configuration class""" + + num_syms_address: int + names_address: int + token_table_address: int + token_index_address: int + offsets_address: int + relative_base_address: int + _stext: int + + # Usually not in VMCOREINFO, these are found during the bootstrap stage. + # If an ISF is available, they are fetched from there instead. + markers_address: int = None + addresses_address: int = None + _sinittext: int = None + _einittext: int = None + _etext: int = None + _end: int = None + mod_tree: int = None + module_addr_min: int = None + module_addr_max: int = None + start_ksymtab: int = None + stop_ksymtab: int = None + bpf_tree_address: int = None + seqs_of_names_address: int = None + + num_syms_type_size: int = None + markers_type_size: int = None + kernel_symbol_size: int = None + + @classmethod + def _get_symbol_address(cls, context, layer_name, module_name, symbol_name): + vmlinux = context.modules[module_name] + if not vmlinux.has_symbol(symbol_name): + return None + + layer = context.layers[layer_name] + address = vmlinux.get_symbol(symbol_name).address + address += layer.config["kernel_virtual_offset"] + return address + + @classmethod + def new_from_isf(cls, context, layer_name, module_name): + vmlinux = context.modules[module_name] + + # kallsyms_num_syms and kallsyms_markers types were updated from a unsigned long + # to unsigned int in 4.20 80ffbaa5b1bd98e80e3239a3b8cfda2da433009a + num_syms_type_size = vmlinux.get_symbol("kallsyms_num_syms").type.size + kernel_symbol_size = vmlinux.get_type("kernel_symbol").size + + def get_symbol_address(symbol_name): + return cls._get_symbol_address( + context, layer_name, module_name, symbol_name + ) + + kas_config = KASConfig( + num_syms_address=get_symbol_address("kallsyms_num_syms"), + names_address=get_symbol_address("kallsyms_names"), + token_table_address=get_symbol_address("kallsyms_token_table"), + token_index_address=get_symbol_address("kallsyms_token_index"), + offsets_address=get_symbol_address("kallsyms_offsets"), + relative_base_address=get_symbol_address("kallsyms_relative_base"), + markers_address=get_symbol_address("kallsyms_markers"), + addresses_address=get_symbol_address("kallsyms_addresses"), + _sinittext=get_symbol_address("_sinittext"), + _einittext=get_symbol_address("_einittext"), + _stext=get_symbol_address("_stext"), + _etext=get_symbol_address("_etext"), + _end=get_symbol_address("_end"), + mod_tree=get_symbol_address("mod_tree"), + module_addr_min=get_symbol_address("module_addr_min"), + module_addr_max=get_symbol_address("module_addr_max"), + start_ksymtab=get_symbol_address("__start___ksymtab"), + stop_ksymtab=get_symbol_address("__stop___ksymtab"), + bpf_tree_address=get_symbol_address("bpf_tree"), + seqs_of_names_address=get_symbol_address("kallsyms_seqs_of_names"), + num_syms_type_size=num_syms_type_size, + markers_type_size=num_syms_type_size, + kernel_symbol_size=kernel_symbol_size, + ) + return kas_config + + +class _KallsymsIO: + """Helper to interpret a memory address as a file pointer. + + For internal use within the Kallsyms API; external use is discouraged. + """ + + def __init__( + self, + context: interfaces.context.ContextInterface, + layer_name: str, + base=0, + endian="little", + ): + self._context = context + self._layer_name = layer_name + self._base = base + self._position = base + self._endian = endian + + def read(self, size: int) -> bytes: + """Return 'size' bytes from the current position""" + layer = self._context.layers[self._layer_name] + buf = layer.read(offset=self._position, length=size) + self._position += size + return buf + + def read_str(self, size: int) -> str: + """Returns 'size' bytes as a string from the current position.""" + return self.read(size).decode() + + def read_int(self, size: int, signed: bool = False) -> int: + """Returns the integer stored in the current position using 'size' bytes. + Args: + size: Number of bytes to use for the int. + signed: Integer sign. + + Returns: + The integer stored in the current position. + """ + return int.from_bytes( + self.read(size), + byteorder=self._endian, + signed=signed, + ) + + def seek(self, offset: int) -> None: + """Seek the pointer to the given offset, based on the base address. + + Args: + offset: offset from the base address + """ + self._position = self._base + offset + + +@dataclasses.dataclass +class KASSymbolBasic: + name: str + type: str + + +@dataclasses.dataclass +class KASSymbol(KASSymbolBasic): + address: int + size: int + module_name: str + exported: bool = False + subsystem: str = None + + def __str__(self): + return ( + f"name:{self.name}, type:{self.type}, address:{self.address:#x}, " + f"size:{self.size}, exported:{self.exported}, subsystem:{self.subsystem}" + ) + + def set_exported_from_type(self) -> None: + """Updates the 'export' member based on the symbol's type. + + This method evaluates the symbol's type and sets the 'export' member + to indicate whether the object is exported. This code and Linux kernel follows + the nm symbol type logic. + """ + # As per the "nm" man page: + # If lowercase, the symbol is usually local; if uppercase, the symbol is + # global (external). There are however a few lowercase symbols that are shown + # for special global symbols ("u", "v" and "w"). + if self.type: + self.exported = bool(self.type.isupper() or self.type in ("u", "v", "w")) + else: + self.exported = None + + @functools.cached_property + def type_description(self) -> Optional[str]: + """Returns the interpreted meaning of the symbol type based on the nm tool. + + Returns: + A string with the type description. + """ + # If a symbol type exists with the original case, get it + symbol_type_description = linux_constants.NM_TYPES_DESC.get(self.type, None) + if symbol_type_description: + return symbol_type_description + + if self.type: + # Otherwise, use the lowercase version + symbol_type_description = linux_constants.NM_TYPES_DESC.get( + self.type.lower(), None + ) + + return symbol_type_description + + +@dataclasses.dataclass +class KASFilter: + name: str + type: str + + +class Kallsyms(interfaces.configuration.VersionableInterface): + """Kallsyms API class""" + + _required_framework_version = (2, 19, 0) + _version = (1, 0, 0) + + # Internal kernel core constants + _CORE_SUBSYSTEM_NAME = "core" + _CORE_MODULE_NAME = "kernel" + + # Internal module constants + _MODULE_SUBSYSTEM_NAME = "module" + + # Internal FTrace constants + _FTRACE_SUBSYSTEM_NAME = "ftrace" + _FTRACE_MODULE_SYM_TYPE = "T" + _FTRACE_TRAMPOLINE_MODULE_NAME = "__builtin__ftrace" + _FTRACE_TRAMPOLINE_SYM = "ftrace_trampoline" + _FTRACE_TRAMPOLINE_SYM_TYPE = "t" + + # Internal BPF constants + _BPF_SUBSYSTEM_NAME = "bpf" + _BPF_MODULE_NAME = "bpf" + _BPF_SYM_TYPE = "t" + + def __init__( + self, + context: interfaces.context.ContextInterface, + layer_name: str, + module_name: str, + kas_config: KASConfig = None, + progress_callback: constants.ProgressCallback = None, + ) -> None: + """Initialize the Kallsyms API + + Args: + context: The context used to access memory layers and symbols + layer_name: The name of layer within the context in which the module exists + module_name: The name of the kernel module on which to operate + kas_config: The KAllSyms configuration + progress_callback: Method that is called periodically during scanning to + update progress + """ + super().__init__() + + self._assert_versions() + + self._context = context + self._layer_name = layer_name + self._module_name = module_name + self._kas_config = kas_config + self._progress_callback = progress_callback + if progress_callback and not callable(progress_callback): + raise TypeError("Progress_callback is not callable") + + if not kas_config: + self._kas_config = KASConfig.new_from_isf( + context=context, + layer_name=layer_name, + module_name=module_name, + ) + + layer = self._context.layers[self._layer_name] + # FIXME: The layer lacks this information. Could there be a better alternative? + self._endian = "little" if layer._entry_format[0] == "<" else "big" + self._long_size = layer.bits_per_register // 8 + + self._kallsyms_num_syms = None + self._kallsyms_relative_base = None + + self._kallsyms_token_index_address = None + self._kallsyms_offsets_address = None + self._kallsyms_names_io = _KallsymsIO( + context=self._context, + layer_name=self._layer_name, + base=self._kas_config.names_address, + endian=self._endian, + ) + + self._kallsyms_token_table_io = _KallsymsIO( + context=self._context, + layer_name=self._layer_name, + base=self._kas_config.token_table_address, + endian=self._endian, + ) + + self._bootstrap() + + @classmethod + def _assert_versions(cls) -> None: + """Verify versions of shared dependencies""" + linux_utilities_modules_version_required = (3, 0, 0) + if not requirements.VersionRequirement.matches_required( + linux_utilities_modules_version_required, + linux_utilities_modules.Modules.version, + ): + raise exceptions.VolatilityException( + "linux_utilities_modules.Modules version not suitable: " + f"required {linux_utilities_modules_version_required} found {linux_utilities_modules.Modules.version}", + ) + + return None + + def _read_bytes(self, address: int, size: int) -> Optional[bytes]: + layer = self._context.layers[self._layer_name] + try: + return layer.read(address, size).decode() + except exceptions.InvalidAddressException: + return None + + def _read_int(self, address: int, size: int, signed: bool = False) -> Optional[int]: + layer = self._context.layers[self._layer_name] + try: + return int.from_bytes( + layer.read(address, size), + byteorder=self._endian, + signed=signed, + ) + except exceptions.InvalidAddressException: + return None + + def _bootstrap(self) -> None: + layer = self._context.layers[self._layer_name] + # kallsyms_num_syms and kallsyms_markers[] types were updated from a unsigned long + # to unsigned int in 4.20 80ffbaa5b1bd98e80e3239a3b8cfda2da433009a + self._kallsyms_num_syms = self._read_int( + self._kas_config.num_syms_address, + self._kas_config.num_syms_type_size, + signed=False, + ) + + if self._kas_config.relative_base_address: + # kernels >= 4.6 + self._kallsyms_relative_base = ( + self._read_int( + self._kas_config.relative_base_address, + self._long_size, + signed=False, + ) + & layer.address_mask + ) + + self._kallsyms_offsets_address = self._kas_config.offsets_address + self._kallsyms_token_index_address = self._kas_config.token_index_address + + # Preload the kallsyms_token_index array + short_size = 2 + self._kallsyms_token_index = [ + self._read_int( + self._kallsyms_token_index_address + index * short_size, + short_size, + signed=False, + ) + for index in range(256) + ] + + def _get_symbol( + self, + offset, + index, + filters: List[KASFilter] = None, + ) -> Optional[Tuple[KASSymbol, int]]: + kassymbolbasic, compressed_length = self._expand_symbol(offset, filters) + kassymbol = None + if kassymbolbasic: + sym_addr = self._get_symbol_address_by_index(index=index) + _, sym_size = self._get_symbol_pos(sym_addr) + + kassymbol = KASSymbol( + name=kassymbolbasic.name, + type=kassymbolbasic.type, + address=sym_addr, + size=sym_size, + module_name=self._CORE_MODULE_NAME, + subsystem=self._CORE_SUBSYSTEM_NAME, + ) + kassymbol.set_exported_from_type() + return kassymbol, compressed_length + + def get_core_symbols( + self, + progress_callback: constants.ProgressCallback = None, + ) -> Iterator[KASSymbol]: + """Yield each kernel core symbol + + Args: + progress_callback: Method that is called periodically during scanning to + update progress + + Based on kallsyms_on_each_symbol() + + Yields: + KASSymbol objects + """ + current_offset = 0 + for sym_idx in range(self._kallsyms_num_syms): + try: + kassymbol, compressed_length = self._get_symbol(current_offset, sym_idx) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to reconstruct core symbol at offset {current_offset:#x} and index {sym_idx}" + ) + continue + + if compressed_length is None: + vollog.debug( + f"Unable to reconstruct compressed_length at offset {current_offset:#x} and index {sym_idx}" + ) + break + + if kassymbol: + yield kassymbol + + if progress_callback: + progress_callback( + (sym_idx / self._kallsyms_num_syms) * 100, + "Populating Kallsyms core symbols", + ) + + current_offset += compressed_length + 1 + + def _expand_symbol( + self, + offset: int, + filters: List[KASFilter] = None, + ) -> Tuple[KASSymbolBasic, int]: + """Expand a compressed symbol using its offset in the stream + Based on kallsyms_expand_symbol() + + Args: + offset: Symbol offset in the kallsyms arrays. + filters: List of KASFilter filters + + Returns: + A tuple with a KASSymbolBasic object and the symbol name's compressed length. + """ + filters = filters if filters is not None else [] + type_filters = tuple(kassymbolfilter.type for kassymbolfilter in filters) + + self._kallsyms_names_io.seek(offset) + # The compressed symbol length is in the first byte + compressed_length = self._kallsyms_names_io.read_int(size=1) + if compressed_length & 0x80 != 0: + # kernels >= 6.1 73bbb94466fd3f8b313eeb0b0467314a262dddb3 + # MSB 1 means a 'big' symbol, we need an extra byte + lower_byte = compressed_length + upper_byte = self._kallsyms_names_io.read_int(size=1) + compressed_length = (upper_byte << 7) | (lower_byte & 0x7F) + + abort_decompression = False + sym_type = None + sym_name = "" + for _ in range(compressed_length): + token_index_index = self._kallsyms_names_io.read_int(size=1) + token_index = self._kallsyms_token_index[token_index_index] + self._kallsyms_token_table_io.seek(token_index) + token = self._kallsyms_token_table_io.read_str(1) + while token != "\x00": + if not sym_type: + sym_type = token + # We got the symbol type, we can abort this immediatelly + if type_filters and sym_type not in type_filters: + abort_decompression = True + break + else: + sym_name += token + for kassymbolfilter in filters: + if kassymbolfilter.type is not None: + if ( + sym_type == kassymbolfilter.type + and kassymbolfilter.name.startswith(sym_name) + ): + break + elif kassymbolfilter.name.startswith(sym_name): + break + + else: + if filters: + abort_decompression = True + + token = self._kallsyms_token_table_io.read_str(1) + + if abort_decompression: + break + + kassymbolbasic = ( + KASSymbolBasic(name=sym_name, type=sym_type) + if not abort_decompression + else None + ) + return kassymbolbasic, compressed_length + + def _get_symbol_address_by_index(self, index: int) -> Optional[int]: + """Return symbol address based on the symbol index in the kallsyms arrays. + Based on kallsyms_sym_address() + + Args: + index: Symbol index + + Returns: + Symbol address + """ + layer = self._context.layers[self._layer_name] + if self._kallsyms_offsets_address: + # kernels >= 4.6 - Addresses are relative to kallsyms_relative_base + # It assumes: CONFIG_KALLSYMS_BASE_RELATIVE=y and CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y + signed_int_size = 4 + sym_offset_ptr = self._kallsyms_offsets_address + (index * signed_int_size) + sym_addr = self._read_int(sym_offset_ptr, signed_int_size, signed=True) + if sym_addr is None: + return None + + if sym_addr < 0: + # Negative offsets are relative to kallsyms_relative_base - 1 + return self._kallsyms_relative_base - 1 - sym_addr + + # Positive offsets are absolute values + return sym_addr & layer.address_mask + elif self._kas_config.addresses_address: + # kernels < 4.6 - Addresses are absolute + # unsigned long kallsyms_addresses[] + kallsyms_address = self._read_int( + self._kas_config.addresses_address + (index * self._long_size), + self._long_size, + signed=False, + ) + if kallsyms_address is None: + return None + + return kallsyms_address & layer.address_mask + else: + raise exceptions.VolatilityException("Unsupported kernel") + + @functools.lru_cache + def _get_symbol_pos(self, address: int) -> Optional[Tuple[int, int]]: + """Returns the symbol position in the kallsyms arrays and its size.""" + low = 0 + high = self._kallsyms_num_syms + + while high - low > 1: + mid = low + (high - low) // 2 + symbol_index = self._get_symbol_address_by_index(mid) + if symbol_index is None: + return None, None + elif symbol_index <= address: + low = mid + else: + high = mid + + # prevent accidental bleed through + symbol_index = None + + # Search for the first aliased symbol. *Aliased symbols* are symbols with the same address. + while low: + symbol_index = self._get_symbol_address_by_index(low - 1) + if symbol_index is None: + return None, None + + if symbol_index == self._get_symbol_address_by_index(low): + low -= 1 + else: + break + + symbol_start = self._get_symbol_address_by_index(low) + if symbol_start is None: + return None, None + + symbol_end = 0 + + # Search for next non-aliased symbol. + for idx in range(low + 1, self._kallsyms_num_syms): + symbol_index = self._get_symbol_address_by_index(idx) + if symbol_index is None: + return None, None + + if symbol_index > symbol_start: + symbol_end = self._get_symbol_address_by_index(idx) + break + + # pylint: disable=protected-access + # If no next symbol is found, we default to using the end of the section + if not symbol_end: + if self._is_kernel_inittext(address): + symbol_end = self._kas_config._einittext + elif self._kas_config._end is not None: + # Assume CONFIG_KALLSYMS_ALL=y. Otherwise, symbol_end will be _etext + symbol_end = self._kas_config._end + else: + symbol_end = self._kas_config._etext + + symbol_size = symbol_end - symbol_start + + return low, symbol_size + + @functools.lru_cache + def _get_symbol_offset(self, index: int) -> int: + """Find the offset on the compressed stream given the index in the kallsyms array. + + Based on get_symbol_offset + + Returns: + Offset on the compressed stream + """ + + # Use the nearest marker, placed every 256 positions + kallsyms_markers_pos_ptr = ( + self._kas_config.markers_address + + (index >> 8) * self._kas_config.markers_type_size + ) + kallsyms_markers_pos = self._read_int( + kallsyms_markers_pos_ptr, self._kas_config.markers_type_size, signed=False + ) + name_addr = self._kas_config.names_address + kallsyms_markers_pos + + # Scan symbols sequentially until the target. Each symbol uses a + # [][ bytes of data] format, so we skip symbols by adding their length + # to the pointer value. + for _ in range(index & 0xFF): + compressed_length = self._read_int(name_addr, 1) + if compressed_length & 0x80 != 0: + # kernels >= 6.1 73bbb94466fd3f8b313eeb0b0467314a262dddb3 + # MSB 1 means a 'big' symbol, we need an extra byte + lower_byte = compressed_length + upper_byte = self._kallsyms_names_io.read_int(size=1) + compressed_length = (upper_byte << 7) | (lower_byte & 0x7F) + + name_addr += compressed_length + 1 + + return name_addr - self._kas_config.names_address + + def _is_kernel_inittext(self, addr: int) -> bool: + # pylint: disable=protected-access + if not (self._kas_config._sinittext and self._kas_config._einittext): + # We don't know + return False + + return self._kas_config._sinittext <= addr < self._kas_config._einittext + + def _is_kernel_text(self, addr: int) -> bool: + # pylint: disable=protected-access + return self._kas_config._stext <= addr < self._kas_config._etext + + def _is_core_ksym_addr(self, addr: int) -> bool: + return self._is_kernel_text(addr) or self._is_kernel_inittext(addr) + + def lookup_address(self, address: int) -> Optional[KASSymbol]: + """Search for a symbol by its memory address. + + This function scans kernel core, module symbols, BPF symbols, and Ftrace symbols + to locate the first symbol matching the specified address. Note that multiple + symbols (aliased symbols) can share the same memory address, so this method + returns the first match found. + + Based on kallsyms_lookup. + + Args: + address: The memory address to search for. + + Returns: + The matching symbol if found, or None if no match is found. + """ + layer = self._context.layers[self._layer_name] + address &= layer.address_mask + + kassymbol = self.core_lookup_address(address) + if not kassymbol: + kassymbol = self.module_lookup_address(address) + + if not kassymbol: + kassymbol = self.bpf_lookup_address(address) + + if not kassymbol: + kassymbol = self.ftrace_lookup_address(address) + + return kassymbol + + def core_lookup_address(self, address: int) -> Optional[KASSymbol]: + """Search for a symbol by its memory address within the kernel core. + + Based on kallsyms_lookup_buildid. + + Args: + address: The memory address to search for. + + Returns: + The matching symbol if found, or None if no match is found. + """ + layer = self._context.layers[self._layer_name] + address &= layer.address_mask + + if not self._is_core_ksym_addr(address): + return None + + pos, sym_size = self._get_symbol_pos(address) + if pos is None: + return None + offset = self._get_symbol_offset(pos) + sym_address = self._get_symbol_address_by_index(pos) + kassymbolbasic, _compressed_length = self._expand_symbol(offset) + + if not kassymbolbasic: + return None + + kas_symbol = KASSymbol( + name=kassymbolbasic.name, + type=kassymbolbasic.type, + address=sym_address, + size=sym_size, + module_name=self._CORE_MODULE_NAME, + subsystem=self._CORE_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + return kas_symbol + + def _is_symbol_exported( + self, + name: int, + address: int, + module: Optional[interfaces.objects.ObjectInterface] = None, + ) -> bool: + """Check if the address belongs to an exported symbol. + If a module object is provided, it searches in that module symbols. + Otherwise, it searches in the global symbols. + + Bases on is_exported + + Args: + name: Symbol name + address: Symbol address + module: Module object. Defaults to None. + + Returns: + True if the symbol is exported; otherwise, returns False + """ + if module: + if module.num_syms <= 0: + return False + + start_mod_ksymtab = module.syms + stop_mod_ksymtab = ( + start_mod_ksymtab + + module.num_syms * self._kas_config.kernel_symbol_size + ) + kernel_symbol = self._find_exported_symbol_in_range( + name, start_mod_ksymtab, stop_mod_ksymtab + ) + else: + # Search the not GPL modules + kernel_symbol = self._find_exported_symbol_in_range( + name, + self._kas_config.start_ksymtab, + self._kas_config.stop_ksymtab, + ) + + if kernel_symbol is not None: + if hasattr(kernel_symbol, "get_value"): + return kernel_symbol.get_value() == address + else: + return kernel_symbol.vol.offset == address + + return None + + def _elfsym_to_kassymbol( + self, + module: interfaces.objects.ObjectInterface, + elf_sym_obj: interfaces.objects.ObjectInterface, + elf_sym_index: int, + subsystem: str = None, + ) -> Optional[KASSymbol]: + """Returns a KASSymbol from a ElfSym + + Args: + module: Module object + elf_sym_obj: ElfSym object + elf_sym_index: ElfSym index + subsystem: Name of the sub-subtem: core, module, bpf, ftrace, etc + + Returns: + A KASSymbol object + """ + layer = self._context.layers[self._layer_name] + sym_name = elf_sym_obj.get_name() + if not sym_name: + return None + + # Normalize sym.st_value offset, which is an address pointing to the symbol value + sym_address = elf_sym_obj.st_value & layer.address_mask + sym_type = module.get_symbol_type(elf_sym_obj, elf_sym_index) + + kas_symbol = KASSymbol( + name=sym_name, + type=sym_type, + address=sym_address, + size=elf_sym_obj.st_size, + module_name=module.get_name(), + exported=False, + subsystem=subsystem, + ) + kas_symbol.set_exported_from_type() + return kas_symbol + + def _is_module_ksym_address(self, address: int) -> bool: + return self._modules_address_min <= address <= self._modules_address_max + + def module_lookup_address( + self, + address: int, + module: Optional[interfaces.objects.ObjectInterface] = None, + ) -> Optional[KASSymbol]: + """Search for a symbol within kernel modules based on its memory address. + If a module object is provided, it will only search in that module. Otherwise, + it will try to first find the module to where the provided address belong to. + + Based on module_address_lookup. + + Args: + address: The memory address of the symbol to search for + module [optional]: The module to search within. If not provided, the module + containing the address will be automatically determined + + Returns: + The matching KASSymbol if found; otherwise, returns None + """ + if not self._is_module_ksym_address(address): + return None + + module = module or self._get_module_by_address(address) + if not module: + # This may occur if the kernel lacks the mod_tree implementation. + for ( + cur_module, + minimum_address, + maximum_address, + ) in self._module_memory_region: + if minimum_address <= address < maximum_address: + module = cur_module + break + + if not module: + # We couldn't find the module + return None + + kassymbol = self._find_address_in_module_symbols(module, address) + if kassymbol: + return kassymbol + + return None + + def _find_address_in_module_symbols( + self, + module: interfaces.objects.ObjectInterface, + address: int, + ) -> Optional[KASSymbol]: + """Find the symbol corresponding to a given address within a module. + + Based on find_kallsyms_symbol + + Args: + module: The module where the address belongs to + address: The memory address to search for + + Returns: + The matching KASSymbol if found; otherwise, returns None + """ + # Before walking all the symbols, ensure the address belongs to this module + module_boundaries = module.get_module_address_boundaries() + if not module_boundaries: + return None + + minimum_address, maximum_address = module_boundaries + if not (minimum_address <= address < maximum_address): + return None + + layer = self._context.layers[self._layer_name] + for elf_sym_idx, elf_sym in enumerate(module.get_symbols()): + if not elf_sym.get_name(): + continue + + sym_address_start = elf_sym.st_value & layer.address_mask + sym_address_end = sym_address_start + elf_sym.st_size + + if sym_address_start <= address < sym_address_end: + return self._elfsym_to_kassymbol( + module, elf_sym, elf_sym_idx, subsystem=self._MODULE_SUBSYSTEM_NAME + ) + + return None + + @functools.cached_property + def _module_memory_region( + self, + ) -> List[Tuple[interfaces.objects.ObjectInterface, int, int]]: + modules_region = [] + for module in linux_utilities_modules.Modules.list_modules( + self._context, self._module_name + ): + minimum_address, maximum_address = module.get_module_address_boundaries() + module_region = module, minimum_address, maximum_address + modules_region.append(module_region) + + return modules_region + + @functools.lru_cache + def _get_modules_memory_boundaries(self) -> Tuple[int, int]: + """Determine the boundaries of the module allocation area + + Returns: + A tuple containing the minimum and maximum addresses for the kernel module + allocation area. + """ + + if self._kas_config.mod_tree: + # Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca + mod_tree_address = self._kas_config.mod_tree + vmlinux = self._context.modules[self._module_name] + mod_tree = vmlinux.object( + object_type="mod_tree_root", + offset=mod_tree_address, + absolute=True, + ) + addr_min, addr_max = mod_tree.addr_min, mod_tree.addr_max + elif self._kas_config.module_addr_min and self._kas_config.module_addr_max: + # 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db + kas_config = self._kas_config + addr_min, addr_max = kas_config.module_addr_min, kas_config.module_addr_max + else: + raise exceptions.VolatilityException( + "Cannot find the module memory allocation area. Unsupported kernel" + ) + + layer = self._context.layers[self._layer_name] + return addr_min & layer.address_mask, addr_max & layer.address_mask + + @functools.cached_property + def _modules_address_min(self): + address_min, _address_max = self._get_modules_memory_boundaries() + return address_min + + @functools.cached_property + def _modules_address_max(self): + _address_min, address_max = self._get_modules_memory_boundaries() + return address_max + + def _get_module_by_address( + self, address: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Searches for the module that contains the given memory address within its range. + It uses a latch tree for optimized address range searching. + + Based on __module_address() + + Args: + address: The module memory address to search for. + + Returns: + The matching module if found; otherwise, returns None + """ + if not self._is_module_ksym_address(address): + return None + + return self._search_module_by_address(address) + + @functools.lru_cache + def _get_type_cache(self, name: str) -> Optional[interfaces.objects.Template]: + vmlinux = self._context.modules[self._module_name] + try: + return vmlinux.get_type(name) + except exceptions.SymbolError: + return None + + def _mod_tree_comp( + self, address: int, latch_tree_node: interfaces.objects.ObjectInterface + ) -> Optional[int]: + vmlinux = self._context.modules[self._module_name] + + module_memory_mtn = self._get_type_cache("module_memory") + if not module_memory_mtn: + vollog.debug( + "`module_memory` symbol not present in the symbol table. Cannot proceed." + ) + return None + + module_memory_mtn_offset = module_memory_mtn.relative_child_offset("mtn") + + mod_tree_node_mod = self._get_type_cache("mod_tree_node") + if not mod_tree_node_mod: + vollog.debug( + "`mod_tree_node` symbol not present in the symbol table. Cannot proceed." + ) + return None + + mod_tree_node_mod_offset = mod_tree_node_mod.relative_child_offset("mod") + + module_memory_offset = ( + latch_tree_node.vol.offset + + module_memory_mtn_offset + + mod_tree_node_mod_offset + ) + + module_memory = vmlinux.object( + object_type="module_memory", + offset=module_memory_offset, + absolute=True, + ) + start = module_memory.base + end = start + module_memory.size + + if address < start: + return -1 + elif address >= end: + return 1 + else: + return 0 + + def _search_module_by_address( + self, address: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Searches for the module that contains the given memory address within its range. + It uses a latch tree for optimized address range searching. + + Based on mod_find + + Args: + address: The module memory address to search for + + Returns: + The matching module if found; otherwise, returns None + """ + vmlinux = self._context.modules[self._module_name] + if self._kas_config.mod_tree: + mod_tree_address = self._kas_config.mod_tree + mod_tree = vmlinux.object( + object_type="mod_tree_root", + offset=mod_tree_address, + absolute=True, + ) + latch_tree_root = mod_tree.root + latch_tree_node = latch_tree_root.find(address, self._mod_tree_comp) + if latch_tree_node: + mod_tree_node = linux.LinuxUtilities.container_of( + latch_tree_node.vol.offset, "mod_tree_node", "node", vmlinux + ) + module_ptr = mod_tree_node.mod + if not module_ptr.is_readable(): + vollog.warning("Modules latch tree seems corrupt") + return None + + return module_ptr.dereference() + + return None + + def _find_exported_symbol_in_range( + self, name: str, start: int, stop: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Find an exported symbol within a specified range of kernel symbols. + + Based on lookup_exported_symbol + + Args: + name: Symbol name + start: Start address + stop: Stop address + + Returns: + The matching kernel_symbol object if found, or None if no match is found. + """ + + num_elems = (stop - start) // self._kas_config.kernel_symbol_size + + return self._search_kernel_symbol_object_by_name( + name, + base=start, + num_elems=num_elems, + ) + + def _cmp_kernel_symbol_name( + self, + name: str, + kernel_symbol: interfaces.objects.ObjectInterface, + ) -> int: + return self._cmp_symbol_name(name, kernel_symbol.get_name()) + + def _cmp_symbol_name( + self, + name: str, + other: str, + ) -> int: + if name is None or other is None: + return None + elif name == other: + return 0 + elif name < other: + return -1 + else: + return 1 + + def _search_kernel_symbol_object_by_name( + self, name: str, base: int, num_elems: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Search a kernel_symbol by name using binary search. + + Based on bsearch / __inline_bsearch() + + Args: + name: Symbol name + base: Base address + num_elems: Number of elements + + Returns: + A kernel_symbol object + """ + vmlinux = self._context.modules[self._module_name] + while num_elems > 0: + pivot = base + (num_elems // 2) * self._kas_config.kernel_symbol_size + + kernel_symbol_pivot = vmlinux.object( + object_type="kernel_symbol", + offset=pivot, + absolute=True, + ) + + result = self._cmp_kernel_symbol_name(name, kernel_symbol_pivot) + if result == 0: + return kernel_symbol_pivot + elif result > 0: + base = pivot + self._kas_config.kernel_symbol_size + num_elems -= 1 + + num_elems = num_elems // 2 + + return None + + def get_modules_symbols(self, name: str = None) -> Iterator[KASSymbol]: + """Yield each symbol from the kernel modules. + This function iterates over the symbols of the kernel modules and yields them as + KASSymbol objects. + + name (optional): If specified, the symbol name used to filter the symbols. + + Yields: + KASSymbol objects + """ + layer = self._context.layers[self._layer_name] + for module in linux_utilities_modules.Modules.list_modules( + self._context, self._module_name + ): + module_name = utility.array_to_string(module.name) + for elf_sym_idx, elf_sym_obj in enumerate(module.get_symbols()): + sym_name = elf_sym_obj.get_name() + if not sym_name: + continue + + if name and name != sym_name: + continue + + # Normalize sym.st_value offset, which is an address pointing to the symbol value + sym_address = elf_sym_obj.st_value & layer.address_mask + sym_size = elf_sym_obj.st_size + sym_type = module.get_symbol_type(elf_sym_obj, elf_sym_idx) + is_exported = self._is_symbol_exported(sym_name, sym_address, module) + sym_type = sym_type.upper() if is_exported else sym_type.lower() + + yield KASSymbol( + name=sym_name, + type=sym_type, + address=sym_address, + size=sym_size, + exported=is_exported, + module_name=module_name, + subsystem=self._MODULE_SUBSYSTEM_NAME, + ) + + def _ftrace_mod_get_symbols(self, address: int = None) -> Iterator[KASSymbol]: + """Yield each symbol from the ftrace modules. + This function iterates over the symbols of the ftrace modules and yields them as + KASSymbol objects. + + Based on ftrace_mod_get_kallsym + + Args: + address (optional): Address to filter symbols by + + Yields: + KASSymbol objects + """ + vmlinux = self._context.modules[self._module_name] + layer = self._context.layers[self._layer_name] + if not ( + vmlinux.has_type("ftrace_mod_map") and vmlinux.has_type("ftrace_mod_func") + ): + # kernel < 4.15 aba4b5c22cbac296f4081a0476d0c55828f135b4 + vollog.info( + "Unsupported Ftrace kallsyms implementation. Ignore this if it's a kernel < 4.15" + ) + return None + + symbol_table_name = vmlinux.symbol_table_name + ftrace_mod_map_symname = f"{symbol_table_name}{constants.BANG}ftrace_mod_map" + ftrace_mod_func_symname = f"{symbol_table_name}{constants.BANG}ftrace_mod_func" + ftrace_mod_maps = vmlinux.object_from_symbol("ftrace_mod_maps") + for mod_map in ftrace_mod_maps.to_list(ftrace_mod_map_symname, "list"): + for mod_func in mod_map.funcs.to_list(ftrace_mod_func_symname, "list"): + sym_name = utility.pointer_to_string( + mod_func.name, count=linux_constants.KSYM_NAME_LEN + ) + sym_addr = mod_func.ip & layer.address_mask + sym_size = mod_func.size + if address is not None and not ( + sym_addr <= address < sym_addr + sym_size + ): + continue + + module_name = utility.array_to_string(mod_map.mod.name) + kas_symbol = KASSymbol( + name=sym_name, + type=self._FTRACE_MODULE_SYM_TYPE, + address=sym_addr, + size=sym_size, + module_name=module_name, + subsystem=self._FTRACE_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + yield kas_symbol + + def _ftrace_get_trampoline_symbols( + self, address: int = None + ) -> Iterator[KASSymbol]: + """Yield each symbol from the ftrace trampoline. + + Based on ftrace_get_trampoline_kallsym + + Args: + address (optional): Address to filter symbols by + + Yields: + KASSymbol objects + """ + # See kernel's ftrace_get_trampoline_kallsym() + vmlinux = self._context.modules[self._module_name] + if not vmlinux.has_type("ftrace_ops"): + # kernels < 2.6.27 16444a8a40d4c7b4f6de34af0cae1f76a4f6c901 + return None + + if not vmlinux.has_symbol("ftrace_ops_trampoline_list"): + # kernels < 5.9 fc0ea795f53c8d7040fa42471f74fe51d78d0834 + return None + + symbol_table_name = vmlinux.symbol_table_name + ftrace_ops_symname = f"{symbol_table_name}{constants.BANG}ftrace_ops" + ftrace_ops_trampoline_list = vmlinux.object_from_symbol( + "ftrace_ops_trampoline_list" + ) + + for ftrace_op in ftrace_ops_trampoline_list.to_list(ftrace_ops_symname, "list"): + sym_name = self._FTRACE_TRAMPOLINE_SYM + sym_addr = ftrace_op.trampoline + sym_size = ftrace_op.trampoline_size + + if address is not None and not (sym_addr <= address < sym_addr + sym_size): + continue + + kas_symbol = KASSymbol( + name=sym_name, + type=self._FTRACE_TRAMPOLINE_SYM_TYPE, + address=sym_addr, + size=sym_size, + module_name=self._FTRACE_TRAMPOLINE_MODULE_NAME, + subsystem=self._FTRACE_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + yield kas_symbol + + def get_ftrace_symbols(self) -> Iterator[KASSymbol]: + """Yield each kernel ftrace symbol + + Yields: + KASSymbol objects + """ + yield from self._ftrace_mod_get_symbols() + yield from self._ftrace_get_trampoline_symbols() + + def get_bpf_symbols(self) -> Iterator[KASSymbol]: + """Yield each kernel BPF symbol + + Based on bpf_get_kallsym() + + Yields: + KASSymbol objects + """ + vmlinux = self._context.modules[self._module_name] + if vmlinux.has_type("bpf_ksym"): + # kernels >= 5.8 + list_type, list_head_member = "bpf_ksym", "lnode" + elif vmlinux.has_type("bpf_prog_aux"): + # 3.18 <= kernels < 5.8 + list_type, list_head_member = "bpf_prog_aux", "ksym_lnode" + else: + # kernels < 3.18 + vollog.info( + "Unsupported BPF kallsysms implementation. Don't worry if kernel < 3.18" + ) + return None + + symbol_table_name = vmlinux.symbol_table_name + list_type_symname = f"{symbol_table_name}{constants.BANG}{list_type}" + + layer = self._context.layers[self._layer_name] + + # Even when bpf_jit_kallsyms is disabled (/proc/sys/net/core/bpf_jit_kallsyms = 0), + # this function will still be able to gather the symbols. + try: + bpf_kallsyms_list = vmlinux.object_from_symbol("bpf_kallsyms") + except exceptions.SymbolError: + vollog.debug( + "`bpf_kallsyms` symbol not present in the symbol table. Cannot proceed." + ) + return None + + for elem in bpf_kallsyms_list.to_list(list_type_symname, list_head_member): + try: + # See kernel's bpf_get_kallsym() + if list_type == "bpf_ksym": + # kernels >= 5.8 + bpf_ksym = elem + sym_name = utility.array_to_string(bpf_ksym.name) + sym_addr = bpf_ksym.start + sym_size = bpf_ksym.end - bpf_ksym.start + else: + # list_type == "bpf_prog_aux" 3.18 <= kernels < 5.8 + bpf_prog_aux = elem + bpf_prog = bpf_prog_aux.prog + sym_name = bpf_prog.get_name() + sym_addr = bpf_prog.bpf_func + sym_start, sym_end = bpf_prog.get_address_region() + sym_size = sym_end - sym_start + except exceptions.InvalidAddressException: + continue + + # The following are also hardcoded in the Linux kernel + # see kernel's get_ksymbol_bpf(), bpf_get_kallsym() and BPF_SYM_ELF_TYPE + module_name = self._BPF_MODULE_NAME + sym_type = self._BPF_SYM_TYPE + sym_addr &= layer.address_mask + + kas_symbol = KASSymbol( + name=sym_name, + type=sym_type, + address=sym_addr, + size=sym_size, + module_name=module_name, + subsystem=self._BPF_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + yield kas_symbol + + def get_all_symbols(self) -> Iterator[KASSymbol]: + """Enumerates each kallsym symbol + + Yields: + KASSymbol objects + """ + yield from self.get_core_symbols() + yield from self.get_modules_symbols() + yield from self.get_ftrace_symbols() + yield from self.get_bpf_symbols() + + def bpf_lookup_address(self, address: int) -> Optional[KASSymbol]: + """Search for a BPF symbol based on its memory address. + + Based on bpf_address_lookup() and __bpf_address_lookup() + + Args: + address: The memory address to search for + + Returns: + The matching KASSymbol if found; otherwise, returns None + """ + vmlinux = self._context.modules[self._module_name] + + if vmlinux.has_type("bpf_ksym"): + # kernels >= 5.7 535911c80ad4f5801700e9d827a1985bbff41519 + bpf_ksym = self._find_bpf_ksym(address) + if not bpf_ksym: + return None + symbol_start = bpf_ksym.start + symbol_end = bpf_ksym.end + sym_name = utility.array_to_string(bpf_ksym.name) + sym_size = symbol_end - symbol_start + elif vmlinux.has_type("latch_tree_root") and vmlinux.get_type( + "bpf_prog_aux" + ).has_member("ksym_tnode"): + # For 4.11 <= kernels < 5.7 + # latch_tree_root was added in kernels 4.2 ade3f510f93a5613b672febe88eff8ea7f1c63b7 + # BPF kallsyms support was added in kernels 4.11 74451e66d516c55e309e8d89a4a1e7596e46aacd + bpf_prog = self._find_bpf_prog(address) + if not bpf_prog: + return None + + symbol_start, symbol_end = bpf_prog.get_addr_region() + sym_name = bpf_prog.get_name() + sym_size = symbol_end - symbol_start + else: + # kernel < 4.11 + vollog.info( + "Unsupported BPF kallsyms implementation. Ignore this if it's a kernel < 4.11" + ) + return None + + layer = self._context.layers[self._layer_name] + symbol_start &= layer.address_mask + + kas_symbol = KASSymbol( + name=sym_name, + type=self._BPF_SYM_TYPE, + address=symbol_start, + size=sym_size, + module_name=self._BPF_MODULE_NAME, + subsystem=self._BPF_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + return kas_symbol + + def _find_bpf_prog( + self, address: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Search for a BPF program based on its address. + Based on __bpf_address_lookup & bpf_prog_kallsyms_find() for kernels < 5.7 + + Args: + address: The BPF symbol address to search for + + Returns: + A bpf_prog object if found; otherwise, returns None. + """ + vmlinux = self._context.modules[self._module_name] + if not self._kas_config.bpf_tree_address: + return None + + bpf_latch_tree_root = vmlinux.object( + object_type="latch_tree_root", + offset=self._kas_config.bpf_tree_address, + absolute=True, + ) + latch_tree_node = bpf_latch_tree_root.find( + address, self._bpf_tree_comp_bpf_prog_aux + ) + + if not latch_tree_node: + return None + + bpf_prog_aux = linux.LinuxUtilities.container_of( + latch_tree_node.vol.offset, "bpf_prog_aux", "ksym_tnode", vmlinux + ) + bpf_prog = bpf_prog_aux.prog + return bpf_prog + + def _bpf_tree_comp_bpf_prog_aux( + self, address: int, latch_tree_node: interfaces.objects.ObjectInterface + ) -> int: + """Comparison function used by _find_bpf_prog() + Based on bpf_tree_comp for kernels < 5.7 + + Args: + address: The memory address to search for + latch_tree_node: A latch tree node + + Returns: + 0: equal, >0: key is greater, <0: key is less than this bpf_prog + """ + vmlinux = self._context.modules[self._module_name] + layer = self._context.layers[self._layer_name] + bpf_prog_aux = linux.LinuxUtilities.container_of( + latch_tree_node.vol.offset, "bpf_prog_aux", "ksym_tnode", vmlinux + ) + bpf_prog = bpf_prog_aux.prog + bpf_start, bpf_end = bpf_prog.get_address_region() + bpf_start &= layer.address_mask + bpf_end &= layer.address_mask + + if address < bpf_start: + return -1 + elif address > bpf_end: + # Keep 'key > end' instead of 'key >= end'. This detects return addresses + # within the program when the final instruction in a stack trace is a call. + return 1 + else: + return 0 + + def _find_bpf_ksym( + self, address: int + ) -> Optional[interfaces.objects.ObjectInterface]: + """Search for the respective bpf_ksym based on a symbol address. + Based on __bpf_address_lookup & bpf_ksym_find() for kernels >= 5.7 + + Args: + address: The memory address to search for + + Returns: + A bpf_ksym object if found; otherwise, returns None. + """ + vmlinux = self._context.modules[self._module_name] + if not self._kas_config.bpf_tree_address: + return None + + bpf_latch_tree_root = vmlinux.object( + object_type="latch_tree_root", + offset=self._kas_config.bpf_tree_address, + absolute=True, + ) + latch_tree_node = bpf_latch_tree_root.find( + address, self._bpf_tree_comp_bpf_ksym + ) + if not latch_tree_node: + return None + + bpf_ksym = linux.LinuxUtilities.container_of( + latch_tree_node.vol.offset, "bpf_ksym", "tnode", vmlinux + ) + return bpf_ksym + + def _bpf_tree_comp_bpf_ksym( + self, address: int, latch_tree_node: interfaces.objects.ObjectInterface + ) -> int: + """Comparison function used by _find_bpf_ksym. + + Based on bpf_tree_comp in kernels >= 5.7 + + Args: + address: The memory address to search for + latch_tree_node: A latch tree node + + Returns: + 0: equal, >0: key is greater, <0: key is less than this bpf_prog + """ + # + vmlinux = self._context.modules[self._module_name] + layer = self._context.layers[self._layer_name] + bpf_ksym = linux.LinuxUtilities.container_of( + latch_tree_node.vol.offset, "bpf_ksym", "tnode", vmlinux + ) + bpf_start = bpf_ksym.start & layer.address_mask + bpf_end = bpf_ksym.end & layer.address_mask + + if address < bpf_start: + return -1 + elif address > bpf_end: + # Keep 'address > bpf_end' instead of 'address >= bpf_end'. This detects return + # addresses within the program when the final instruction in a stack trace is a call. + return 1 + else: + return 0 + + def ftrace_lookup_address(self, address: int) -> Optional[KASSymbol]: + """Search for a ftrace symbol based on its address. + + Based on ftrace_mod_address_lookup() + + Args: + address: The memory address to search for + + Returns: + The matching KASSymbol if found, or None if no match is found. + """ + + # Filter by address and return only the first matching result. + for kassymbol in self._ftrace_mod_get_symbols(address): + return kassymbol + + for kassymbol in self._ftrace_get_trampoline_symbols(address): + return kassymbol + + return None + + def _core_lookup_name_slow(self, name) -> Optional[KASSymbol]: + """Search a core symbol by name + + Based on kallsyms_lookup_name in kernels < 6.2 + + Args: + name: The symbol name to search for. + + Returns: + A KASSymbol object + """ + # kernels < 6.2 60443c88f3a89fd303a9e8c0e84895910675c316 + current_offset = 0 + for sym_idx in range(self._kallsyms_num_syms): + kassymbol, compressed_length = self._get_symbol(current_offset, sym_idx) + if kassymbol and name == kassymbol.name: + return kassymbol + + current_offset += compressed_length + 1 + + return None + + @functools.cached_property + def _kallsyms_seqs_of_names(self): + vmlinux = self._context.modules[self._module_name] + symbol_table_name = vmlinux.symbol_table_name + unsigned_char_symname = symbol_table_name + constants.BANG + "unsigned char" + # See 19bd8981dc2ee35fdc81ab1b0104b607c917d470: 3 bytes per index + array_size = 3 * self._kallsyms_num_syms + kallsyms_seqs_of_names = vmlinux.object( + object_type="array", + offset=self._kas_config.seqs_of_names_address, + subtype=vmlinux.get_type(unsigned_char_symname), + count=array_size, + absolute=True, + ) + return kallsyms_seqs_of_names + + def _get_symbol_seq(self, index: int) -> int: + # See 19bd8981dc2ee35fdc81ab1b0104b607c917d470 + bits = 3 + seq = 0 + for i in range(bits): + seq = (seq << 8) | self._kallsyms_seqs_of_names[bits * index + i] + return seq + + def _get_symbol_by_index(self, index) -> Tuple[KASSymbolBasic, int]: + seq = self._get_symbol_seq(index) + offset = self._get_symbol_offset(seq) + kassymbolbasic, _compressed_length = self._expand_symbol(offset) + return kassymbolbasic + + def _lookup_name_index(self, name: str) -> Optional[int]: + # based on kallsyms_lookup_names + high = self._kallsyms_num_syms - 1 + low = 0 + + while low <= high: + mid = (low + high) // 2 + kassymbolbasic = self._get_symbol_by_index(mid) + if not kassymbolbasic: + return None + + ret = self._cmp_symbol_name(name, kassymbolbasic.name) + if ret > 0: + low = mid + 1 + elif ret < 0: + high = mid - 1 + else: + break + + if low > high: + # Not found + return None + + low = mid + while low: + kassymbolbasic = self._get_symbol_by_index(low - 1) + if not kassymbolbasic: + return None + if self._cmp_symbol_name(name, kassymbolbasic.name) != 0: + return low + low -= 1 + + return None + + def _core_lookup_name_fast(self, name: str) -> Optional[KASSymbol]: + """Search a core symbol by name + + Based on kallsyms_lookup_name in kernels >= 6.2 + + Args: + name: The symbol name to search for + + Returns: + A KASSymbol object + """ + # kernels >= 6.2 60443c88f3a89fd303a9e8c0e84895910675c316 + index = self._lookup_name_index(name) + if not index: + return None + + seq = self._get_symbol_seq(index) + offset = self._get_symbol_offset(seq) + kassymbolbasic, _compressed_length = self._expand_symbol(offset) + sym_address = self._get_symbol_address_by_index(seq) + _seq, sym_size = self._get_symbol_pos(sym_address) + + kas_symbol = KASSymbol( + name=kassymbolbasic.name, + type=kassymbolbasic.type, + address=sym_address, + size=sym_size, + module_name=self._CORE_MODULE_NAME, + subsystem=self._CORE_SUBSYSTEM_NAME, + ) + kas_symbol.set_exported_from_type() + return kas_symbol + + def _kallsyms_lookup_name_modules(self, name: str) -> Optional[KASSymbol]: + """_summary_ + + Based on module_kallsyms_lookup_name + + Args: + name: The symbol name to search for. + + Returns: + A KASSymbol object + """ + for kassymbol in self.get_modules_symbols(name): + if name == kassymbol.name: + # First match only + return kassymbol + return None + + def lookup_name(self, name: str) -> Optional[KASSymbol]: + """Search symbols by name. + WARNING: This function is super slow. The kernel does not index the symbols by + name, so the it is a linear search. + + Based on kallsyms_lookup_name + + Args: + name: The symbol name to search for. + + Returns: + A KASSymbol object + """ + if self._kas_config.seqs_of_names_address: + # kernels >= 6.2: + # 60443c88f3a89fd303a9e8c0e84895910675c316 and 19bd8981dc2ee35fdc81ab1b0104b607c917d470 + kassymbol = self._core_lookup_name_fast(name) + else: + # kernels < 6.2 + kassymbol = self._core_lookup_name_slow(name) + + if kassymbol: + return kassymbol + + return self._kallsyms_lookup_name_modules(name) diff --git a/volatility3/framework/symbols/linux/network.py b/volatility3/framework/symbols/linux/network.py new file mode 100644 index 000000000..72ffe8047 --- /dev/null +++ b/volatility3/framework/symbols/linux/network.py @@ -0,0 +1,27 @@ +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.linux.extensions import network +from volatility3.framework.interfaces import configuration + + +class NetSymbols(configuration.VersionableInterface): + _version = (1, 0, 0) + + @classmethod + def apply(cls, symbol_table: intermed.IntermediateSymbolTable): + # Network + symbol_table.set_type_class("net", network.net) + symbol_table.set_type_class("net_device", network.net_device) + symbol_table.set_type_class("in_device", network.in_device) + symbol_table.set_type_class("in_ifaddr", network.in_ifaddr) + symbol_table.set_type_class("inet6_dev", network.inet6_dev) + symbol_table.set_type_class("inet6_ifaddr", network.inet6_ifaddr) + symbol_table.set_type_class("socket", network.socket) + symbol_table.set_type_class("sock", network.sock) + symbol_table.set_type_class("inet_sock", network.inet_sock) + symbol_table.set_type_class("unix_sock", network.unix_sock) + # Might not exist in older kernels or the current symbols + symbol_table.optional_set_type_class("netlink_sock", network.netlink_sock) + symbol_table.optional_set_type_class("vsock_sock", network.vsock_sock) + symbol_table.optional_set_type_class("packet_sock", network.packet_sock) + symbol_table.optional_set_type_class("bt_sock", network.bt_sock) + symbol_table.optional_set_type_class("xdp_sock", network.xdp_sock) diff --git a/volatility3/framework/symbols/linux/utilities/__init__.py b/volatility3/framework/symbols/linux/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py new file mode 100644 index 000000000..5ec254368 --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -0,0 +1,726 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +import logging +import struct + +from typing import ( + List, + Optional, + Tuple, + Dict, +) + +from volatility3 import framework +from volatility3.framework import interfaces, exceptions, symbols, deprecation +from volatility3.framework.constants import linux as linux_constants +from volatility3.framework.symbols.linux import extensions + +vollog = logging.getLogger(__name__) + +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory +# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time +# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections +# There are also several other significant complications that we must deal with when trying to extract an LKM +# that can be analyzed with static analysis tools +# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections +# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense +# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory +# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, +# we create the .shstrtab based on the sections in memory and then glue it in as the final section + +# ModuleExtract.extract_module is the entry point and only visible method for plugins + + +# See PR #1773 +@deprecation.renamed_class( + deprecated_class_name="ModuleExtract", + removal_date="2026-06-01", + message="volatility3.framework.symbols.linux.utilities.module_extract.ModuleExtract is to be deprecated. Use volatility3.framework.symbols.linux.utilities.modules.ModuleExtract instead.", +) +class ModuleExtract(interfaces.configuration.VersionableInterface): + """Extracts Linux kernel module structures into an analyzable ELF file""" + + _version = (1, 0, 1) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def _find_section( + cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int + ) -> Optional[Tuple[str, int, int, int]]: + """ + Finds the section containing `sym_address` + """ + for name, index, address, size in section_lookups: + if address <= sym_address < address + size: + return name, index, address, size + + return None + + @classmethod + def _get_st_info_for_sym( + cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str + ) -> bytes: + """ + This is a helper function called from `_fix_sym_table` + + Calculates the `st_info` value for the given symbol + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + if sym.st_name > 0: + # Global symbol + bind = linux_constants.STB_GLOBAL + + if sym_address == 0: + sect_type = linux_constants.STT_NOTYPE + elif sect_name: + # rela = relocations + if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1: + sect_type = linux_constants.STT_FUNC + else: + sect_type = linux_constants.STT_OBJECT + + else: + # outside the module being extracted + sect_type = linux_constants.STT_NOTYPE + + else: + # Local symbol + bind = linux_constants.STB_LOCAL + sect_type = linux_constants.STT_SECTION + + # Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO + bind_bits = (bind << 4) & 0xF0 + type_bits = sect_type & 0xF + + st_info_int = (bind_bits | type_bits) & 0xFF + + return struct.pack("B", st_info_int) + + @classmethod + def _get_fixed_sym_fields( + cls, + st_fmt: str, + sym: interfaces.objects.ObjectInterface, + sections: List[Tuple[str, int, int, int]], + ) -> Tuple[str, int, int, int]: + """ + This is a helper function called from `_fix_sym_table` + + The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading + Static analysis tools do not understand these transformed values as they only make sense to the kernel loader + We must de-mangle these to have analysis tools understand symbols (a key aspect) + """ + # Start by trying to map a symbol to its section + sym_address = sym.st_value + sect_info = cls._find_section(sections, sym_address) + + if not sect_info: + # Symbol does not point into the module being extracted + sect_name, sect_index, sect_address = None, None, None + st_value_int = sym_address + else: + # relative address inside the section + sect_name, sect_index, sect_address, _ = sect_info + st_value_int = sym_address - sect_address + + # Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file + + # formatted to be written into the extracted file + st_value = struct.pack(st_fmt, st_value_int) + + # returns formatted to be written into the extracted file + st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name) + + # format to reference its section, if any + if sect_name: + st_shndx = struct.pack(" Optional[bytes]: + """ + This function implements the most painful part of the reconstruction + + The symbols in .symtab are broken/mangled during loading. + We need to normalize these for static analysis tools to understand the references. + Without proper symbols, analysis is pretty pointless and gets nowhere. + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + kernel = context.modules[vmlinux_name] + + # Gather the section information into a list + section_lookups: List[Tuple[str, int, int, int]] = [] + for index, (address, name) in enumerate(original_sections.items()): + # We are fixing symtab references... + if name == ".symtab": + continue + + size = section_sizes[address] + + # Add 1 to account for leading NULL section + section_lookups.append((name, index + 1, address, size)) + + # Build the array of symbols as they are in memory + sym_type = kernel.get_type(sym_type_name) + + symbols = kernel.object( + object_type="array", + subtype=sym_type, + offset=module.section_symtab, + count=module.num_symtab, + absolute=True, + ) + + # used to hold the new (fixed) symbol table + sym_table_data = b"" + + # build a correct/normalized Elf32_Sym or Elf64_Sym for each symbol + for sym in symbols: + # get the mangled fields' correct values + sect_name, st_value, st_info, st_shndx = cls._get_fixed_sym_fields( + st_fmt, sym, section_lookups + ) + + # these aren't mangled during loading + st_name = struct.pack(" Optional[Tuple[List, int, int]]: + """ + This function first parses the sections as maintained by the kernel + It then orders the sections by load address, and then gathers the data of each section + We also track the file_offset to correctly have alignment in the output file + + .symtab requires special handling as its so broken in memory as described in `_fix_sym_table` + The data of .strtab is read directly off the module structure and not its section + as the section from the original module has no meaning after loading as the kernel does not reference it. + """ + original_sections = {} + for index, section in enumerate(module.get_sections()): + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: + return None + + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + sym_type = "Elf64_Sym" + elf_hdr_type = "Elf64_Ehdr" + st_fmt = " Optional[bytes]: + """ + Creates a `bits` bit ELF header for the file based on recovered values + Called last as it needs information computed from the sections + + Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + if bits == 32: + fmt = " Optional[int]: + """ + This function makes a best effort to map common section names + to their attributes + """ + known_sections = { + ".note.gnu.build-id": linux_constants.SHT_NOTE, + ".text": linux_constants.SHT_PROGBITS, + ".init.text": linux_constants.SHT_PROGBITS, + ".exit.text": linux_constants.SHT_PROGBITS, + ".static_call.text": linux_constants.SHT_PROGBITS, + ".rodata": linux_constants.SHT_PROGBITS, + ".modinfo": linux_constants.SHT_PROGBITS, + "__param": linux_constants.SHT_PROGBITS, + ".data": linux_constants.SHT_PROGBITS, + ".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS, + ".comment": linux_constants.SHT_PROGBITS, + ".shstrtab": linux_constants.SHT_STRTAB, + ".symtab": linux_constants.SHT_SYMTAB, + ".strtab": linux_constants.SHT_STRTAB, + } + + sect_type_val = linux_constants.SHT_PROGBITS + + if section_name.find(".rela.") != -1: + sect_type_val = linux_constants.SHT_RELA + + elif section_name in known_sections: + sect_type_val = known_sections[section_name] + + return sect_type_val + + # all sections from memory are allocated (SHF_ALLOC) + # special check certain other sections to try and ensure extra flags are added where needed + @classmethod + def _calc_sect_flags(cls, name: str) -> int: + """ + Make a best effort to map common section names to their permissions + If we miss a section here, users of common static analysis tools can mark the + sections are writable or executable manually, but that becomes very cumbersome + and breaks initial analysis by the tool + """ + # All sections in memory are allocated (`A` in readelf -S) + flags = linux_constants.SHF_ALLOC + + if name in [".text", ".init.text", ".exit.text", ".static_call.text"]: + flags = flags | linux_constants.SHF_EXECINSTR + + elif name in [ + ".data", + ".init.data", + ".exit.data", + ".bss", + "__tracepoints", + ".data.once", + "_ftrace_events", + ".gnu.linkonce.this_module", + ]: + flags = flags | linux_constants.SHF_WRITE + + return flags + + @classmethod + def _calc_link( + cls, name: str, strtab_index: int, symtab_index: int, sect_type: int + ) -> int: + """ + Calculates the link value for a section + + The most important ones are symtab indexes for relocations + and to point the symbol table to the string tab + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return symtab_index + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + return strtab_index + + return 0 + + @classmethod + def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int: + """ + Calculates the entsize for relocation sections and the symbol table section + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return 24 + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + if bits == 32: + return 16 + else: + return 24 + + return 0 + + @classmethod + def _make_section_header( + cls, + bits: int, + name_index: int, + name: str, + address: int, + size: int, + file_offset: int, + strtab_index: int, + symtab_index: int, + ) -> Optional[bytes]: + """ + Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section + """ + if bits == 32: + fmt = " Optional[bytes]: + # Bail early if bad address sent in + try: + hasattr(module.sect_attrs, "nsections") + except exceptions.InvalidAddressException: + vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.") + return None + + # Gather sections + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result + + kernel = context.modules[vmlinux_name] + + # Figure out header sizes + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + header_type = "Elf64_Ehdr" + section_type = "Elf64_Shdr" + bits = 64 + else: + header_type = "Elf32_Ehdr" + section_type = "Elf32_Shdr" + bits = 32 + + header_type_size = kernel.get_type(header_type).size + section_type_size = kernel.get_type(section_type).size + + # Per Linux-spec, all LKMs must start with a null section header + # This buffer is used to hold the headers as they are built + sections_headers = b"\x00" * section_type_size + + # Holder of the data of the sections + sections_data = b"" + + # the .shstrtab section is "\x00" + section name for each section + # followed by a terminating null. + # It starts with the null string (\x00) + shstrtab_data = b"\x00" + + # Track where we end the sections and data to glue `.shstrtab` after + last_file_offset = None + last_sect_size = None + + # Start at 1 in the string table + name_index = 1 + + # Create the actual section headers + for index, (name, address, file_offset, section_data) in enumerate( + updated_sections + ): + # Make the section header + header_bytes = cls._make_section_header( + bits, + name_index, + name, + address, + len(section_data), + file_offset, + strtab_index, + symtab_index, + ) + if not header_bytes: + vollog.debug(f"make_section_header failed for section {name}") + return None + + # ndex into the string table + name_index += len(name) + 1 + + # concatenate the header and section bytes + sections_headers += header_bytes + sections_data += section_data + + # track where we are so .shstrtab goes into correct offset + last_file_offset = file_offset + last_sect_size = len(section_data) + + # append each section name to what will become .shstrtab + shstrtab_data += bytes(name, encoding="utf8") + b"\x00" + + # stick our own section reference string at end + # name_index points to the end of the last section string after the loop ends + shstrtab_data += b".shstrtab\x00" + + # create our .shstrtab section so sections have names + sections_headers += cls._make_section_header( + bits, + name_index, + ".shstrtab", + 0, + len(shstrtab_data), + last_file_offset + last_sect_size, + strtab_index, + symtab_index, + ) + + sections_data += shstrtab_data + + num_sections = len(updated_sections) + 1 + + header = cls._make_elf_header( + bits, + header_type_size + len(sections_data), + num_sections, + ) + + if not header: + vollog.error( + f"Hit error creating Elf header for module at {module.vol.offset:#x}" + ) + return None + + # Return our beautiful, hand-crafted, farm raised ELF file + return header + sections_data + sections_headers diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py new file mode 100644 index 000000000..63b25380a --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -0,0 +1,1752 @@ +import logging +import warnings +import functools +import struct +from abc import ABCMeta, abstractmethod +from typing import ( + Callable, + Dict, + Generator, + Iterable, + Iterator, + List, + NamedTuple, + Optional, + Set, + Tuple, + Union, +) + +from volatility3 import framework +from volatility3.framework import ( + constants, + deprecation, + exceptions, + interfaces, + objects, + renderers, + symbols, +) +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 +from volatility3.framework.constants import linux as linux_constants + +vollog = logging.getLogger(__name__) + + +class ModuleInfo(NamedTuple): + """ + Used to track the name and boundary of a kernel module + """ + + offset: int + name: str + start: int + end: int + + +class ModuleGathererInterface( + interfaces.configuration.VersionableInterface, metaclass=ABCMeta +): + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + gatherer_return_type = Generator[Union[ModuleInfo, "extensions.module"], None, None] + + # Must be set to a unique, descriptive name of the gathering technique or data structure source + name = None + + @classmethod + @abstractmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> gatherer_return_type: + """ + This method must return a generator (yield) of each `gatherer_return_type` found from its source + """ + + +class Modules(interfaces.configuration.VersionableInterface): + """Kernel modules related utilities.""" + + _version = (3, 0, 2) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def module_lookup_by_address( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + modules: Iterable[ModuleInfo], + target_address: int, + ) -> Optional[Tuple[ModuleInfo, Optional[str]]]: + """ + Determine if a target address lies in a module memory space. + Returns the module where the provided address lies. + + `modules` must be non-empty and contain masked addresses via `get_module_info_for_module` or + a ValueError will be thrown + + Args: + context: The context on which to operate + layer_name: The name of the layer on which to operate + modules: An iterable containing the modules to match the address against + target_address: The address to check for a match + + Returns: + The first memory module in which the address fits and the symbol name for `target_address` + + Kernel documentation: + "within_module" and "within_module_mem_type" functions + """ + kernel = context.modules[kernel_module_name] + + kernel_layer = context.layers[kernel.layer_name] + + if not modules: + raise ValueError("Empty list sent to `module_lookup_by_address`") + + matches = [] + for module in modules: + if module.start != module.start & kernel_layer.address_mask: + raise ValueError( + "Modules list must be gathered from `run_modules_scanners` to be used in this function" + ) + + if module.start <= target_address < module.end: + matches.append(module) + + if len(matches) >= 1: + if len(matches) > 1: + warnings.warn( + f"Address {hex(target_address)} fits in modules at {[hex(module.start) for module in matches]}, indicating potential modules memory space overlap. The first matching entry {matches[0].name} will be used", + UserWarning, + ) + + symbol_name = None + + match = matches[0] + + if match.name == constants.linux.KERNEL_NAME: + symbols = list(kernel.get_symbols_by_absolute_location(target_address)) + + if len(symbols): + symbol_name = symbols[0] + else: + module = kernel.object("module", offset=module.offset, absolute=True) + symbol_name = module.get_symbol_by_address(target_address) + + if symbol_name and symbol_name.find(constants.BANG) != -1: + symbol_name = symbol_name.split(constants.BANG)[1] + + return match, symbol_name + + return None, None + + @classmethod + @deprecation.method_being_removed( + removal_date="2025-09-25", + message="Code using this function should adapt `linux_utilities_modules.Modules.run_module_scanners`", + ) + def mask_mods_list( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + mods: Iterator[extensions.module], + ) -> List[Tuple[str, int, int]]: + """ + A helper function to mask the starting and end address of kernel modules + """ + mask = context.layers[kernel_layer_name].address_mask + + return [ + ( + utility.array_to_string(mod.name), + mod.get_module_base() & mask, + (mod.get_module_base() & mask) + mod.get_core_size(), + ) + for mod in mods + ] + + @classmethod + @deprecation.method_being_removed( + removal_date="2025-09-25", + message="Use `module_lookup_by_address` to map address to their hosting kernel module and symbol.", + ) + def lookup_module_address( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + handlers: List[Tuple[str, int, int]], + target_address: int, + ) -> Tuple[str, str]: + """ + Searches between the start and end address of the kernel module using target_address. + Returns the module and symbol name of the address provided. + """ + kernel_module = context.modules[kernel_module_name] + mod_name = "UNKNOWN" + symbol_name = "N/A" + + for name, start, end in handlers: + if start <= target_address <= end: + mod_name = name + if name == constants.linux.KERNEL_NAME: + symbols = list( + kernel_module.get_symbols_by_absolute_location(target_address) + ) + + if len(symbols): + symbol_name = ( + symbols[0].split(constants.BANG)[1] + if constants.BANG in symbols[0] + else symbols[0] + ) + + break + + return mod_name, symbol_name + + @classmethod + def get_module_info_for_module( + cls, address_mask: int, module: extensions.module + ) -> Optional[ModuleInfo]: + """ + Returns a ModuleInfo instance for `module` + + This performs address masking to avoid endless calls to `mask_mods_list` + + Returns None if the name is smeared + """ + try: + mod_name = utility.array_to_string(module.name) + except exceptions.InvalidAddressException: + return None + + start = module.get_module_base() & address_mask + + end = start + module.get_core_size() + + return ModuleInfo(module.vol.offset, mod_name, start, end) + + @classmethod + def run_modules_scanners( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + caller_wanted_gatherers: List[ModuleGathererInterface], + flatten: bool = True, + ) -> Dict[str, List[ModuleInfo]]: + """Run module scanning plugins and aggregate the results. It is designed + to not operate any inter-plugin results triage. + + Rules for `caller_wanted_gatherers`: + If `ModuleGatherers.all_gathers_identifier` is specified then every source will be populated + + If empty or an invalid gatherer is specified then a ValueError is thrown + + All gatherer names must be unique + Args: + called_wanted_sources: The list of sources to gather modules. + flatten: Whether to de-duplicate modules across gatherers + Returns: + Dictionary mapping each gatherer to its corresponding result + """ + if not caller_wanted_gatherers: + raise ValueError( + "`caller_wanted_gatherers` must have at least one gatherer." + ) + + if not isinstance(caller_wanted_gatherers, Iterable): + raise ValueError("`caller_wanted_gatherers` must be iterable") + + seen_names = set() + + for gatherer in caller_wanted_gatherers: + if not issubclass(gatherer, ModuleGathererInterface): + raise ValueError( + f"Invalid gatherer sent through `caller_wanted_gatherers`: {gatherer}" + ) + + if not gatherer.name: + raise ValueError( + f"{gatherer} does not have a valid name attribute, which is required. It must be a non-zero length string." + ) + + if gatherer.name in seen_names: + raise ValueError( + f"{gatherer} has a name {gatherer.name} which has already been processed. Names must be unique." + ) + + seen_names.add(gatherer.name) + + kernel = context.modules[kernel_module_name] + + address_mask = context.layers[kernel.layer_name].address_mask + + run_results: Dict[ModuleGathererInterface, List[ModuleInfo]] = {} + + # Walk each source gathering modules + for gatherer in caller_wanted_gatherers: + run_results[gatherer.name] = [] + + # 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 + else: + modinfo = cls.get_module_info_for_module(address_mask, module) + + if modinfo: + run_results[gatherer.name].append(modinfo) + + if flatten: + return cls.flatten_run_modules_results(run_results) + + return run_results + + @staticmethod + @functools.lru_cache + def get_modules_memory_boundaries( + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Tuple[int, int]: + """Determine the boundaries of the module allocation area + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A tuple containing the minimum and maximum addresses for the module allocation area. + """ + vmlinux = context.modules[vmlinux_module_name] + if vmlinux.has_symbol("mod_tree"): + # Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca + mod_tree = vmlinux.object_from_symbol("mod_tree") + modules_addr_min = mod_tree.addr_min + modules_addr_max = mod_tree.addr_max + elif vmlinux.has_symbol("module_addr_min"): + # 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db + modules_addr_min = vmlinux.object_from_symbol("module_addr_min") + modules_addr_max = vmlinux.object_from_symbol("module_addr_max") + + if isinstance(modules_addr_min, objects.Void): + raise exceptions.VolatilityException( + "Your ISF symbols lack type information. You may need to update the" + "ISF using the latest version of dwarf2json" + ) + else: + raise exceptions.VolatilityException( + "Cannot find the module memory allocation area. Unsupported kernel" + ) + + return modules_addr_min, modules_addr_max + + @classmethod + def flatten_run_modules_results( + cls, run_results: Dict[str, List[ModuleInfo]], deduplicate: bool = True + ) -> List[ModuleInfo]: + """Flatten a dictionary mapping plugin names and modules list, to a single merged list. + This is useful to get a generic lookup list of all the detected modules. + + Args: + run_results: dictionary of plugin names mapping a list of detected modules + deduplicate: remove duplicate modules, based on their offsets + + Returns: + List of ModuleInfo objects + """ + uniq_modules: List[ModuleInfo] = [] + + seen_addresses: int = set() + + for modules in run_results.values(): + for module in modules: + if deduplicate and (module.start in seen_addresses): + continue + seen_addresses.add(module.start) + uniq_modules.append(module) + + return uniq_modules + + @classmethod + def get_hidden_modules( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, + ) -> 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 + in Volatility2, but it doesn't work with older kernels. + + From kernels 4.2 struct module allocation are aligned to the L1 cache line size. + In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in + the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can + also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json + doesn't support this feature yet. + In kernels < 4.2, alignment attributes are absent in the struct module, meaning + alignment cannot be guaranteed. Therefore, for older kernels, it's better to use + the traditional scan technique. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: + module objects + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + module_addr_min, module_addr_max = modules_memory_boundaries + module_address_alignment = cls.get_module_address_alignment( + context, vmlinux_module_name + ) + if not cls.validate_alignment_patterns( + known_module_addresses, module_address_alignment + ): + vollog.warning( + f"Module addresses aren't aligned to {module_address_alignment} bytes. " + "Switching to 1 byte alignment scan method." + ) + module_address_alignment = 1 + + mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") + mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") + offset_to_mkobj_mod = mkobj_offset + mod_offset + mod_member_template = vmlinux.get_type("module_kobject").child_template("mod") + mod_size = mod_member_template.size + mod_member_data_format = mod_member_template.data_format + + for module_addr in range( + module_addr_min, module_addr_max, module_address_alignment + ): + if module_addr in known_module_addresses: + continue + + try: + # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() + self_referential_bytes = vmlinux_layer.read( + module_addr + offset_to_mkobj_mod, mod_size + ) + self_referential = objects.convert_data_to_value( + self_referential_bytes, int, mod_member_data_format + ) + if self_referential != module_addr: + continue + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): + continue + + module = vmlinux.object("module", offset=module_addr, absolute=True) + if module and module.is_valid(): + yield module + + @classmethod + def get_module_address_alignment( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> int: + """Obtain the module memory address alignment. + + struct module is aligned to the L1 cache line, which is typically 64 bytes for most + common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this + will still work. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + The struct module alignment + """ + return context.modules[vmlinux_module_name].get_type("pointer").size + + @classmethod + def list_modules( + cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Lists all the modules in the primary layer. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + layer_name: The name of the layer on which to operate + vmlinux_symbols: The name of the table containing the kernel symbols + + Yields: + The modules present in the `layer_name` layer's modules list + + This function will throw a SymbolError exception if kernel module support is not enabled. + """ + vmlinux = context.modules[vmlinux_module_name] + + modules = vmlinux.object_from_symbol(symbol_name="modules").cast("list_head") + + table_name = vmlinux.symbol_table_name + + yield from modules.to_list(table_name + constants.BANG + "module", "list") + + @classmethod + def get_kset_modules( + cls, context: interfaces.context.ContextInterface, vmlinux_name: str + ) -> Dict[str, extensions.module]: + vmlinux = context.modules[vmlinux_name] + + try: + module_kset = vmlinux.object_from_symbol("module_kset") + except exceptions.SymbolError: + module_kset = None + + if not module_kset: + raise TypeError( + "This plugin requires the module_kset structure. This structure is not present in the supplied symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + ret = {} + + kobj_off = vmlinux.get_type("module_kobject").relative_child_offset("kobj") + + for kobj in module_kset.list.to_list( + vmlinux.symbol_table_name + constants.BANG + "kobject", "entry" + ): + mod_kobj = vmlinux.object( + object_type="module_kobject", + offset=kobj.vol.offset - kobj_off, + absolute=True, + ) + + mod = mod_kobj.mod + + try: + name = utility.pointer_to_string(kobj.name, 32) + except exceptions.InvalidAddressException: + continue + + if kobj.name and kobj.reference_count() > 2: + ret[name] = mod + + return ret + + @staticmethod + def validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation + + Returns: + True if all the addresses meet the alignment + """ + return all(addr % address_alignment == 0 for addr in addresses) + + @classmethod + def _get_param_handlers( + cls, context: interfaces.context.ContextInterface, vmlinux_name: str + ) -> Tuple[Dict[int, str], Dict[str, Optional[int]]]: + """ + This function builds the dictionaries needed to map kernel parameters to their types + We need these values and information to properly decode each parameter to its input representation + """ + kernel = context.modules[vmlinux_name] + + # All the integer type parameters + pairs = { + "param_get_invbool": "int", + "param_get_bool": "int", + "param_get_int": "int", + "param_get_ulong": "long unsigned int", + "param_get_ullong": "long long unsigned int", + "param_get_long": "long int", + "param_get_uint": "unsigned int", + "param_get_ushort": "short unsigned int", + "param_get_short": "short int", + "param_get_byte": "char", + } + + int_handlers: Dict[int, str] = {} + + for sym_name, val_type in pairs.items(): + try: + sym_address = kernel.get_absolute_symbol_address(sym_name) + except exceptions.SymbolError: + continue + + int_handlers[sym_address] = val_type + + # Strings, arrays, booleans + getters = { + "param_get_string": None, + "param_array_get": None, + "param_get_charp": None, + "param_get_bool": None, + "param_get_invbool": None, + } + + for sym_name in getters: + try: + sym_address = kernel.get_absolute_symbol_address(sym_name) + except exceptions.SymbolError: + continue + + getters[sym_name] = sym_address + + return int_handlers, getters + + @classmethod + def _get_param_val( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + int_handlers, + getters, + module, + param, + ) -> Optional[Union[str, int]]: + """ + Properly determines the type of a parameter and decodes based on the type. + The type is determined by examining its `get` function, which will be a pointer to + predefined operations handler for particular parameter types. + """ + + # Attempt to retrieve the `get` pointer. Bail if smeared + try: + if hasattr(param, "get"): + param_func = param.get + else: + param_func = param.ops.get + + except exceptions.InvalidAddressException: + return None + + if not param_func: + return None + + kernel = context.modules[vmlinux_name] + + # For arrays, recursively get the value of each member as the type can be different + if param_func == getters["param_array_get"]: + array = param.arr + + if array.num: + max_index = array.num.dereference() + else: + max_index = array.member("max") + + if max_index > 32: + vollog.debug( + f"Skipping array parameter with invalid index for module {module.vol.offset:#x}" + ) + return None + + element_vals = [] + for i in range(max_index): + kp = kernel.object( + object_type="kernel_param", + offset=array.elem + (array.elemsize * i), + absolute=True, + ) + + element_vals.append( + cls._get_param_val( + context, vmlinux_name, int_handlers, getters, module, kp + ) + ) + + # nothing was gathered + if not element_vals: + return None + + return ",".join([str(ele) for ele in element_vals]) + + # strings types + elif param_func in [getters["param_get_string"], getters["param_get_charp"]]: + try: + if param_func == getters["param_get_string"]: + count = param.member("str").maxlen + else: + count = 256 + + return utility.pointer_to_string(param.member("str"), count=count) + except exceptions.InvalidAddressException: + vollog.debug( + f"Skipping string parameter with invalid address for module {module.vol.offset:#x}" + ) + return None + + # The integer handles, which also encompass boolean handlers + elif param_func in int_handlers: + try: + int_value = kernel.object( + object_type=int_handlers[param_func], offset=param.arg + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"Skipping {int_handlers[param_func]} parameter with invalid address for module {module.vol.offset:#x}" + ) + return None + + if param_func == getters["param_get_bool"]: + if int_value == 0: + return "N" + else: + return "Y" + elif param_func == getters["param_get_invbool"]: + if int_value == 0: + return "Y" + else: + return "N" + else: + return int_value + + else: + handler_symbol = kernel.get_symbols_by_absolute_location(param_func) + + msg = f"Unknown kernel parameter handling function ({handler_symbol}) at address {param_func:#x} for module at {module.vol.offset:#x}" + + # If a new kernel has a handler symbol we don't support then we want to always see that information + # If the handler doesn't map to a kernel symbol then its smeared/invalid + if handler_symbol: + vollog.warning(msg) + else: + vollog.debug(msg) + + return None + + @classmethod + def get_load_parameters( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + module: extensions.module, + ) -> Generator[Tuple[str, Optional[Union[str, int]]], None, None]: + """ + Recovers the load parameters of the given kernel module + Returns a tuple (key,value) for each parameter + """ + if not hasattr(module, "kp"): + vollog.debug( + "kp member missing for struct module. Cannot recover parameters." + ) + return None + + if module.num_kp > 128: + vollog.debug( + f"Smeared number of parameters ({module.num_kp}) found for module at offset {module.vol.offset:#x}" + ) + return None + + kernel = context.modules[vmlinux_name] + + int_handlers, getters = cls._get_param_handlers(context, vmlinux_name) + + # Build the array of parameters + param_array = kernel.object( + object_type="array", + offset=module.kp.dereference().vol.offset, + subtype=kernel.get_type("kernel_param"), + count=module.num_kp, + absolute=True, + ) + + for i in range(len(param_array)): + try: + param = param_array[i] + name = utility.pointer_to_string(param.name, count=32) + except exceptions.InvalidAddressException: + vollog.debug( + f"Smeared load parameter module at offset {module.vol.offset:#x}" + ) + continue + + value = cls._get_param_val( + context, vmlinux_name, int_handlers, getters, module, param + ) + + yield name, value + + +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory +# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time +# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections +# There are also several other significant complications that we must deal with when trying to extract an LKM +# that can be analyzed with static analysis tools +# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections +# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense +# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory +# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, +# we create the .shstrtab based on the sections in memory and then glue it in as the final section + + +# ModuleExtract.extract_module is the entry point and only visible method for plugins +class ModuleExtract(interfaces.configuration.VersionableInterface): + """Extracts Linux kernel module structures into an analyzable ELF file""" + + _version = (1, 0, 2) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules_modules", + component=Modules, + version=(3, 0, 2), + ), + ] + + @classmethod + def _find_section( + cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int + ) -> Optional[Tuple[str, int, int, int]]: + """ + Finds the section containing `sym_address` + """ + for name, index, address, size in section_lookups: + if address <= sym_address < address + size: + return name, index, address, size + + return None + + @classmethod + def _get_st_info_for_sym( + cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str + ) -> bytes: + """ + This is a helper function called from `_fix_sym_table` + + Calculates the `st_info` value for the given symbol + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + if sym.st_name > 0: + # Global symbol + bind = linux_constants.STB_GLOBAL + + if sym_address == 0: + sect_type = linux_constants.STT_NOTYPE + elif sect_name: + # rela = relocations + if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1: + sect_type = linux_constants.STT_FUNC + else: + sect_type = linux_constants.STT_OBJECT + + else: + # outside the module being extracted + sect_type = linux_constants.STT_NOTYPE + + else: + # Local symbol + bind = linux_constants.STB_LOCAL + sect_type = linux_constants.STT_SECTION + + # Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO + bind_bits = (bind << 4) & 0xF0 + type_bits = sect_type & 0xF + + st_info_int = (bind_bits | type_bits) & 0xFF + + return struct.pack("B", st_info_int) + + @classmethod + def _get_fixed_sym_fields( + cls, + st_fmt: str, + sym: interfaces.objects.ObjectInterface, + sections: List[Tuple[str, int, int, int]], + ) -> Tuple[str, int, int, int]: + """ + This is a helper function called from `_fix_sym_table` + + The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading + Static analysis tools do not understand these transformed values as they only make sense to the kernel loader + We must de-mangle these to have analysis tools understand symbols (a key aspect) + """ + # Start by trying to map a symbol to its section + sym_address = sym.st_value + sect_info = cls._find_section(sections, sym_address) + + if not sect_info: + # Symbol does not point into the module being extracted + sect_name, sect_index, sect_address = None, None, None + st_value_int = sym_address + else: + # relative address inside the section + sect_name, sect_index, sect_address, _ = sect_info + st_value_int = sym_address - sect_address + + # Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file + + # formatted to be written into the extracted file + st_value = struct.pack(st_fmt, st_value_int) + + # returns formatted to be written into the extracted file + st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name) + + # format to reference its section, if any + if sect_name: + st_shndx = struct.pack(" Optional[bytes]: + """ + Args: + context: The context on which to operate. + vmlinux_name: The name of the kernel module. + original_sections: Dict of module section addresses and names. + section_sizes: Dict of module section addresses and sizes. + sym_type_name: ELF symbol type name (should be one of "Elf64_Sym" or "Elf32_Sym"). + st_fmt: "struct"-like unpack format string (should be one of " Optional[Tuple[List, int, int]]: + """ + This function first parses the sections as maintained by the kernel + It then orders the sections by load address, and then gathers the data of each section + We also track the file_offset to correctly have alignment in the output file + + .symtab requires special handling as its so broken in memory as described in `_fix_sym_table` + The data of .strtab is read directly off the module structure and not its section + as the section from the original module has no meaning after loading as the kernel does not reference it. + """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} + for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask + < modules_addr_max + ): + continue + + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: + return None + + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + sym_type = "Elf64_Sym" + elf_hdr_type = "Elf64_Ehdr" + st_fmt = " Optional[bytes]: + """ + Creates a `bits` bit ELF header for the file based on recovered values + Called last as it needs information computed from the sections + + Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + if bits == 32: + fmt = " Optional[int]: + """ + This function makes a best effort to map common section names + to their attributes + """ + known_sections = { + ".note.gnu.build-id": linux_constants.SHT_NOTE, + ".text": linux_constants.SHT_PROGBITS, + ".init.text": linux_constants.SHT_PROGBITS, + ".exit.text": linux_constants.SHT_PROGBITS, + ".static_call.text": linux_constants.SHT_PROGBITS, + ".rodata": linux_constants.SHT_PROGBITS, + ".modinfo": linux_constants.SHT_PROGBITS, + "__param": linux_constants.SHT_PROGBITS, + ".data": linux_constants.SHT_PROGBITS, + ".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS, + ".comment": linux_constants.SHT_PROGBITS, + ".shstrtab": linux_constants.SHT_STRTAB, + ".symtab": linux_constants.SHT_SYMTAB, + ".strtab": linux_constants.SHT_STRTAB, + } + + sect_type_val = linux_constants.SHT_PROGBITS + + if section_name.find(".rela.") != -1: + sect_type_val = linux_constants.SHT_RELA + + elif section_name in known_sections: + sect_type_val = known_sections[section_name] + + return sect_type_val + + # all sections from memory are allocated (SHF_ALLOC) + # special check certain other sections to try and ensure extra flags are added where needed + @classmethod + def _calc_sect_flags(cls, name: str) -> int: + """ + Make a best effort to map common section names to their permissions + If we miss a section here, users of common static analysis tools can mark the + sections are writable or executable manually, but that becomes very cumbersome + and breaks initial analysis by the tool + """ + # All sections in memory are allocated (`A` in readelf -S) + flags = linux_constants.SHF_ALLOC + + if name in [".text", ".init.text", ".exit.text", ".static_call.text"]: + flags = flags | linux_constants.SHF_EXECINSTR + + elif name in [ + ".data", + ".init.data", + ".exit.data", + ".bss", + "__tracepoints", + ".data.once", + "_ftrace_events", + ".gnu.linkonce.this_module", + ]: + flags = flags | linux_constants.SHF_WRITE + + return flags + + @classmethod + def _calc_link( + cls, name: str, strtab_index: int, symtab_index: int, sect_type: int + ) -> int: + """ + Calculates the link value for a section + + The most important ones are symtab indexes for relocations + and to point the symbol table to the string tab + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return symtab_index + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + return strtab_index + + return 0 + + @classmethod + def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int: + """ + Calculates the entsize for relocation sections and the symbol table section + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return 24 + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + if bits == 32: + return 16 + else: + return 24 + + return 0 + + @classmethod + def _make_section_header( + cls, + bits: int, + name_index: int, + name: str, + address: int, + size: int, + file_offset: int, + strtab_index: int, + symtab_index: int, + ) -> Optional[bytes]: + """ + Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section + """ + if bits == 32: + fmt = " Optional[bytes]: + # Bail early if bad address sent in + try: + hasattr(module.sect_attrs, "nsections") + except exceptions.InvalidAddressException: + vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.") + return None + + # Gather sections + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result + + kernel = context.modules[vmlinux_name] + + # Figure out header sizes + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + header_type = "Elf64_Ehdr" + section_type = "Elf64_Shdr" + bits = 64 + else: + header_type = "Elf32_Ehdr" + section_type = "Elf32_Shdr" + bits = 32 + + header_type_size = kernel.get_type(header_type).size + section_type_size = kernel.get_type(section_type).size + + # Per Linux-spec, all LKMs must start with a null section header + # This buffer is used to hold the headers as they are built + sections_headers = b"\x00" * section_type_size + + # Holder of the data of the sections + sections_data = b"" + + # the .shstrtab section is "\x00" + section name for each section + # followed by a terminating null. + # It starts with the null string (\x00) + shstrtab_data = b"\x00" + + # Track where we end the sections and data to glue `.shstrtab` after + last_file_offset = None + last_sect_size = None + + # Start at 1 in the string table + name_index = 1 + + # Create the actual section headers + for index, (name, address, file_offset, section_data) in enumerate( + updated_sections + ): + # Make the section header + header_bytes = cls._make_section_header( + bits, + name_index, + name, + address, + len(section_data), + file_offset, + strtab_index, + symtab_index, + ) + if not header_bytes: + vollog.debug(f"make_section_header failed for section {name}") + return None + + # ndex into the string table + name_index += len(name) + 1 + + # concatenate the header and section bytes + sections_headers += header_bytes + sections_data += section_data + + # track where we are so .shstrtab goes into correct offset + last_file_offset = file_offset + last_sect_size = len(section_data) + + # append each section name to what will become .shstrtab + shstrtab_data += bytes(name, encoding="utf8") + b"\x00" + + # stick our own section reference string at end + # name_index points to the end of the last section string after the loop ends + shstrtab_data += b".shstrtab\x00" + + # create our .shstrtab section so sections have names + sections_headers += cls._make_section_header( + bits, + name_index, + ".shstrtab", + 0, + len(shstrtab_data), + last_file_offset + last_sect_size, + strtab_index, + symtab_index, + ) + + sections_data += shstrtab_data + + num_sections = len(updated_sections) + 1 + + header = cls._make_elf_header( + bits, + header_type_size + len(sections_data), + num_sections, + ) + + if not header: + vollog.error( + f"Hit error creating Elf header for module at {module.vol.offset:#x}" + ) + return None + + # Return our beautiful, hand-crafted, farm raised ELF file + return header + sections_data + sections_headers + + +class ModuleGathererLsmod(ModuleGathererInterface): + """ + Gathers modules from the main kernel list + """ + + _version = (1, 0, 0) + + name = "Lsmod" + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + yield from Modules.list_modules(context, kernel_module_name) + + +class ModuleGathererSysFs(ModuleGathererInterface): + """ + Gathers modules from the sysfs /sys/modules objects + """ + + _version = (1, 0, 0) + + name = "SysFs" + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + kernel = context.modules[kernel_module_name] + + sysfs_modules: dict = Modules.get_kset_modules(context, kernel_module_name) + + for m_offset in sysfs_modules.values(): + yield kernel.object(object_type="module", offset=m_offset, absolute=True) + + +class ModuleGathererScanner(ModuleGathererInterface): + """ + Gathers modules by scanning memory + """ + + _version = (1, 0, 0) + + name = "Scanner" + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + modules_memory_boundaries = Modules.get_modules_memory_boundaries( + context, kernel_module_name + ) + + # Send in an empty list to not filter on any modules + yield from Modules.get_hidden_modules( + context=context, + vmlinux_module_name=kernel_module_name, + known_module_addresses=[], + modules_memory_boundaries=modules_memory_boundaries, + ) + + +class ModuleGathererKernel(ModuleGathererInterface): + """ + Creates a ModuleInfo instance for the kernel so that plugins + can determine when function pointers reference the kernel + """ + + _version = (1, 0, 0) + + name = "kernel" + + @classmethod + def gather_modules( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> ModuleGathererInterface.gatherer_return_type: + """ + Returns a ModuleInfo instance that encodes the kernel + This is required to map function pointers to the kernel executable + """ + kernel = context.modules[kernel_module_name] + + address_mask = context.layers[kernel.layer_name].address_mask + + start_addr = kernel.object_from_symbol("_text") + start_addr = start_addr.vol.offset & address_mask + + end_addr = kernel.object_from_symbol("_etext") + end_addr = end_addr.vol.offset & address_mask + + yield ModuleInfo(start_addr, constants.linux.KERNEL_NAME, start_addr, end_addr) + + +class ModuleGatherers( + interfaces.configuration.VersionableInterface, + interfaces.configuration.ConfigurableInterface, +): + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + # Valid sources of cores kernel module gatherers to send to `run_module_scanners` + # With few exceptions, rootkit checking plugins want all sources + # This provides a stable identifier as new sources are added over time + all_gatherers_identifier = [ + ModuleGathererLsmod, + ModuleGathererSysFs, + ModuleGathererScanner, + ModuleGathererKernel, + ] + + @classmethod + def get_requirements(cls): + reqs = [] + + # for now, all versions are 1, this will be broken out if/when that changes + for gatherer in ModuleGatherers.all_gatherers_identifier: + reqs.append( + requirements.VersionRequirement( + name=gatherer.name.replace(" ", ""), + component=gatherer, + version=(1, 0, 0), + ) + ) + + return reqs + + +class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): + """ + Plugins that enumerate kernel modules (lsmod, check_modules, etc.) + must inherit from this class to have unified output columns across plugins. + The constructor of the plugin must call super() with the `implementation` set + """ + + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules", + component=Modules, + version=(3, 0, 1), + ), + requirements.VersionRequirement( + name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) + ), + ] + + @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 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 {implementation}" + ) + continue + + code_size = format_hints.Hex( + module.get_init_size() + module.get_core_size() + ) + + taints = ",".join( + tainting.Tainting.get_taints_parsed( + context, kernel_module_name, module.taints, True + ) + ) + + parameters_iter = Modules.get_load_parameters( + context, kernel_module_name, module + ) + + parameters = ", ".join([f"{key}={value}" for key, value in parameters_iter]) + + file_name = renderers.NotApplicableValue() + + if dump and open_implementation: + elf_data = ModuleExtract.extract_module( + context, kernel_module_name, module + ) + if not elf_data: + vollog.warning( + f"Unable to reconstruct the ELF for module struct at {module.vol.offset:#x}" + ) + file_name = renderers.NotAvailableValue() + else: + file_name = open_implementation.sanitize_filename( + f"kernel_module.{name}.{module.vol.offset:#x}.elf" + ) + + 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, + ), + ) + + 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/linux/utilities/tainting.py b/volatility3/framework/symbols/linux/utilities/tainting.py new file mode 100644 index 000000000..cb81ab1a3 --- /dev/null +++ b/volatility3/framework/symbols/linux/utilities/tainting.py @@ -0,0 +1,167 @@ +import functools + +from volatility3 import framework +from volatility3.framework import interfaces +from volatility3.framework.constants import linux as linux_constants +from typing import List, Optional + + +class Tainting(interfaces.configuration.VersionableInterface): + """Tainted kernel and modules parsing capabilities. + + Relevant Linux kernel functions: + - modules: module_flags_taint + - kernel: print_tainted + """ + + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + @functools.lru_cache + def _get_kernel_taint_flags_list( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ) -> Optional[List[interfaces.objects.ObjectInterface]]: + """Determine whether the kernel embeds taint flags definition + in-memory or not. + + Returns: + A list of "taint_flag" kernel objects if taint_flags symbol exists + """ + kernel = context.modules[kernel_module_name] + if kernel.has_symbol("taint_flags"): + return list(kernel.object_from_symbol("taint_flags")) + return None + + @classmethod + def _module_flags_taint_pre_4_10_rc1( + cls, + taints: int, + is_module: bool = False, + ) -> str: + """Convert the module's taints value to a 1-1 character mapping. + Relies on statically defined taints mappings in the framework. + + Args: + taints: The taints value, represented by an integer + is_module: Indicates if the taints value is associated with a built-in/LKM module + + Returns: + The raw taints string. + """ + taints_string = "" + for char, taint_flag in linux_constants.TAINT_FLAGS.items(): + if is_module and not taint_flag.module: + continue + + if taints & taint_flag.shift: + taints_string += char + + return taints_string + + @classmethod + def _module_flags_taint_post_4_10_rc1( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + taints: int, + is_module: bool = False, + ) -> str: + """Convert the module's taints value to a 1-1 character mapping. + Relies on kernel symbol embedded taints definitions. + + struct taint_flag { + char c_true; /* character printed when tainted */ + char c_false; /* character printed when not tainted */ + bool module; /* also show as a per-module taint flag */ + }; + + Args: + taints: The taints value, represented by an integer + is_module: Indicates if the taints value is associated with a built-in/LKM module + + Returns: + The raw taints string. + """ + taints_string = "" + for taint_bit, taint_flag in enumerate( + cls._get_kernel_taint_flags_list(context, kernel_module_name) + ): + if is_module and not taint_flag.module: + continue + + try: + c_true = chr(taint_flag.c_true) + c_false = chr(taint_flag.c_false) + except ValueError: + # thrown when the c_true or c_false values are out of range + continue + + if taints & (1 << taint_bit): + taints_string += c_true + elif c_false != " ": + taints_string += c_false + + return taints_string + + @classmethod + def get_taints_as_plain_string( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + taints: int, + is_module: bool = False, + ) -> str: + """Convert the taints value to a 1-1 character mapping. + + Args: + taints: The taints value, represented by an integer + is_module: Indicates if the taints value is associated with a built-in/LKM module + Returns: + The raw taints string. + + Documentation: + - module_flags_taint kernel function + """ + + if cls._get_kernel_taint_flags_list(context, kernel_module_name): + return cls._module_flags_taint_post_4_10_rc1( + context, kernel_module_name, taints, is_module + ) + return cls._module_flags_taint_pre_4_10_rc1(taints, is_module) + + @classmethod + def get_taints_parsed( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + taints: int, + is_module: bool = False, + ) -> List[str]: + """Convert the taints string to a 1-1 descriptor mapping. + + Args: + taints: The taints value, represented by an integer + is_module: Indicates if the taints value is associated with a built-in/LKM module + + Returns: + A comprehensive (user-friendly) taint descriptor list. + + Documentation: + - module_flags_taint kernel function + """ + comprehensive_taints = [] + for character in cls.get_taints_as_plain_string( + context, kernel_module_name, taints, is_module + ): + taint_flag = linux_constants.TAINT_FLAGS.get(character) + if not taint_flag: + comprehensive_taints.append(f"") + elif taint_flag.when_present: + comprehensive_taints.append(taint_flag.desc) + + return comprehensive_taints diff --git a/volatility3/framework/symbols/mac/__init__.py b/volatility3/framework/symbols/mac/__init__.py index c695ca77a..dc54a8371 100644 --- a/volatility3/framework/symbols/mac/__init__.py +++ b/volatility3/framework/symbols/mac/__init__.py @@ -1,7 +1,7 @@ # 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 # -from typing import Iterator, Any, Iterable, List, Tuple, Set +from typing import Iterator, Any, Iterable, List, Optional, Tuple, Set from volatility3.framework import interfaces, objects, exceptions, constants from volatility3.framework.symbols import intermed @@ -97,7 +97,7 @@ class MacUtilities(interfaces.configuration.VersionableInterface): context: interfaces.context.ContextInterface, handlers: Iterator[Any], target_address, - kernel_module_name: str = None, + kernel_module_name: Optional[str] = None, ): mod_name = "UNKNOWN" symbol_name = "N/A" @@ -232,10 +232,9 @@ class MacUtilities(interfaces.configuration.VersionableInterface): next_member: str, max_elements: int = 4096, ) -> Iterable[interfaces.objects.ObjectInterface]: - for element in cls._walk_iterable( + yield from cls._walk_iterable( queue, "tqh_first", "tqe_next", next_member, max_elements - ): - yield element + ) @classmethod def walk_list_head( @@ -244,10 +243,9 @@ class MacUtilities(interfaces.configuration.VersionableInterface): next_member: str, max_elements: int = 4096, ) -> Iterable[interfaces.objects.ObjectInterface]: - for element in cls._walk_iterable( + yield from cls._walk_iterable( queue, "lh_first", "le_next", next_member, max_elements - ): - yield element + ) @classmethod def walk_slist( @@ -256,7 +254,6 @@ class MacUtilities(interfaces.configuration.VersionableInterface): next_member: str, max_elements: int = 4096, ) -> Iterable[interfaces.objects.ObjectInterface]: - for element in cls._walk_iterable( + yield from cls._walk_iterable( queue, "slh_first", "sle_next", next_member, max_elements - ): - yield element + ) diff --git a/volatility3/framework/symbols/mac/extensions/__init__.py b/volatility3/framework/symbols/mac/extensions/__init__.py index 15fe7aeda..08ec63afa 100644 --- a/volatility3/framework/symbols/mac/extensions/__init__.py +++ b/volatility3/framework/symbols/mac/extensions/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import contextlib +import functools import logging from typing import Generator, Iterable, Optional, Set, Tuple @@ -17,8 +18,9 @@ class proc(generic.GenericIntelProcess): def get_task(self): return self.task.dereference().cast("task") + @functools.lru_cache def add_process_layer( - self, config_prefix: str = None, preferred_name: str = None + self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None ) -> Optional[str]: """Constructs a new layer based on the process's DTB. @@ -237,7 +239,7 @@ class vm_map_entry(objects.StructType): def get_path(self, context, config_prefix): node = self.get_vnode(context, config_prefix) - if type(node) == str and node == "sub_map": + if type(node) is str and node == "sub_map": ret = node elif node: path = [] diff --git a/volatility3/framework/symbols/metadata.py b/volatility3/framework/symbols/metadata.py index 95f542f07..ea635f1f1 100644 --- a/volatility3/framework/symbols/metadata.py +++ b/volatility3/framework/symbols/metadata.py @@ -4,8 +4,7 @@ import datetime import logging -from typing import Optional, Tuple, Union - +from typing import Optional, Tuple, Union, List, Dict from volatility3.framework import constants, interfaces vollog = logging.getLogger(__name__) @@ -19,9 +18,16 @@ class ProducerMetadata(interfaces.symbols.MetadataInterface): return self._json_data.get("name", None) @property - def version(self) -> Optional[Tuple[int]]: + def version_string(self) -> str: + """Returns the ISF file producer's version as a string. + If no version is present, an empty string is returned. + """ + return self._json_data.get("version", "") + + @property + def version(self) -> Optional[Tuple[int, ...]]: """Returns the version of the ISF file producer""" - version = self._json_data.get("version", None) + version = self.version_string if not version: return None if all(x in "0123456789." for x in version): @@ -79,5 +85,21 @@ class WindowsMetadata(interfaces.symbols.MetadataInterface): return self._json_data.get("pdb", {}).get("age", None) -class LinuxMetadata(interfaces.symbols.MetadataInterface): +class PosixMetadata(interfaces.symbols.MetadataInterface): + """Base class to handle metadata of Posix-based ISF sources""" + + def get_types_sources(self) -> List[Optional[Dict]]: + """Returns the types sources metadata""" + return self._json_data.get("types", []) + + def get_symbols_sources(self) -> List[Optional[Dict]]: + """Returns the symbols sources metadata""" + return self._json_data.get("symbols", []) + + +class LinuxMetadata(PosixMetadata): """Class to handle the metadata from a Linux symbol table.""" + + +class MacMetadata(PosixMetadata): + """Class to handle the metadata from a Mac symbol table.""" diff --git a/volatility3/framework/symbols/native.py b/volatility3/framework/symbols/native.py index 7c3e1b312..61417532e 100644 --- a/volatility3/framework/symbols/native.py +++ b/volatility3/framework/symbols/native.py @@ -30,7 +30,7 @@ class NativeTable(interfaces.symbols.NativeTableInterface): @property def types(self) -> Iterable[str]: - """Returns an iterator of the symbol type names.""" + """Returns an iterable (set) of the available symbol type names.""" return self._types def get_type(self, type_name: str) -> interfaces.objects.Template: diff --git a/volatility3/framework/symbols/windows/__init__.py b/volatility3/framework/symbols/windows/__init__.py index c1b7894ff..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) @@ -48,7 +49,9 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable): # This doesn't exist in very specific versions of windows with contextlib.suppress(ValueError): - if self.get_type("_POOL_TRACKER_BIG_PAGES").has_member("PoolType"): + if self.get_type("_POOL_TRACKER_BIG_PAGES").has_member( + "PoolType" + ) or self.get_type("_POOL_TRACKER_BIG_PAGES").has_member("SlushSize"): self.set_type_class("_POOL_HEADER", pool.POOL_HEADER_VISTA) else: self.set_type_class("_POOL_HEADER", pool.POOL_HEADER) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index ecfc2f163..f32415124 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -18,11 +18,10 @@ from volatility3.framework import ( renderers, symbols, ) -from volatility3.framework.interfaces.objects import ObjectInterface from volatility3.framework.layers import intel from volatility3.framework.objects import utility from volatility3.framework.renderers import conversion -from volatility3.framework.symbols import generic +from volatility3.framework.symbols import generic, windows from volatility3.framework.symbols.windows.extensions import pool vollog = logging.getLogger(__name__) @@ -52,7 +51,9 @@ class MMVAD_SHORT(objects.StructType): # the offset is different on 32 and 64 bits symbol_table_name = self.vol.type_name.split(constants.BANG)[0] - if not symbols.symbol_table_is_64bit(self._context, symbol_table_name): + if not symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ): vad_address -= 4 else: vad_address -= 12 @@ -262,14 +263,20 @@ class MMVAD_SHORT(objects.StructType): def get_commit_charge(self): """Get the VAD's commit charge (number of committed pages)""" - if self.has_member("u1") and self.u1.has_member("VadFlags1"): + if self.has_member("CommitCharge"): + return self.CommitCharge + + elif self.has_member("u1") and self.u1.has_member("VadFlags1"): return self.u1.VadFlags1.CommitCharge elif self.has_member("u") and self.u.has_member("VadFlags"): return self.u.VadFlags.CommitCharge elif self.has_member("Core"): - return self.Core.u1.VadFlags1.CommitCharge + if self.Core.has_member("CommitCharge"): + return self.Core.CommitCharge + else: + return self.Core.u1.VadFlags1.CommitCharge raise AttributeError("Unable to find the commit charge member") @@ -382,7 +389,9 @@ class EX_FAST_REF(objects.StructType): # the mask value is different on 32 and 64 bits symbol_table_name = self.vol.type_name.split(constants.BANG)[0] - if not symbols.symbol_table_is_64bit(self._context, symbol_table_name): + if not symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ): max_fast_ref = 7 else: max_fast_ref = 15 @@ -403,12 +412,28 @@ class DEVICE_OBJECT(objects.StructType, pool.ExecutiveObject): header = self.get_object_header() return header.NameInfo.Name.String # type: ignore - def get_attached_devices(self) -> Generator[ObjectInterface, None, None]: + def get_attached_devices( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Enumerate the attached device's objects""" - device = self.AttachedDevice.dereference() + seen = set() + + try: + device = self.AttachedDevice.dereference() + except exceptions.InvalidAddressException: + return + while device: + if device.vol.offset in seen: + break + seen.add(device.vol.offset) + yield device - device = device.AttachedDevice.dereference() + + try: + device = device.AttachedDevice.dereference() + except exceptions.InvalidAddressException: + return class DRIVER_OBJECT(objects.StructType, pool.ExecutiveObject): @@ -419,12 +444,26 @@ class DRIVER_OBJECT(objects.StructType, pool.ExecutiveObject): header = self.get_object_header() return header.NameInfo.Name.String # type: ignore - def get_devices(self) -> Generator[ObjectInterface, None, None]: + def get_devices(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Enumerate the driver's device objects""" - device = self.DeviceObject.dereference() + seen = set() + + try: + device = self.DeviceObject.dereference() + except exceptions.InvalidAddressException: + return + while device: + if device.vol.offset in seen: + return + seen.add(device.vol.offset) + yield device - device = device.NextDevice.dereference() + + try: + device = device.NextDevice.dereference() + except exceptions.InvalidAddressException: + return def is_valid(self) -> bool: """Determine if the object is valid.""" @@ -519,7 +558,8 @@ class ETHREAD(objects.StructType, pool.ExecutiveObject): if not isinstance(ctime, datetime.datetime): return False - if not (1998 < ctime.year < 2030): + current_year = datetime.datetime.now().year + if not (1998 < ctime.year < current_year + 10): return False except exceptions.InvalidAddressException: @@ -528,16 +568,20 @@ class ETHREAD(objects.StructType, pool.ExecutiveObject): # passed all validations return True - def get_create_time(self): + def get_create_time( + self, + ) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]: # For Windows XPs if self.has_member("ThreadsProcess"): return conversion.wintime_to_datetime(self.CreateTime.QuadPart >> 3) return conversion.wintime_to_datetime(self.CreateTime.QuadPart) - def get_exit_time(self): + def get_exit_time( + self, + ) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]: return conversion.wintime_to_datetime(self.ExitTime.QuadPart) - def owning_process(self) -> interfaces.objects.ObjectInterface: + def owning_process(self) -> "EPROCESS": """Return the EPROCESS that owns this thread.""" # For Windows XPs @@ -665,7 +709,11 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return False # NT pids are divisible by 4 - if self.UniqueProcessId % 4 != 0: + if ( + self.UniqueProcessId % 4 != 0 + or self.UniqueProcessId == 0 + or self.UniqueProcessId > constants.windows.MAX_PID + ): return False # check for all 0s besides the PCID entries @@ -692,7 +740,10 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return True - def add_process_layer(self, config_prefix: str = None, preferred_name: str = None): + @functools.lru_cache + def add_process_layer( + self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None + ) -> str: """Constructs a new layer based on the process's DirectoryTableBase.""" parent_layer = self._context.layers[self.vol.layer_name] @@ -744,44 +795,136 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): ) return peb - def load_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: - """Generator for DLLs in the order that they were loaded.""" + def get_peb32(self) -> Optional[interfaces.objects.ObjectInterface]: + """Constructs a PEB32 object""" + if constants.BANG not in self.vol.type_name: + raise ValueError( + f"Invalid symbol table name syntax (no {constants.BANG} found)" + ) + + # add_process_layer can raise InvalidAddressException. + # if that happens, we let the exception propagate upwards + proc_layer_name = self.add_process_layer() + proc_layer = self._context.layers[proc_layer_name] + + # Determine if process is running under WOW64. + if self.get_is_wow64(): + proc = self.get_wow_64_process() + else: + return None + # Confirm WoW64Process points to a valid process address + if not proc_layer.is_valid(proc): + raise exceptions.InvalidAddressException( + proc_layer_name, proc, f"Invalid Wow64Process address at {self.Peb:0x}" + ) + + # Leverage the context of existing symbol table to help configure + # a new symbol table for 32-bit types + sym_table = self.get_symbol_table_name() + config_path = self._context.symbol_space[sym_table].config_path + + # Load the 32-bit types into a new symbol space + # We use the WindowsKernelIntermedSymbols class to make + # sure we get all the object helpers. For example, traversing + # linked-lists. + self._32bit_table_name = windows.WindowsKernelIntermedSymbols.create( + self._context, config_path, "windows", "wow64" + ) + + # windows 10 + if self._context.symbol_space.has_type( + sym_table + constants.BANG + "_EWOW64PROCESS" + ): + offset = proc.Peb + + # vista sp0-sp1 and 2003 sp1-sp2 + elif self._context.symbol_space.has_type( + sym_table + constants.BANG + "_WOW64_PROCESS" + ): + offset = proc.Wow64 + + else: + offset = proc + + peb32 = self._context.object( + f"{self._32bit_table_name}{constants.BANG}_PEB32", + layer_name=proc_layer_name, + offset=offset, + ) + return peb32 + + def set_types(self, peb) -> str: + ldr_data = self._context.symbol_space.get_type( + self._32bit_table_name + constants.BANG + "_PEB_LDR_DATA" + ) + peb.Ldr = peb.Ldr.cast("pointer", subtype=ldr_data) + sym_table = self._32bit_table_name + return sym_table + + def _walk_ldr_list( + self, list_member: str, link_member: str + ) -> Iterable[interfaces.objects.ObjectInterface]: + """ + Walks LDR_DATA_TABLEs and enforces the entries at least have a valid base address + This function also breaks up exception handling as much as possible to ensure the + most data is returned as possible + """ + pebs = [] try: peb = self.get_peb() - for entry in peb.Ldr.InLoadOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InLoadOrderLinks", - ): - yield entry + if peb: + pebs.append(peb) except exceptions.InvalidAddressException: - return None + vollog.debug(f"Process at {self.vol.offset:#x} has invalid PEB") + + try: + peb32 = self.get_peb32() + if peb32: + pebs.append(peb32) + except exceptions.InvalidAddressException: + vollog.debug(f"Process at {self.vol.offset:#x} has invalid 32 bit PEB") + + for peb in pebs: + sym_table = self.get_symbol_table_name() + # Fixes #1636 + try: + peb.Ldr + except exceptions.InvalidAddressException: + continue + + if peb.Ldr.vol.type_name.split(constants.BANG)[-1] == ("unsigned long"): + sym_table = self.set_types(peb) + + for ldr in peb.Ldr.member(list_member).to_list( + f"{sym_table}{constants.BANG}" + "_LDR_DATA_TABLE_ENTRY", link_member + ): + try: + # Several samples in testing crashed from DLLs being returned + # where DllBase was on the next page and that page was not in memory + # Not being able to retrieve the base makes the entry pretty useless + # So we enforce here its presence + ldr.DllBase + yield ldr + except exceptions.InvalidAddressException: + continue + + def load_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Generator for DLLs in the order that they were loaded.""" + + yield from self._walk_ldr_list("InLoadOrderModuleList", "InLoadOrderLinks") def init_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: """Generator for DLLs in the order that they were initialized""" - try: - peb = self.get_peb() - for entry in peb.Ldr.InInitializationOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InInitializationOrderLinks", - ): - yield entry - except exceptions.InvalidAddressException: - return None + yield from self._walk_ldr_list( + "InInitializationOrderModuleList", "InInitializationOrderLinks" + ) def mem_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: """Generator for DLLs in the order that they appear in memory""" - try: - peb = self.get_peb() - for entry in peb.Ldr.InMemoryOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InMemoryOrderLinks", - ): - yield entry - except exceptions.InvalidAddressException: - return None + yield from self._walk_ldr_list("InMemoryOrderModuleList", "InMemoryOrderLinks") def get_handle_count(self): try: @@ -797,28 +940,51 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return renderers.UnreadableValue() - def get_session_id(self): + def get_session_id(self) -> Union[int, interfaces.renderers.BaseAbsentValue]: try: if self.has_member("Session"): if self.Session == 0: return renderers.NotApplicableValue() symbol_table_name = self.get_symbol_table_name() - kvo = self._context.layers[self.vol.native_layer_name].config[ - "kernel_virtual_offset" - ] + kvo = self._context.layers[self.vol.native_layer_name].config.get( + "kernel_virtual_offset", None + ) + if not kvo: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) + ntkrnlmp = self._context.module( symbol_table_name, layer_name=self.vol.native_layer_name, offset=kvo, native_layer_name=self.vol.native_layer_name, ) - session = ntkrnlmp.object( - object_type="_MM_SESSION_SPACE", offset=self.Session, absolute=True - ) - - if session.has_member("SessionId"): - return session.SessionId + try: + session = ntkrnlmp.object( + object_type="_MM_SESSION_SPACE", + offset=self.Session, + absolute=True, + ) + if session.has_member("SessionId"): + return session.SessionId + except exceptions.SymbolError: + # In Windows 11 24H2, the _MM_SESSION_SPACE type was + # replaced with _PSP_SESSION_SPACE, and the kernel PDB + # doesn't contain information about its members (otherwise, + # we would just fall back to the new type). However, it + # appears to be, for our purposes, functionally identical + # to the _MM_SESSION_SPACE. Because _MM_SESSION_SPACE + # stores its session ID at offset 8 as an unsigned long, we + # create an unsigned long at that offset and use that + # instead. + session_id = ntkrnlmp.object( + object_type="unsigned long", + offset=self.Session + 8, + absolute=True, + ) + return session_id except exceptions.InvalidAddressException: vollog.log( @@ -916,56 +1082,55 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable): ) -> Iterator[interfaces.objects.ObjectInterface]: """Returns an iterator of the entries in the list.""" - layer = layer or self.vol.layer_name + layer_name = layer or self.vol.layer_name + native_layer_name = layer_name or self.vol.native_layer_name + + trans_layer = self._context.layers[layer_name] + if not trans_layer.is_valid(self.vol.offset): + return None relative_offset = self._context.symbol_space.get_type( symbol_type ).relative_child_offset(member) - direction = "Blink" - if forward: - direction = "Flink" + direction = "Flink" if forward else "Blink" - trans_layer = self._context.layers[layer] - - try: - is_valid = trans_layer.is_valid(self.vol.offset) - if not is_valid: - return None - - link = getattr(self, direction).dereference() - except exceptions.InvalidAddressException: + link_ptr = getattr(self, direction) + if not (link_ptr and link_ptr.is_readable()): return None + link = link_ptr.dereference() if not sentinel: + obj_offset = self.vol.offset - relative_offset + if not trans_layer.is_valid(obj_offset): + return None + yield self._context.object( symbol_type, - layer, - offset=self.vol.offset - relative_offset, - native_layer_name=layer or self.vol.native_layer_name, + layer_name, + offset=obj_offset, + native_layer_name=native_layer_name, ) seen = {self.vol.offset} while link.vol.offset not in seen: obj_offset = link.vol.offset - relative_offset - if not trans_layer.is_valid(obj_offset): return None - obj = self._context.object( + yield self._context.object( symbol_type, - layer, + layer_name, offset=obj_offset, - native_layer_name=layer or self.vol.native_layer_name, + native_layer_name=native_layer_name, ) - yield obj seen.add(link.vol.offset) - try: - link = getattr(link, direction).dereference() - except exceptions.InvalidAddressException: + link_ptr = getattr(link, direction) + if not (link_ptr and link_ptr.is_readable()): return None + link = link_ptr.dereference() def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) @@ -979,7 +1144,13 @@ class TOKEN(objects.StructType): if self.UserAndGroupCount < 0xFFFF: layer_name = self.vol.layer_name - kvo = self._context.layers[layer_name].config["kernel_virtual_offset"] + kvo = self._context.layers[layer_name].config.get( + "kernel_virtual_offset", None + ) + if not kvo: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) symbol_table = self.get_symbol_table_name() ntkrnlmp = self._context.module( symbol_table, layer_name=layer_name, offset=kvo @@ -1059,31 +1230,22 @@ class KTIMER(objects.StructType): return "Yes" return "-" - def get_raw_dpc(self): - """Returns the encoded DPC since it may not look like a pointer after encoding""" - symbol_table_name = self.get_symbol_table_name() - pointer_type = self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ) - - return self._context.object( - object_type=pointer_type, - layer_name=self.vol.layer_name, - offset=self.Dpc.vol.offset, - ) - def valid_type(self): return self.Header.Type in self.VALID_TYPES def get_due_time(self): - return "{0:#010x}:{1:#010x}".format(self.DueTime.HighPart, self.DueTime.LowPart) + return f"{self.DueTime.HighPart:#010x}:{self.DueTime.LowPart:#010x}" def get_dpc(self): """Return Dpc, and if Windows 7 or later, decode it""" symbol_table_name = self.get_symbol_table_name() - kvo = self._context.layers[self.vol.native_layer_name].config[ - "kernel_virtual_offset" - ] + kvo = self._context.layers[self.vol.native_layer_name].config.get( + "kernel_virtual_offset", None + ) + if not kvo: + raise ValueError( + "Intel layer does not have an associated kernel virtual offset, failing" + ) ntkrnlmp = self._context.module( symbol_table_name, layer_name=self.vol.native_layer_name, @@ -1102,7 +1264,7 @@ class KTIMER(objects.StructType): ) low_byte = (wait_never) & 0xFF - entry = utility.rol(self.get_raw_dpc() ^ wait_never, low_byte) + entry = utility.rol(self.Dpc.get_raw_value() ^ wait_never, low_byte) swap_xor = self._context.layers[self.vol.native_layer_name].canonicalize( self.vol.offset ) @@ -1244,7 +1406,9 @@ class CONTROL_AREA(objects.StructType): ) mmpte_size = mmpte_type.size subsection = self.get_subsection() - is_64bit = symbols.symbol_table_is_64bit(self._context, symbol_table_name) + is_64bit = symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ) is_pae = self._context.layers[self.vol.layer_name].metadata.get("pae", False) # the sector_size is used as a multiplier to the StartingSector @@ -1383,7 +1547,7 @@ class SHARED_CACHE_MAP(objects.StructType): ) # Iterate through the entries - for counter in range(0, self.VACB_ARRAY): + for counter in range(self.VACB_ARRAY): # Check if the VACB entry is in use if not vacb_array[counter]: continue @@ -1467,7 +1631,7 @@ class SHARED_CACHE_MAP(objects.StructType): if not section_size > self.VACB_SIZE_OF_FIRST_LEVEL: array_head = vacb_obj - for counter in range(0, full_blocks): + for counter in range(full_blocks): vacb_entry = self._context.object( symbol_table_name + constants.BANG + "pointer", layer_name=self.vol.layer_name, @@ -1526,7 +1690,7 @@ class SHARED_CACHE_MAP(objects.StructType): # Walk the array and if any entry points to the shared cache map object then we extract it. # Otherwise, if it is non-zero, then traverse to the next level. - for counter in range(0, self.VACB_ARRAY): + for counter in range(self.VACB_ARRAY): if not vacb_array[counter]: continue @@ -1546,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/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 2312149c7..b16855b9c 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -73,7 +73,7 @@ class ROW(objects.StructType): ) for i in range(0, len(char_row), 3) ) - except Exception as e: + except Exception: line = "" if truncate: @@ -107,11 +107,10 @@ class EXE_ALIAS_LIST(objects.StructType): def get_aliases(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Generator for the individual aliases for a particular executable.""" - for alias in self.AliasList.to_list( + yield from self.AliasList.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_ALIAS", "ListEntry", - ): - yield alias + ) class SCREEN_INFORMATION(objects.StructType): @@ -169,7 +168,7 @@ class SCREEN_INFORMATION(objects.StructType): @param truncate: True if the empty rows at the end (i.e. bottom) of the screen buffer should be - supressed. + suppressed. """ rows = [] @@ -245,11 +244,10 @@ class CONSOLE_INFORMATION(objects.StructType): def get_histories( self, ) -> Generator[interfaces.objects.ObjectInterface, None, None]: - for cmd_hist in self.HistoryList.to_list( + yield from self.HistoryList.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_COMMAND_HISTORY", "ListEntry", - ): - yield cmd_hist + ) def get_exe_aliases( self, @@ -258,20 +256,18 @@ class CONSOLE_INFORMATION(objects.StructType): # Windows 10 22000 and Server 20348 made this a Pointer if isinstance(exe_alias_list, objects.Pointer): exe_alias_list = exe_alias_list.dereference() - for exe_alias_list_item in exe_alias_list.to_list( + yield from exe_alias_list.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_EXE_ALIAS_LIST", "ListEntry", - ): - yield exe_alias_list_item + ) def get_processes( self, ) -> Generator[interfaces.objects.ObjectInterface, None, None]: - for proc in self.ConsoleProcessList.to_list( + yield from self.ConsoleProcessList.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_CONSOLE_PROCESS_LIST", "ListEntry", - ): - yield proc + ) def get_title(self) -> Union[str, None]: try: @@ -393,8 +389,7 @@ class COMMAND_HISTORY(objects.StructType): rest are coalesced. """ - for i, cmd in self.scan_command_bucket(self.CommandBucket.End): - yield i, cmd + yield from self.scan_command_bucket(self.CommandBucket.End) win10_x64_class_types = { diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py new file mode 100644 index 000000000..000c50319 --- /dev/null +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -0,0 +1,332 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import Optional, Tuple, Iterator, Generator + +from volatility3 import framework +from volatility3.framework import exceptions, constants, interfaces +from volatility3.framework import objects +from volatility3.framework.objects import utility +from volatility3.framework.symbols.windows import extensions +from volatility3.framework.symbols.windows.extensions import pool + +vollog = logging.getLogger(__name__) + + +class GUIExtensions(interfaces.configuration.VersionableInterface): + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + framework.require_interface_version(*_required_framework_version) + + class tagWINDOWSTATION(objects.StructType, pool.ExecutiveObject): + def is_valid(self) -> bool: + sid = self.get_session_id() + return sid is not None and 0 <= sid < 256 + + def get_session_id(self) -> Optional[int]: + try: + return self.dwSessionId + except exceptions.InvalidAddressException: + return None + + def traverse(self, max_stations: int = 15): + """ + Traverses the window stations referenced in the list of stations + """ + seen = set() + + # include the first window station + yield self + + while len(seen) < max_stations: + try: + winsta = self.rpwinstaNext.dereference() + except exceptions.InvalidAddressException: + break + + if winsta.vol.offset in seen: + break + + yield winsta + + seen.add(winsta.vol.offset) + + def get_info(self, kernel_symbol_table_name) -> Optional[Tuple[str, int]]: + try: + name = self.get_name(kernel_symbol_table_name) + session_id = self.get_session_id() + except exceptions.InvalidAddressException: + return None, None + + # attempt to avoid smear + if session_id is not None and session_id < 256 and name and len(name) > 1: + return name, session_id + + return None, None + + def desktops(self, symbol_table_name, max_desktops: int = 12): + seen = set() + + while len(seen) < max_desktops: + try: + desktop = self.rpdeskList.dereference() + name = desktop.get_name(symbol_table_name) + except exceptions.InvalidAddressException: + break + + if desktop.vol.offset in seen: + break + + yield desktop, name + + seen.add(desktop.vol.offset) + + class tagDESKTOP(objects.StructType, pool.ExecutiveObject): + def is_valid(self) -> bool: + """ + Enforce a valid session ID and Window station + We aren't interested in terminated desktops as there are so many pointers + going from station -> desktop -> windows, that we would just be processing junk. + Even if the pointers were still in tact by some miracle, its not that helpful to + have a floating desktop appear in the output as you can't do much with it. + """ + sid = self.get_session_id() + + valid_sid = sid is not None and 0 <= sid < 256 + + if valid_sid: + return self.get_window_station() is not None + + return False + + def get_window_station(self) -> Optional["GUIExtensions.tagWINDOWSTATION"]: + """ + Attempts to return the window station for this desktop + """ + try: + return self.rpwinstaParent.dereference() + except exceptions.InvalidAddressException: + return None + + def get_session_id(self) -> Optional[int]: + """ + Attempts to return the session ID for this desktop + """ + winsta = self.get_window_station() + if winsta: + return winsta.get_session_id() + + return None + + def get_threads( + self, + ) -> Iterator[Tuple[interfaces.objects.ObjectInterface, str, int]]: + """ + Returns the threads of each desktop along with owning process information + """ + symbol_table_name = self.vol.type_name.split(constants.BANG)[0] + + for thread in self.PtiList.to_list( + symbol_table_name + constants.BANG + "tagTHREADINFO", "PtiLink" + ): + try: + process_name = utility.array_to_string( + thread.ppi.Process.ImageFileName + ) + process_pid = thread.ppi.Process.UniqueProcessId + except exceptions.InvalidAddressException: + continue + + yield thread, process_name, process_pid + + def _do_get_windows( + self, window, max_windows + ) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]: + """ + Recursively walks and yields the adjacent and child windows + """ + seen_windows = set() + seen_children = set() + + if not window.vol.offset: + return + + yield window, window.get_name() + + seen_windows.add(window) + + # Walk adjacent windows + while len(seen_windows) < max_windows: + try: + window = window.spwndNext.dereference() + except exceptions.InvalidAddressException: + break + + if not window.vol.offset: + break + + if window.vol.offset in seen_windows: + break + + yield window, window.get_name() + + seen_windows.add(window) + + # Walk children windows and recursively yield them + for window in seen_windows: + child = window + + while len(seen_windows) + len(seen_children) < max_windows: + try: + child = child.spwndChild + except exceptions.InvalidAddressException: + break + + if not child.vol.offset: + break + + if child in seen_children: + break + seen_children.add(child) + + yield from self._do_get_windows(child, max_windows) + + def windows( + self, window, max_windows=10000 + ) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]: + """ + Enumerates all windows adjacent to and children of `window` + + Args: + window: The window to enumerate windows from + + Returns: + A generator of tuples containing the window and its name + """ + seen_windows = set() + + for window, window_name in self._do_get_windows(window, max_windows): + if window.vol.offset in seen_windows: + continue + + seen_windows.add(window.vol.offset) + + yield window, window_name + + if len(seen_windows) == max_windows: + break + + class tagWND(objects.StructType, pool.ExecutiveObject): + def is_valid(self) -> bool: + """ + Enforce a valid sid + """ + sid = self.get_session_id() + + return sid is not None and 0 <= sid < 256 + + def get_name(self) -> Optional[str]: + """ + directName appeared in later Windows 10 versions and is pointer + strName is a unicode string directly in the structure + """ + if self.has_member("directName"): + try: + return utility.pointer_to_string( + self.directName, count=256, encoding="utf16" + ) + except exceptions.InvalidAddressException: + vollog.debug( + f"directname for window at {self.vol.offset:#x} in layer {self.vol.layer_name} is invalid" + ) + + try: + return self.strName.get_string() + except exceptions.InvalidAddressException: + vollog.debug( + f"strName for window at {self.vol.offset:#x} in layer {self.vol.layer_name} is invalid" + ) + + return None + + def get_session_id(self) -> Optional[int]: + """ + Uses its tagDESKTOP pointer to find its session + """ + desktop = self.get_desktop() + if desktop: + return desktop.get_session_id() + + return None + + def get_desktop(self) -> Optional["GUIExtensions.tagDESKTOP"]: + """ + Attempts to return the host desktop (tagDESKTOP) for this window + """ + try: + return self.head.rpdesk.dereference() + except exceptions.InvalidAddressException: + vollog.debug( + f"Reading the desktop pointer for window {self.vol.offset:#x} caused a page fault" + ) + return None + + def get_process(self) -> Optional["extensions.EPROCESS"]: + """ + Attempts to return the host process (_EPROCESS) for this window + """ + try: + return self.head.pti.ppi.Process.dereference() + except exceptions.InvalidAddressException: + vollog.debug( + f"Reading the process pointer for window {self.vol.offset:#x} caused a page fault" + ) + return None + + def get_window_procedure(self): + """ + Attempts to return the window procedure for this windows + """ + try: + # >= 17134 + if hasattr(self, "subPointer"): + return self.subPointer.lpfnWndProc + else: + return self.lpfnWndProc + except exceptions.InvalidAddressException: + vollog.debug( + f"Invalid window procedure for window {self.vol.offset:#x}" + ) + return None + + # This is copy/paste from UNICODE_STRING in `symbols/windows/extensions/__init__.py` + # The versioning of modules would get very ugly if we let different modules share implementations + # across different data structures + class LARGE_UNICODE_STRING(objects.StructType): + """A class for Windows unicode string structures.""" + + def get_string(self) -> interfaces.objects.ObjectInterface: + # We explicitly do *not* catch errors here, we allow an exception to be thrown + # (otherwise there's no way to determine anything went wrong) + # It's up to the user of this method to catch exceptions + + # We manually construct an object rather than casting a dereferenced pointer in case + # the buffer length is 0 and the pointer is a NULL pointer + return self._context.object( + self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string", + layer_name=self.Buffer.vol.native_layer_name, + offset=self.Buffer, + max_length=self.Length, + errors="replace", + encoding="utf16", + ) + + class_types = { + "tagWINDOWSTATION": tagWINDOWSTATION, + "tagDESKTOP": tagDESKTOP, + "tagWND": tagWND, + "_LARGE_UNICODE_STRING": LARGE_UNICODE_STRING, + } diff --git a/volatility3/framework/symbols/windows/extensions/mbr.py b/volatility3/framework/symbols/windows/extensions/mbr.py index afdc73a17..078c4beb0 100644 --- a/volatility3/framework/symbols/windows/extensions/mbr.py +++ b/volatility3/framework/symbols/windows/extensions/mbr.py @@ -8,12 +8,7 @@ from volatility3.framework import objects class PARTITION_TABLE(objects.StructType): def get_disk_signature(self) -> str: """Get Disk Signature (GUID).""" - return "{0:02x}-{1:02x}-{2:02x}-{3:02x}".format( - self.DiskSignature[0], - self.DiskSignature[1], - self.DiskSignature[2], - self.DiskSignature[3], - ) + return f"{self.DiskSignature[0]:02x}-{self.DiskSignature[1]:02x}-{self.DiskSignature[2]:02x}-{self.DiskSignature[3]:02x}" class PARTITION_ENTRY(objects.StructType): diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index ebba882c0..ddc21798f 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -2,23 +2,169 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Optional +import logging +from typing import Dict, Iterator, List, Optional, Tuple -from volatility3.framework import objects, constants, exceptions +from volatility3.framework import constants, exceptions, interfaces, objects + +vollog = logging.getLogger(__name__) class MFTEntry(objects.StructType): """This represents the base MFT Record""" - def get_signature(self) -> str: + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + size: int, + members: Dict[str, Tuple[int, interfaces.objects.Template]], + ) -> None: + super().__init__(context, type_name, object_info, size, members) + + self._attrs_loaded = False + self._attrs: List[MFTAttribute] = [] + + @property + def symbol_table_name(self) -> str: + return self.vol.type_name.split(constants.BANG)[0] + + def get_signature(self) -> objects.String: signature = self.Signature.cast("string", max_length=4, encoding="latin-1") return signature + @property + def attributes(self) -> Iterator["MFTAttribute"]: + """ + Lazily evaluate and yield attributes, caching them in an internal list + for re-retrieval. + """ + if not self._attrs_loaded: + self._attrs = list(self._attributes()) + self._attrs_loaded = True + + yield from self._attrs + + def longest_filename(self) -> Optional[objects.String]: + names = [name.get_full_name() for name in self.filename_entries()] + if not names: + return None + + 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 = ( + self.symbol_table_name + constants.BANG + "ATTRIBUTE" + ) + + attr: MFTAttribute = self._context.object( + attribute_object_type_name, + offset=self.vol.offset + attr_base_offset, + layer_name=self.vol.layer_name, + ) + + # There is no field that has a count of Attributes + # Keep Attempting to read attributes until we get an invalid attr_header.AttrType + try: + while attr.Attr_Header.AttrType.is_valid_choice: + yield attr + + # If there's no advancement the loop will never end, so break it now + if attr.Attr_Header.Length == 0: + break + + # Update the base offset to point to the next attribute + attr_base_offset += attr.Attr_Header.Length + # Get the next attribute + attr: MFTAttribute = self._context.object( + attribute_object_type_name, + offset=self.vol.offset + attr_base_offset, + layer_name=self.vol.layer_name, + ) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Failed to read attribute at {attr.vol.offset:#x}: {e.__class__.__name__}" + ) + return + + def standard_information_entries( + self, + ) -> Iterator[objects.StructType]: + """ + Yields a STANDARD_INFORMATION struct for each of the + STANDARD_INFORMATION attributes in this MFT record (although there + should only be one per record). + """ + for attr in self.attributes: + attr_type = attr.Attr_Header.AttrType.lookup() + if attr_type != "STANDARD_INFORMATION": + continue + + si_object = ( + self.symbol_table_name + constants.BANG + "STANDARD_INFORMATION_ENTRY" + ) + + yield attr.Attr_Data.cast(si_object) + + def filename_entries(self) -> Iterator["MFTFileName"]: + """ + Yields an MFT Filename for each of the FILE_NAME attributes contained + in this MFT record. There are often two - one for the long filename, + and the other with the DOS 8.3 short name. + """ + for attr in self.attributes: + try: + attr_type = attr.Attr_Header.AttrType.lookup() + if attr_type != "FILE_NAME": + continue + + fn_object = self.symbol_table_name + constants.BANG + "FILE_NAME_ENTRY" + attr_data = attr.Attr_Data.cast(fn_object) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Failed to read attr at {attr.vol.offset:#x}: {e.__class__.__name__}" + ) + continue + yield attr_data + + def _data_attributes(self): + for attr in self.attributes: + if not ( + attr.Attr_Header.AttrType.lookup() == "DATA" + and attr.Attr_Header.NonResidentFlag == 0 + ): + continue + + yield attr + + def resident_data_attributes(self) -> Iterator["MFTAttribute"]: + """ + Yields all MFT attributes that contain resident data for the primary + stream. + """ + for attr in self._data_attributes(): + if attr.Attr_Header.NameLength == 0: + yield attr + + def alternate_data_streams(self) -> Iterator["MFTAttribute"]: + """ + Yields all MFT attributes that contain alternate data streams (ADS). + """ + for attr in self._data_attributes(): + if attr.Attr_Header.NameLength != 0: + yield attr + class MFTFileName(objects.StructType): """This represents an MFT $FILE_NAME Attribute""" - def get_full_name(self) -> str: + def get_full_name(self) -> objects.String: + """ + Returns the UTF-16 decoded filename. + """ output = self.Name.cast( "string", encoding="utf16", max_length=self.NameLength * 2, errors="replace" ) @@ -28,7 +174,10 @@ class MFTFileName(objects.StructType): class MFTAttribute(objects.StructType): """This represents an MFT ATTRIBUTE""" - def get_resident_filename(self) -> Optional[str]: + def get_resident_filename(self) -> Optional[objects.String]: + """ + Returns the resident filename (typically for an Alternate Data Stream (ADS)). + """ # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems # Length as 512 as its 256*2, which is the maximum size for an entire file path, so this is even generous if ( @@ -48,10 +197,17 @@ class MFTAttribute(objects.StructType): encoding="utf16", ) return name - except exceptions.InvalidAddressException: + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Failed to get resident file content due to {e.__class__.__name__}" + ) return None - def get_resident_filecontent(self) -> Optional[bytes]: + def get_resident_filecontent(self) -> Optional[objects.Bytes]: + """ + Returns the file content that is resident within this MFT attribute, + for either the primary or an alternate data stream. + """ # smear observed in mass testing of samples # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems if ( @@ -70,5 +226,8 @@ class MFTAttribute(objects.StructType): length=self.Attr_Header.ContentLength, ) return bytesobj - except exceptions.InvalidAddressException: + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Failed to get resident file content due to {e.__class__.__name__}" + ) return None diff --git a/volatility3/framework/symbols/windows/extensions/network.py b/volatility3/framework/symbols/windows/extensions/network.py index 9b7573c2e..62cb4fba4 100644 --- a/volatility3/framework/symbols/windows/extensions/network.py +++ b/volatility3/framework/symbols/windows/extensions/network.py @@ -4,17 +4,15 @@ import logging import socket -from typing import Dict, Tuple, List, Union +from typing import Dict, List, Optional, Tuple, Union -from volatility3.framework import exceptions -from volatility3.framework import objects, interfaces -from volatility3.framework.objects import Array +from volatility3.framework import exceptions, interfaces, objects from volatility3.framework.renderers import conversion vollog = logging.getLogger(__name__) -def inet_ntop(address_family: int, packed_ip: Union[List[int], Array]) -> str: +def inet_ntop(address_family: int, packed_ip: Union[List[int], objects.Array]) -> str: if address_family in [socket.AF_INET6, socket.AF_INET]: try: return socket.inet_ntop(address_family, bytes(packed_ip)) @@ -22,7 +20,7 @@ def inet_ntop(address_family: int, packed_ip: Union[List[int], Array]) -> str: raise RuntimeError( "This version of python does not have socket.inet_ntop, please upgrade" ) - raise socket.error("[Errno 97] Address family not supported by protocol") + raise OSError("[Errno 97] Address family not supported by protocol") # Python's socket.AF_INET6 is 0x1e but Microsoft defines it @@ -86,19 +84,29 @@ class _TCP_LISTENER(objects.StructType): except exceptions.InvalidAddressException: return None - def get_owner_pid(self): - if self.get_owner().is_valid(): - if self.get_owner().has_valid_member("UniqueProcessId"): - return self.get_owner().UniqueProcessId + def get_owner_pid(self) -> Optional[int]: + owner = self.get_owner() + + if owner is None: + return None + + if owner.is_valid(): + if owner.has_valid_member("UniqueProcessId"): + return owner.UniqueProcessId return None - def get_owner_procname(self): - if self.get_owner().is_valid(): - if self.get_owner().has_valid_member("ImageFileName"): - return self.get_owner().ImageFileName.cast( + def get_owner_procname(self) -> Optional[str]: + owner = self.get_owner() + + if owner is None: + return None + + if owner.is_valid(): + if owner.has_valid_member("ImageFileName"): + return owner.ImageFileName.cast( "string", - max_length=self.get_owner().ImageFileName.vol.count, + max_length=owner.ImageFileName.vol.count, errors="replace", ) @@ -167,11 +175,9 @@ class _TCP_LISTENER(objects.StructType): def is_valid(self): try: - if not self.get_address_family() in (AF_INET, AF_INET6): + if self.get_address_family() not in (AF_INET, AF_INET6): vollog.debug( - "netw obj 0x{:x} invalid due to invalid address_family {}".format( - self.vol.offset, self.get_address_family() - ) + f"netw obj 0x{self.vol.offset:x} invalid due to invalid address_family {self.get_address_family()}" ) return False @@ -211,7 +217,13 @@ class _TCP_ENDPOINT(_TCP_LISTENER): return None def is_valid(self): - if self.State not in self.State.choices.values(): + # netstat calls this before validating the object itself + try: + state = self.State + except exceptions.InvalidAddressException: + return False + + if state not in state.choices.values(): vollog.debug( f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid tcp state {self.State}" ) diff --git a/volatility3/framework/symbols/windows/extensions/pe.py b/volatility3/framework/symbols/windows/extensions/pe.py index 3f34fc3dd..2c7400f25 100644 --- a/volatility3/framework/symbols/windows/extensions/pe.py +++ b/volatility3/framework/symbols/windows/extensions/pe.py @@ -101,9 +101,9 @@ class IMAGE_DOS_HEADER(objects.StructType): ) except OverflowError: vollog.warning( - "Volatility was unable to fix the image base for the PE file at base address {:#x}. " + f"Volatility was unable to fix the image base for the PE file at base address {self.vol.offset:#x}. " "This will cause issues with many static analysis tools if you do not inform the " - "tool of the in-memory load address.".format(self.vol.offset) + "tool of the in-memory load address." ) new_pe = raw_data diff --git a/volatility3/framework/symbols/windows/extensions/pool.py b/volatility3/framework/symbols/windows/extensions/pool.py index b761ddad8..f12182fa7 100644 --- a/volatility3/framework/symbols/windows/extensions/pool.py +++ b/volatility3/framework/symbols/windows/extensions/pool.py @@ -4,7 +4,7 @@ import logging import struct from typing import Dict, List, Optional, Tuple, Union -from volatility3.plugins.windows.poolscanner import PoolConstraint +from volatility3.plugins.windows import poolscanner from volatility3.framework import ( constants, @@ -28,7 +28,7 @@ class POOL_HEADER(objects.StructType): def get_object( self, - constraint: PoolConstraint, + constraint: poolscanner.PoolConstraint, use_top_down: bool, kernel_symbol_table: Optional[str] = None, native_layer_name: Optional[str] = None, @@ -78,7 +78,9 @@ class POOL_HEADER(objects.StructType): # otherwise we have an executive object in the pool else: - if symbols.symbol_table_is_64bit(self._context, symbol_table_name): + if symbols.symbol_table_is_64bit( + context=self._context, symbol_table_name=symbol_table_name + ): alignment = 16 else: alignment = 8 @@ -95,7 +97,7 @@ class POOL_HEADER(objects.StructType): optional_headers, lengths_of_optional_headers, ) = self._calculate_optional_header_lengths( - self._context, symbol_table_name + self._context, kernel_symbol_table ) padding_available = ( None @@ -217,7 +219,7 @@ class POOL_HEADER(objects.StructType): yield mem_object @classmethod - @functools.lru_cache() + @functools.lru_cache def _calculate_optional_header_lengths( cls, context: interfaces.context.ContextInterface, symbol_table_name: str ) -> Tuple[List[str], List[int]]: @@ -326,12 +328,18 @@ class ExecutiveObject(interfaces.objects.ObjectInterface): """This is used as a "mixin" that provides all kernel executive objects with a means of finding their own object header.""" - def get_object_header(self) -> "OBJECT_HEADER": + def get_object_header( + self, symbol_table_name: Optional[str] = None + ) -> "OBJECT_HEADER": if constants.BANG not in self.vol.type_name: raise ValueError( f"Invalid symbol table name syntax (no {constants.BANG} found)" ) - symbol_table_name = self.vol.type_name.split(constants.BANG)[0] + + # caller provided symbol table allows for scanning for objects from any module + if not symbol_table_name: + symbol_table_name = self.vol.type_name.split(constants.BANG)[0] + body_offset = self._context.symbol_space.get_type( symbol_table_name + constants.BANG + "_OBJECT_HEADER" ).relative_child_offset("Body") @@ -342,6 +350,12 @@ class ExecutiveObject(interfaces.objects.ObjectInterface): native_layer_name=self.vol.native_layer_name, ) + def get_name(self, symbol_table_name: Optional[str] = None) -> Optional[str]: + try: + return self.get_object_header(symbol_table_name).get_name() + except exceptions.InvalidAddressException: + return None + class OBJECT_HEADER(objects.StructType): """A class for the headers for executive kernel objects, which contains @@ -362,7 +376,7 @@ class OBJECT_HEADER(objects.StructType): return True def get_object_type( - self, type_map: Dict[int, str], cookie: int = None + self, type_map: Dict[int, str], cookie: Optional[int] = None ) -> Optional[str]: """Across all Windows versions, the _OBJECT_HEADER embeds details on the type of object (i.e. process, file) but the way its embedded @@ -376,7 +390,16 @@ class OBJECT_HEADER(objects.StructType): try: # vista and earlier have a Type member - self._vol["object_header_object_type"] = self.Type.Name.String + length = self.Type.member("Name").Length + if length == 0 or length > 128: + string = None + else: + string = self.Type.Name.String + if len(string) == 0 or len(string) > 128: + string = None + + self._vol["object_header_object_type"] = string + except AttributeError: # windows 7 and later have a TypeIndex, but windows 10 # further encodes the index value with nt1!ObHeaderCookie @@ -430,9 +453,7 @@ class OBJECT_HEADER(objects.StructType): if header_offset == 0: raise ValueError( - "Could not find _OBJECT_HEADER_NAME_INFO for object at {} of layer {}".format( - self.vol.offset, self.vol.layer_name - ) + f"Could not find _OBJECT_HEADER_NAME_INFO for object at {self.vol.offset} of layer {self.vol.layer_name}" ) header = ntkrnlmp.object( @@ -443,3 +464,22 @@ class OBJECT_HEADER(objects.StructType): absolute=True, ) return header + + def get_name(self) -> Optional[str]: + """ + Attempts to get the name of the object + Sanity checks size members to avoid FPs + Returns None if any issues detected + """ + try: + name_info = self.NameInfo.Name + if ( + name_info.Length == 0 + or name_info.MaximumLength == 0 + or name_info.Length > name_info.MaximumLength + ): + return None + + return name_info.String + except (ValueError, exceptions.InvalidAddressException): + return None diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index bebfaea89..e18a15cba 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -8,11 +8,7 @@ import struct from typing import Iterator, Optional, Union, cast from volatility3.framework import constants, exceptions, interfaces, objects -from volatility3.framework.layers.registry import ( - RegistryFormatException, - RegistryHive, - RegistryInvalidIndex, -) +from volatility3.framework.layers import registry vollog = logging.getLogger(__name__) @@ -103,7 +99,9 @@ class CMHIVE(objects.StructType): for attr in ["FileFullPath", "FileUserName", "HiveRootPath"]: with contextlib.suppress( - AttributeError, exceptions.InvalidAddressException + AttributeError, + exceptions.InvalidAddressException, + registry.RegistryException, ): name = getattr(self, attr) if name.Length > 0: @@ -133,8 +131,17 @@ class CM_KEY_BODY(objects.StructType): def get_full_key_name(self) -> str: output = [] + seen = set() + kcb = self.KeyControlBlock while kcb.ParentKcb: + if kcb.ParentKcb.vol.offset in seen: + return None + seen.add(kcb.ParentKcb.vol.offset) + + if len(output) > 128: + return None + if kcb.NameBlock.Name is None: break @@ -159,16 +166,24 @@ class CM_KEY_NODE(objects.StructType): """Extension to allow traversal of registry keys.""" def get_volatile(self) -> bool: - if not isinstance(self._context.layers[self.vol.layer_name], RegistryHive): - raise ValueError( - "Cannot determine volatility of registry key without an offset in a RegistryHive layer" - ) + """ + Returns a bool indicating whether or not the key is volatile. + + Raises TypeError if the key was not instantiated on a RegistryHive layer + """ + if not isinstance( + self._context.layers[self.vol.layer_name], registry.RegistryHive + ): + raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") return bool(self.vol.offset & 0x80000000) def get_subkeys(self) -> Iterator["CM_KEY_NODE"]: - """Returns a list of the key nodes.""" + """Returns a list of the key nodes. + + Raises TypeError if the key was not instantiated on a RegistryHive layer + """ hive = self._context.layers[self.vol.layer_name] - if not isinstance(hive, RegistryHive): + if not isinstance(hive, registry.RegistryHive): raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") for index in range(2): # Use get_cell because it should *always* be a KeyIndex @@ -176,7 +191,7 @@ class CM_KEY_NODE(objects.StructType): yield from self._get_subkeys_recursive(hive, subkey_node) def _get_subkeys_recursive( - self, hive: RegistryHive, node: interfaces.objects.ObjectInterface + self, hive: "registry.RegistryHive", node: interfaces.objects.ObjectInterface ) -> Iterator["CM_KEY_NODE"]: """Recursively descend a node returning subkeys.""" # The keylist appears to include 4 bytes of key name after each value @@ -184,7 +199,10 @@ class CM_KEY_NODE(objects.StructType): # We could change the array type to a struct with both parts try: signature = node.cast("string", max_length=2, encoding="latin-1") - except (exceptions.InvalidAddressException, RegistryFormatException): + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): return None listjump = None @@ -196,9 +214,7 @@ class CM_KEY_NODE(objects.StructType): yield cast("CM_KEY_NODE", node) else: vollog.debug( - "Unexpected node type encountered when traversing subkeys: {}, signature: {}".format( - node.vol.type_name, signature - ) + f"Unexpected node type encountered when traversing subkeys: {node.vol.type_name}, signature: {signature}" ) if listjump: @@ -214,7 +230,7 @@ class CM_KEY_NODE(objects.StructType): subnode = hive.get_node(subnode_offset) except ( exceptions.InvalidAddressException, - RegistryFormatException, + registry.RegistryException, ): vollog.log( constants.LOGLEVEL_VVV, @@ -224,25 +240,32 @@ class CM_KEY_NODE(objects.StructType): yield from self._get_subkeys_recursive(hive, subnode) def get_values(self) -> Iterator["CM_KEY_VALUE"]: - """Returns a list of the Value nodes for a key.""" + """Returns a list of the Value nodes for a key. + + Raises TypeError if the key was not instantiated on a RegistryHive layer + """ hive = self._context.layers[self.vol.layer_name] - if not isinstance(hive, RegistryHive): + if not isinstance(hive, registry.RegistryHive): raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") - child_list = hive.get_cell(self.ValueList.List).u.KeyList - child_list.count = self.ValueList.Count try: + child_list = hive.get_cell(self.ValueList.List).u.KeyList + child_list.count = self.ValueList.Count + for v in child_list: if v != 0: try: node = hive.get_node(v) - except (RegistryInvalidIndex, RegistryFormatException) as excp: + except (registry.RegistryException,) as excp: vollog.debug(f"Invalid address {excp}") continue if isinstance(node, CM_KEY_VALUE): yield node - except (exceptions.InvalidAddressException, RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ) as excp: vollog.debug(f"Invalid address in get_values iteration: {excp}") return None @@ -253,8 +276,13 @@ class CM_KEY_NODE(objects.StructType): return self.Name.cast("string", max_length=namelength, encoding="latin-1") def get_key_path(self) -> str: + """ + Returns the full path to this registry key. + + Raises TypeError if the key was not instantiated on a RegistryHive layer + """ reg = self._context.layers[self.vol.layer_name] - if not isinstance(reg, RegistryHive): + if not isinstance(reg, registry.RegistryHive): raise TypeError("Key was not instantiated on a RegistryHive layer") # Using the offset adds a significant delay (since it cannot be cached easily) # if self.vol.offset == reg.get_node(reg.root_cell_offset).vol.offset: @@ -278,13 +306,22 @@ class CM_KEY_VALUE(objects.StructType): return RegValueTypes(self.Type) def decode_data(self) -> Union[int, bytes]: - """Properly decodes the data associated with the value node""" + """ + Properly decodes the data associated with the value node. + + If an InvalidAddressException occurs when reading data from the + underlying RegistryHive layer, the data will be padded with null bytes + of the same length. + + Raises ValueError if the data cannot be read + Raises TypeError if the class was not instantiated on a RegistryHive layer + """ # Determine if the data is stored inline datalen = self.DataLength data = b"" # Check if the data is stored inline layer = self._context.layers[self.vol.layer_name] - if not isinstance(layer, RegistryHive): + if not isinstance(layer, registry.RegistryHive): raise TypeError("Key value was not instantiated on a RegistryHive layer") # If the high-bit is set @@ -299,7 +336,7 @@ class CM_KEY_VALUE(objects.StructType): data = layer.read(self.Data.vol.offset, datalen) elif layer.hive.Version == 5 and datalen > 0x4000: # We're bigdata - big_data = layer.get_node(self.Data) + big_data = layer.get_node(self.Data).cast("_CM_BIG_DATA") # Oddly, we get a list of addresses, at which are addresses, which then point to data blocks for i in range(big_data.Count): # The value 4 should actually be unsigned-int.size, but since it's a file format that shouldn't change @@ -312,14 +349,29 @@ class CM_KEY_VALUE(objects.StructType): and block_offset < layer.maximum_address ): amount = min(BIG_DATA_MAXLEN, datalen) - data += layer.read( - offset=layer.get_cell(block_offset).vol.offset, length=amount - ) + try: + data += layer.read( + offset=layer.get_cell(block_offset).vol.offset, + length=amount, + ) + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): + vollog.debug( + f"Failed to read {amount:x} bytes of data, padding with {amount:x}" + ) datalen -= amount else: # Suspect Data actually points to a Cell, # but the length at the start could be negative so just adding 4 to jump past it - data = layer.read(self.Data + 4, datalen) + try: + data = layer.read(self.Data + 4, datalen) + except (exceptions.InvalidAddressException, registry.RegistryException): + vollog.debug( + f"Failed to read {datalen:x} bytes of data, returning {datalen:x} null bytes" + ) + data = b"\x00" * datalen if self.get_type() == RegValueTypes.REG_DWORD: if len(data) != struct.calcsize(" bool: """Determine if the structure is valid.""" - if self.Order < 0 or self.Order > 0xFFFF: - return False - try: - _ = self.State.description - _ = self.Start.description - except ValueError: + if self.Order < 0 or self.Order > 0xFFFF: + return False + + try: + _ = self.State.description + _ = self.Start.description + except ValueError: + return False + except exceptions.InvalidAddressException: return False return True @@ -88,7 +91,7 @@ class SERVICE_RECORD(objects.StructType): "SERVICE_INTERACTIVE_PROCESS": 256, } - type_flags = Flags(choices=SERVICE_TYPE_FLAGS) + type_flags = wrappers.Flags(choices=SERVICE_TYPE_FLAGS) return "|".join(type_flags(self.Type)) def traverse(self): diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index b84a7df6f..e7d92a48d 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -8,6 +8,7 @@ from datetime import datetime from typing import Dict, Optional, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, objects, renderers +from volatility3.framework.objects.utility import address_to_string from volatility3.framework.symbols.windows.extensions import conversion vollog = logging.getLogger(__name__) @@ -38,39 +39,49 @@ class SHIM_CACHE_ENTRY(objects.StructType): if self._exec_flag is not None: return self._exec_flag - if hasattr(self, "ListEntryDetail") and hasattr( - self.ListEntryDetail, "InsertFlags" - ): - self._exec_flag = self.ListEntryDetail.InsertFlags & 0x2 == 2 - - elif hasattr(self, "InsertFlags"): - self._exec_flag = self.InsertFlags & 0x2 == 2 - - elif hasattr(self, "ListEntryDetail") and hasattr( - self.ListEntryDetail, "BlobBuffer" - ): - blob_offset = self.ListEntryDetail.BlobBuffer - blob_size = self.ListEntryDetail.BlobSize - - if not self._context.layers[self.vol.native_layer_name].is_valid( - blob_offset, blob_size + try: + if hasattr(self, "ListEntryDetail") and hasattr( + self.ListEntryDetail, "InsertFlags" ): - self._exec_flag = renderers.UnparsableValue() + self._exec_flag = self.ListEntryDetail.InsertFlags & 0x2 == 2 - raw_flag = self._context.layers[self.vol.native_layer_name].read( - blob_offset, blob_size + elif hasattr(self, "InsertFlags"): + self._exec_flag = self.InsertFlags & 0x2 == 2 + + elif hasattr(self, "ListEntryDetail") and hasattr( + self.ListEntryDetail, "BlobBuffer" + ): + blob_offset = self.ListEntryDetail.BlobBuffer + blob_size = self.ListEntryDetail.BlobSize + + if not self._context.layers[self.vol.native_layer_name].is_valid( + blob_offset, blob_size + ): + self._exec_flag = renderers.UnreadableValue() + return self._exec_flag + + raw_flag = self._context.layers[self.vol.native_layer_name].read( + blob_offset, blob_size + ) + if not raw_flag: + self._exec_flag = renderers.UnparsableValue() + return self._exec_flag + + try: + self._exec_flag = bool(struct.unpack(" Optional[str]: vollog.info("Download PDB file...") file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) - for sym_url in ["http://msdl.microsoft.com/download/symbols"]: + for sym_url in [constants.SYMBOL_SERVER_URL]: url = sym_url + f"/{file_name}/{guid}/" result = None @@ -976,14 +977,16 @@ class PdbRetreiver: if __name__ == "__main__": import argparse - class PrintedProgress(object): + class PrintedProgress: """A progress handler that prints the progress value and the description onto the command line.""" def __init__(self): self._max_message_len = 0 - def __call__(self, progress: Union[int, float], description: str = None): + def __call__( + self, progress: Union[int, float], description: Optional[str] = None + ): """A simple function for providing text-based feedback. .. warning:: Only for development use. diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index 3816312cd..5f5c8cac8 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -16,7 +16,6 @@ from volatility3 import symbols from volatility3.framework import constants, contexts, exceptions, interfaces from volatility3.framework.automagic import symbol_cache from volatility3.framework.configuration import requirements -from volatility3.framework.configuration.requirements import SymbolTableRequirement from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbconv @@ -36,7 +35,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): layer_name: str, offset: int, symbol_table_class: str = "volatility3.framework.symbols.intermed.IntermediateSymbolTable", - config_path: str = None, + config_path: Optional[str] = None, progress_callback: constants.ProgressCallback = None, ) -> Optional[str]: """Produces the name of a symbol table loaded from the offset for an MZ header @@ -94,7 +93,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): if not requirements.VersionRequirement.matches_required( (1, 0, 0), symbol_cache.SqliteCache.version ): - vollog.debug(f"Required version of SQLiteCache not found") + vollog.debug("Required version of SQLiteCache not found") return None identifiers_path = os.path.join( @@ -140,7 +139,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): requirement_name = interfaces.configuration.path_head(config_path) # Construct the appropriate symbol table - requirement = SymbolTableRequirement( + requirement = requirements.SymbolTableRequirement( name=requirement_name, description="PDBUtility generated symbol table" ) requirement.construct(context, parent_config_path) @@ -291,9 +290,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): break except PermissionError: vollog.warning( - "Cannot write necessary symbol file, please check permissions on {}".format( - potential_output_filename - ) + f"Cannot write necessary symbol file, please check permissions on {potential_output_filename}" ) continue finally: @@ -390,8 +387,8 @@ class PDBUtility(interfaces.configuration.VersionableInterface): config_path: str, layer_name: str, pdb_name: str, - module_offset: int = None, - module_size: int = None, + module_offset: Optional[int] = None, + module_size: Optional[int] = None, ) -> str: """Creates symbol table for a module in the specified layer_name. @@ -411,6 +408,12 @@ class PDBUtility(interfaces.configuration.VersionableInterface): _, symbol_table_name = cls._modtable_from_pdb( context, config_path, layer_name, pdb_name, module_offset, module_size ) + + if symbol_table_name is None: + raise exceptions.SymbolSpaceError( + f"Symbol table could not be reconstructed for module {pdb_name}" + ) + return symbol_table_name @classmethod @@ -420,8 +423,8 @@ class PDBUtility(interfaces.configuration.VersionableInterface): config_path: str, layer_name: str, pdb_name: str, - module_offset: int = None, - module_size: int = None, + module_offset: Optional[int] = None, + module_size: Optional[int] = None, create_module: bool = False, ) -> Tuple[Optional[str], Optional[str]]: if module_offset is None: @@ -441,7 +444,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): ) if not guids: - raise exceptions.VolatilityException( + raise exceptions.SymbolSpaceError( f"Did not find GUID of {pdb_name} in module @ 0x{module_offset:x}!" ) @@ -480,8 +483,8 @@ class PDBUtility(interfaces.configuration.VersionableInterface): config_path: str, layer_name: str, pdb_name: str, - module_offset: int = None, - module_size: int = None, + module_offset: Optional[int] = None, + module_size: Optional[int] = None, ) -> str: """Creates a module in the specified layer_name based on a pdb name. diff --git a/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json b/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json index 6114e6c85..6c990f9f5 100644 --- a/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json +++ b/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json @@ -331,23 +331,23 @@ "LastModified": { "type": { "kind": "union", - "name": "LARGE_INTEGER" + "name": "_LARGE_INTEGER" }, - "offset": 4 + "offset": 528 }, "FileSize": { "type": { "kind": "base", "name": "long long" }, - "offset": 8 + "offset": 536 }, "LastUpdate": { "type": { "kind": "union", - "name": "LARGE_INTEGER" + "name": "_LARGE_INTEGER" }, - "offset": 12 + "offset": 544 } }, "kind": "struct", diff --git a/volatility3/framework/symbols/windows/versions.py b/volatility3/framework/symbols/windows/versions.py index 495655681..28b7edfdf 100644 --- a/volatility3/framework/symbols/windows/versions.py +++ b/volatility3/framework/symbols/windows/versions.py @@ -1,7 +1,7 @@ import logging -from typing import Callable, Tuple, List, Optional +from typing import Callable, List, Optional, Tuple -from volatility3.framework import interfaces, constants, exceptions +from volatility3.framework import constants, exceptions, interfaces vollog = logging.getLogger(__name__) @@ -88,24 +88,6 @@ class OsDistinguisher: return True -is_windows_8_1_or_later = OsDistinguisher( - version_check=lambda x: x >= (6, 3), - fallback_checks=[("_KPRCB", "PendingTickFlags", True)], -) - -is_vista_or_later = OsDistinguisher( - version_check=lambda x: x >= (6, 0), - fallback_checks=[("KdCopyDataBlock", None, True)], -) - -is_win10 = OsDistinguisher( - version_check=lambda x: (10, 0) <= x, - fallback_checks=[ - ("ObHeaderCookie", None, True), - ("_HANDLE_TABLE", "HandleCount", False), - ], -) - is_windows_xp = OsDistinguisher( version_check=lambda x: (5, 1) <= x < (5, 2), fallback_checks=[ @@ -149,6 +131,32 @@ is_2003 = OsDistinguisher( ], ) +is_vista_or_later = OsDistinguisher( + version_check=lambda x: x >= (6, 0), + fallback_checks=[("KdCopyDataBlock", None, True)], +) + +is_windows_8_1_or_later = OsDistinguisher( + version_check=lambda x: x >= (6, 3), + fallback_checks=[("_KPRCB", "PendingTickFlags", True)], +) + +is_win10 = OsDistinguisher( + version_check=lambda x: (10, 0) <= x, + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_HANDLE_TABLE", "HandleCount", False), + ], +) + +is_win10_10586_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 10586), + fallback_checks=[ + ("_UNLOADED_DRIVERS", None, False), + ("ObHeaderCookie", None, True), + ], +) + is_win10_up_to_15063 = OsDistinguisher( version_check=lambda x: (10, 0) <= x < (10, 0, 15063), fallback_checks=[ @@ -187,6 +195,22 @@ is_win10_16299_or_later = OsDistinguisher( ], ) +is_win10_17134_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 17134), + fallback_checks=[ + ("_EPROCESS", "ProcessFirstResume", True), + ("_EPROCESS", "HighMemoryPriority", True), + ], +) + +is_win10_17735_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 17735), + fallback_checks=[ + ("_EPROCESS", "VmProcessorHost", True), + ("_EPROCESS", "VdmObjects", False), + ], +) + is_win10_17763_or_later = OsDistinguisher( version_check=lambda x: x >= (10, 0, 17763), fallback_checks=[ @@ -218,6 +242,14 @@ is_win10_19041_or_later = OsDistinguisher( ], ) +is_win10_19577_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 19577), + fallback_checks=[ + ("_EPROCESS", "PaeTop", False), + ("_EPROCESS", "IdealProcessorAssignmentBlock", True), + ], +) + is_win10_25398_or_later = OsDistinguisher( version_check=lambda x: x >= (10, 0, 25398), fallback_checks=[ @@ -235,6 +267,31 @@ is_windows_8_or_later = OsDistinguisher( version_check=lambda x: x >= (6, 2), fallback_checks=[("_HANDLE_TABLE", "HandleCount", False)], ) + +is_windows_7_sp0 = OsDistinguisher( + version_check=lambda x: x == (6, 1, 7600), + fallback_checks=[ + ("_EPROCESS", "VdmObjects", True), + ("_EPROCESS", "UmsScheduledThreads", False), + # Dropped after vista + ("_EPROCESS", "QuotaUsage", False), + # Added win8 + ("_EPROCESS", "WnfContext", False), + ], +) + +is_windows_7_sp1 = OsDistinguisher( + version_check=lambda x: x == (6, 1, 7601), + fallback_checks=[ + ("_EPROCESS", "VdmObjects", False), + ("_EPROCESS", "UmsScheduledThreads", True), + # Dropped after vista + ("_EPROCESS", "QuotaUsage", False), + # Added win8 + ("_EPROCESS", "WnfContext", False), + ], +) + # Technically, this is win7 or less is_windows_7 = OsDistinguisher( version_check=lambda x: x == (6, 1), diff --git a/volatility3/framework/symbols/windows/wow64.json b/volatility3/framework/symbols/windows/wow64.json new file mode 100644 index 000000000..4c5cdd4a4 --- /dev/null +++ b/volatility3/framework/symbols/windows/wow64.json @@ -0,0 +1,2426 @@ +{ + "symbols": { + }, + "enums": { + "_LDR_DLL_LOAD_REASON": { + "base": "int", + "constants": { + "LoadReasonAsDataLoad": 6, + "LoadReasonAsImageLoad": 5, + "LoadReasonDelayloadDependency": 3, + "LoadReasonDynamicForwarderDependency": 2, + "LoadReasonDynamicLoad": 4, + "LoadReasonStaticDependency": 0, + "LoadReasonStaticForwarderDependency": 1, + "LoadReasonUnknown": -1 + }, + "size": 4 + }, + "_LDR_DDAG_STATE": { + "base": "int", + "constants": { + "LdrModulesCondensed": 6, + "LdrModulesInitError": -4, + "LdrModulesInitializing": 8, + "LdrModulesMapped": 2, + "LdrModulesMapping": 1, + "LdrModulesMerged": -5, + "LdrModulesPlaceHolder": 0, + "LdrModulesReadyToInit": 7, + "LdrModulesReadyToRun": 9, + "LdrModulesSnapError": -3, + "LdrModulesSnapped": 5, + "LdrModulesSnapping": 4, + "LdrModulesUnloaded": -2, + "LdrModulesUnloading": -1, + "LdrModulesWaitingForDependencies": 3 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "int": { + "endian": "little", + "kind": "int", + "signed": true, + "size": 4 + }, + "unsigned long long": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "long long": { + "endian": "little", + "kind": "int", + "signed": true, + "size": 8 + }, + "void": { + "endian": "little", + "kind": "void", + "signed": true, + "size": 0 + } + }, + "metadata": { + "format": "4.1.0", + "producer": { + "datetime": "2024-05-30T17:02:06.755760", + "name": "awalters-by-hand", + "version": "0.0.2" + } + }, + "user_types": { + "_LDR_SERVICE_TAG_RECORD": { + "fields": { + "Next": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LDR_SERVICE_TAG_RECORD" + } + } + }, + "ServiceTag": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 8 + }, + "_KTIMER": { + "fields": { + "Dpc": { + "offset": 32, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_KDPC" + } + } + }, + "DueTime": { + "offset": 16, + "type": { + "kind": "union", + "name": "_ULARGE_INTEGER" + } + }, + "Header": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_DISPATCHER_HEADER" + } + }, + "Period": { + "offset": 36, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "TimerListEntry": { + "offset": 24, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + } + }, + "kind": "struct", + "size": 40 + }, + "_ERESOURCE": { + "fields": { + "ActiveCount": { + "offset": 12, + "type": { + "kind": "base", + "name": "short" + } + }, + "ActiveEntries": { + "offset": 32, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Address": { + "offset": 48, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "ContentionCount": { + "offset": 36, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "CreatorBackTraceIndex": { + "offset": 48, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ExclusiveWaiters": { + "offset": 20, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_KEVENT" + } + } + }, + "Flag": { + "offset": 14, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "NumberOfExclusiveWaiters": { + "offset": 44, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NumberOfSharedWaiters": { + "offset": 40, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "OwnerEntry": { + "offset": 24, + "type": { + "kind": "struct", + "name": "_OWNER_ENTRY" + } + }, + "OwnerTable": { + "offset": 8, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_OWNER_ENTRY" + } + } + }, + "ReservedLowFlags": { + "offset": 14, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "SharedWaiters": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_KSEMAPHORE" + } + } + }, + "SpinLock": { + "offset": 52, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "SystemResourcesList": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "WaiterPriority": { + "offset": 15, + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "kind": "struct", + "size": 56 + }, + "_LARGE_INTEGER": { + "fields": { + "HighPart": { + "offset": 4, + "type": { + "kind": "base", + "name": "long" + } + }, + "LowPart": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "QuadPart": { + "offset": 0, + "type": { + "kind": "base", + "name": "long long" + } + }, + "u": { + "offset": 0, + "type": { + "kind": "struct", + "name": "__unnamed_1083" + } + } + }, + "kind": "union", + "size": 8 + }, + "_ETHREAD": { + "fields": { + "Cid": { + "offset": 868, + "type": { + "kind": "struct", + "name": "_CLIENT_ID" + } + }, + "CreateTime": { + "offset": 824, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "CrossThreadFlags": { + "offset": 952, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ExitTime": { + "offset": 832, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "Tcb": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_KTHREAD" + } + } + }, + "kind": "struct", + "size": 1048 + }, + "_KTHREAD": { + "fields": { + "State": { + "offset": 144, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "WaitReason": { + "offset": 395, + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "kind": "struct", + "size": 824 + }, + "_EPROCESS": { + "fields": { + "CreateTime": { + "offset": 168, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "ExitTime": { + "offset": 688, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "ImageFileName": { + "offset": 1080, + "type": { + "count": 368, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "ObjectTable": { + "offset": 336, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_HANDLE_TABLE" + } + } + }, + "Pcb": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_KPROCESS" + } + }, + "Peb": { + "offset": 320, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_PEB" + } + } + }, + "Session": { + "offset": 324, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "ThreadListHead": { + "offset": 404, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "UniqueProcessId": { + "offset": 180, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "VadRoot": { + "offset": 628, + "type": { + "kind": "struct", + "name": "_RTL_AVL_TREE" + } + } + }, + "kind": "struct", + "size": 760 + }, + "_EX_FAST_REF": { + "fields": { + "Object": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "RefCnt": { + "offset": 0, + "type": { + "bit_length": 4, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "Value": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 4 + }, + "_TOKEN": { + "fields": { + "Privileges": { + "offset": 64, + "type": { + "kind": "struct", + "name": "_SEP_TOKEN_PRIVILEGES" + } + }, + "UserAndGroupCount": { + "offset": 124, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "UserAndGroups": { + "offset": 148, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SID_AND_ATTRIBUTES" + } + } + } + }, + "kind": "struct", + "size": 656 + }, + "_OBJECT_HEADER": { + "fields": { + "Body": { + "offset": 24, + "type": { + "kind": "struct", + "name": "_QUAD" + } + }, + "InfoMask": { + "offset": 14, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "PointerCount": { + "offset": 0, + "type": { + "kind": "base", + "name": "long" + } + }, + "TypeIndex": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "kind": "struct", + "size": 32 + }, + "_FILE_OBJECT": { + "fields": { + "DeleteAccess": { + "offset": 40, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "DeviceObject": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEVICE_OBJECT" + } + } + }, + "FileName": { + "offset": 48, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + }, + "ReadAccess": { + "offset": 38, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "SharedDelete": { + "offset": 43, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "SharedRead": { + "offset": 41, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "SharedWrite": { + "offset": 42, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "WriteAccess": { + "offset": 39, + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "kind": "struct", + "size": 128 + }, + "_DEVICE_OBJECT": { + "fields": { + "AttachedDevice": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEVICE_OBJECT" + } + } + }, + "Flags": { + "offset": 48, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NextDevice": { + "offset": 12, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEVICE_OBJECT" + } + } + } + }, + "kind": "struct", + "size": 184 + }, + "_CM_KEY_BODY": { + "fields": { + "KeyControlBlock": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CM_KEY_CONTROL_BLOCK" + } + } + }, + "Type": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 44 + }, + "_CMHIVE": { + "fields": { + "FileFullPath": { + "offset": 1136, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + }, + "FileUserName": { + "offset": 1144, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + }, + "Hive": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_HHIVE" + } + }, + "HiveRootPath": { + "offset": 1160, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + } + }, + "kind": "struct", + "size": 3104 + }, + "_CM_KEY_NODE": { + "fields": { + "Name": { + "offset": 76, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "wchar" + } + } + }, + "NameLength": { + "offset": 72, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "Parent": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SubKeyLists": { + "offset": 28, + "type": { + "count": 2, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ValueList": { + "offset": 36, + "type": { + "kind": "struct", + "name": "_CHILD_LIST" + } + } + }, + "kind": "struct", + "size": 80 + }, + "_CM_KEY_VALUE": { + "fields": { + "Data": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "DataLength": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Flags": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "Name": { + "offset": 20, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "wchar" + } + } + }, + "NameLength": { + "offset": 2, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "Signature": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "Spare": { + "offset": 18, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "Type": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 24 + }, + "_HMAP_ENTRY": { + "fields": { + "BinAddress": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "BlockAddress": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "MemAlloc": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 12 + }, + "_MMVAD_SHORT": { + "fields": { + "EndingVpn": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NextVad": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_MMVAD_SHORT" + } + } + }, + "StartingVpn": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "VadNode": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_RTL_BALANCED_NODE" + } + } + }, + "kind": "struct", + "size": 40 + }, + "_MMVAD": { + "fields": { + "Core": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_MMVAD_SHORT" + } + }, + "Subsection": { + "offset": 44, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SUBSECTION" + } + } + } + }, + "kind": "struct", + "size": 72 + }, + "_KSYSTEM_TIME": { + "fields": { + "High1Time": { + "offset": 4, + "type": { + "kind": "base", + "name": "long" + } + }, + "High2Time": { + "offset": 8, + "type": { + "kind": "base", + "name": "long" + } + }, + "LowPart": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 12 + }, + "_KMUTANT": { + "fields": { + "Header": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_DISPATCHER_HEADER" + } + } + }, + "kind": "struct", + "size": 32 + }, + "_DRIVER_OBJECT": { + "fields": { + "DeviceObject": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEVICE_OBJECT" + } + } + } + }, + "kind": "struct", + "size": 168 + }, + "_OBJECT_SYMBOLIC_LINK": { + "fields": { + "CreationTime": { + "offset": 0, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + } + }, + "kind": "struct", + "size": 24 + }, + "_CONTROL_AREA": { + "fields": { + "FilePointer": { + "offset": 32, + "type": { + "kind": "struct", + "name": "_EX_FAST_REF" + } + } + }, + "kind": "struct", + "size": 80 + }, + "_SHARED_CACHE_MAP": { + "fields": { + "FileSize": { + "offset": 8, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "InitialVacbs": { + "offset": 48, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_VACB" + } + } + } + }, + "Section": { + "offset": 108, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "SectionSize": { + "offset": 24, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "Vacbs": { + "offset": 64, + "type": { + "kind": "pointer", + "subtype": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_VACB" + } + } + } + }, + "ValidDataLength": { + "offset": 32, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + } + }, + "kind": "struct", + "size": 368 + }, + "_VACB": { + "fields": { + "ArrayHead": { + "offset": 16, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_VACB_ARRAY_HEADER" + } + } + }, + "BaseAddress": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "Overlay": { + "offset": 8, + "type": { + "kind": "union", + "name": "__unnamed_1971" + } + }, + "SharedCacheMap": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SHARED_CACHE_MAP" + } + } + } + }, + "kind": "struct", + "size": 24 + }, + "_POOL_TRACKER_BIG_PAGES": { + "fields": { + "Key": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NumberOfBytes": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "PoolType": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Va": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 16 + }, + "_IMAGE_DOS_HEADER": { + "fields": { + "e_cblp": { + "offset": 2, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_cp": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_cparhdr": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_crlc": { + "offset": 6, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_cs": { + "offset": 22, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_csum": { + "offset": 18, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_ip": { + "offset": 20, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_lfanew": { + "offset": 60, + "type": { + "kind": "base", + "name": "long" + } + }, + "e_lfarlc": { + "offset": 24, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_magic": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_maxalloc": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_minalloc": { + "offset": 10, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_oemid": { + "offset": 36, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_oeminfo": { + "offset": 38, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_ovno": { + "offset": 26, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_res": { + "offset": 28, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "e_res2": { + "offset": 40, + "type": { + "count": 10, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "e_sp": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "e_ss": { + "offset": 14, + "type": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 64 + }, + "_SINGLE_LIST_ENTRY": { + "fields": { + "Next": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SINGLE_LIST_ENTRY" + } + } + } + }, + "kind": "struct", + "size": 4 + }, + "_LDRP_CSLIST": { + "fields": { + "Tail": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SINGLE_LIST_ENTRY" + } + } + } + }, + "kind": "struct", + "size": 4 + }, + "_RTL_BALANCED_NODE": { + "fields": { + "Balance": { + "offset": 8, + "type": { + "bit_length": 2, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "Children": { + "offset": 0, + "type": { + "count": 2, + "kind": "array", + "subtype": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_RTL_BALANCED_NODE" + } + } + } + }, + "Left": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_RTL_BALANCED_NODE" + } + } + }, + "ParentValue": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Red": { + "offset": 8, + "type": { + "bit_length": 1, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "Right": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_RTL_BALANCED_NODE" + } + } + } + }, + "kind": "struct", + "size": 12 + }, + "_LIST_ENTRY": { + "fields": { + "Blink": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + } + }, + "Flink": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + } + } + }, + "kind": "struct", + "size": 8 + }, + "LIST_ENTRY32": { + "fields": { + "Blink": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Flink": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 8 + }, + "_PEB_LDR_DATA": { + "fields": { + "EntryInProgress": { + "offset": 36, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "InInitializationOrderModuleList": { + "offset": 28, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "InLoadOrderModuleList": { + "offset": 12, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "InMemoryOrderModuleList": { + "offset": 20, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "Initialized": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "Length": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ShutdownInProgress": { + "offset": 40, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "ShutdownThreadId": { + "offset": 44, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "SsHandle": { + "offset": 8, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + } + }, + "kind": "struct", + "size": 48 + }, + "_LDR_DATA_TABLE_ENTRY": { + "fields": { + "BaseDllName": { + "offset": 44, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + }, + "FullDllName": { + "offset": 36, + "type": { + "kind": "struct", + "name": "_UNICODE_STRING" + } + }, + "LoadTime": { + "offset": 256, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "DllBase": { + "offset": 24, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + } + }, + "SizeOfImage": { + "offset": 32, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "InInitializationOrderLinks": { + "offset": 16, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "InLoadOrderLinks": { + "offset": 0, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + }, + "InMemoryOrderLinks": { + "offset": 8, + "type": { + "kind": "struct", + "name": "_LIST_ENTRY" + } + } + }, + "kind": "struct", + "size": 160 + }, + "_PEB32": { + "fields": { + "ActivationContextData": { + "offset": 504, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ActiveProcessAffinityMask": { + "offset": 192, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "AnsiCodePageData": { + "offset": 88, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ApiSetMap": { + "offset": 56, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "AppCompatFlags": { + "offset": 472, + "type": { + "kind": "union", + "name": "_ULARGE_INTEGER" + } + }, + "AppCompatFlagsUser": { + "offset": 480, + "type": { + "kind": "union", + "name": "_ULARGE_INTEGER" + } + }, + "AppCompatInfo": { + "offset": 492, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "AtlThunkSListPtr": { + "offset": 32, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "AtlThunkSListPtr32": { + "offset": 52, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "BeingDebugged": { + "offset": 2, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "BitField": { + "offset": 3, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "CSDVersion": { + "offset": 496, + "type": { + "kind": "struct", + "name": "_STRING32" + } + }, + "CritSecTracingEnabled": { + "offset": 576, + "type": { + "bit_length": 1, + "bit_position": 1, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "CriticalSectionTimeout": { + "offset": 112, + "type": { + "kind": "union", + "name": "_LARGE_INTEGER" + } + }, + "CrossProcessFlags": { + "offset": 40, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "CsrServerReadOnlySharedMemoryBase": { + "offset": 584, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "FastPebLock": { + "offset": 28, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "FlsBitmap": { + "offset": 536, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "FlsBitmapBits": { + "offset": 540, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "FlsCallback": { + "offset": 524, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "FlsHighIndex": { + "offset": 556, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "FlsListHead": { + "offset": 528, + "type": { + "kind": "struct", + "name": "LIST_ENTRY32" + } + }, + "GdiDCAttributeList": { + "offset": 156, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "GdiHandleBuffer": { + "offset": 196, + "type": { + "count": 34, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "GdiSharedHandleTable": { + "offset": 148, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "HeapDeCommitFreeBlockThreshold": { + "offset": 132, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "HeapDeCommitTotalFreeThreshold": { + "offset": 128, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "HeapSegmentCommit": { + "offset": 124, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "HeapSegmentReserve": { + "offset": 120, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "HeapTracingEnabled": { + "offset": 576, + "type": { + "bit_length": 1, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "IFEOKey": { + "offset": 36, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ImageBaseAddress": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ImageSubsystem": { + "offset": 180, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ImageSubsystemMajorVersion": { + "offset": 184, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ImageSubsystemMinorVersion": { + "offset": 188, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ImageUsesLargePages": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "InheritedAddressSpace": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "IsAppContainer": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 5, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "IsImageDynamicallyRelocated": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 2, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "IsPackagedProcess": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 4, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "IsProtectedProcess": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 1, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "IsProtectedProcessLight": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 6, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "KernelCallbackTable": { + "offset": 44, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Ldr": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "LibLoaderTracingEnabled": { + "offset": 576, + "type": { + "bit_length": 1, + "bit_position": 2, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "LoaderLock": { + "offset": 160, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "MaximumNumberOfHeaps": { + "offset": 140, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "MinimumStackCommit": { + "offset": 520, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "Mutant": { + "offset": 4, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NtGlobalFlag": { + "offset": 104, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NumberOfHeaps": { + "offset": 136, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "NumberOfProcessors": { + "offset": 100, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "OSBuildNumber": { + "offset": 172, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "OSCSDVersion": { + "offset": 174, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "OSMajorVersion": { + "offset": 164, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "OSMinorVersion": { + "offset": 168, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "OSPlatformId": { + "offset": 176, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "OemCodePageData": { + "offset": 92, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "PostProcessInitRoutine": { + "offset": 332, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessAssemblyStorageMap": { + "offset": 508, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessHeap": { + "offset": 24, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessHeaps": { + "offset": 144, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessInJob": { + "offset": 40, + "type": { + "bit_length": 1, + "bit_position": 0, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ProcessInitializing": { + "offset": 40, + "type": { + "bit_length": 1, + "bit_position": 1, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ProcessParameters": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessStarterHelper": { + "offset": 152, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ProcessUsingFTH": { + "offset": 40, + "type": { + "bit_length": 1, + "bit_position": 4, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ProcessUsingVCH": { + "offset": 40, + "type": { + "bit_length": 1, + "bit_position": 3, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ProcessUsingVEH": { + "offset": 40, + "type": { + "bit_length": 1, + "bit_position": 2, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "ReadImageFileExecOptions": { + "offset": 1, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "ReadOnlySharedMemoryBase": { + "offset": 76, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ReadOnlyStaticServerData": { + "offset": 84, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "ReservedBits0": { + "offset": 40, + "type": { + "bit_length": 27, + "bit_position": 5, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "SessionId": { + "offset": 468, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SkipPatchingUser32Forwarders": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 3, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "SpareBits": { + "offset": 3, + "type": { + "bit_length": 1, + "bit_position": 7, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "SparePvoid0": { + "offset": 80, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SpareTracingBits": { + "offset": 576, + "type": { + "bit_length": 29, + "bit_position": 3, + "kind": "bitfield", + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "SubSystemData": { + "offset": 20, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SystemAssemblyStorageMap": { + "offset": 516, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SystemDefaultActivationContextData": { + "offset": 512, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "SystemReserved": { + "offset": 48, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "TlsBitmap": { + "offset": 64, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "TlsBitmapBits": { + "offset": 68, + "type": { + "count": 2, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "TlsExpansionBitmap": { + "offset": 336, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "TlsExpansionBitmapBits": { + "offset": 340, + "type": { + "count": 32, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "TlsExpansionCounter": { + "offset": 60, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "TracingFlags": { + "offset": 576, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "UnicodeCaseTableData": { + "offset": 96, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "UserSharedInfoPtr": { + "offset": 44, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "WerRegistrationData": { + "offset": 560, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "WerShipAssertPtr": { + "offset": 564, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "pImageHeaderHash": { + "offset": 572, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "pShimData": { + "offset": 488, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "pUnused": { + "offset": 568, + "type": { + "kind": "base", + "name": "unsigned long" + } + } + }, + "kind": "struct", + "size": 592 + }, + "_UNICODE_STRING": { + "fields": { + "Buffer": { + "offset": 4, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "Length": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "MaximumLength": { + "offset": 2, + "type": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 8 + } + } +} diff --git a/volatility3/framework/versionutils.py b/volatility3/framework/versionutils.py new file mode 100644 index 000000000..334410ff8 --- /dev/null +++ b/volatility3/framework/versionutils.py @@ -0,0 +1,23 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import Tuple + + +def matches_required(required: Tuple[int, ...], version: Tuple[int, int, int]) -> bool: + """ + Checks if a version tuple satisfies the required version major and minor constraints. + + Parameters: + required (Tuple[int, ...]): A tuple containing required major and optionally minor version numbers. + version (Tuple[int, int, int]): A tuple containing the full version (major, minor, patch). + + Returns: + bool: True if the version matches the required constraints, False otherwise. + """ + if len(required) > 0 and version[0] != required[0]: + return False + if len(required) > 1 and version[1] < required[1]: + return False + return True 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 8915cdfad..f012a52a0 100644 --- a/volatility3/plugins/windows/registry/__init__.py +++ b/volatility3/plugins/windows/registry/__init__.py @@ -11,9 +11,10 @@ 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 # This is necessary to ensure the core plugins are available, whilst still be overridable -parent_module, module_name = ".".join(__name__.split(".")[:-1]), __name__.split(".")[-1] +parent_module, module_name = __name__.rsplit(".", maxsplit=1) __path__ = [os.path.join(x, module_name) for x in sys.modules[parent_module].__path__] diff --git a/volatility3/plugins/windows/registry/certificates.py b/volatility3/plugins/windows/registry/certificates.py index 5ef840f32..d96284036 100644 --- a/volatility3/plugins/windows/registry/certificates.py +++ b/volatility3/plugins/windows/registry/certificates.py @@ -1,11 +1,12 @@ import contextlib import logging import struct -from typing import List, Iterator, Optional, Tuple, Type +from typing import Iterator, List, Optional, Tuple, Type from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.windows.extensions.registry import RegValueTypes +from volatility3.framework.symbols.windows.extensions import registry from volatility3.plugins.windows.registry import hivelist, printkey vollog = logging.getLogger(__name__) @@ -24,11 +25,11 @@ class Certificates(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="printkey", plugin=printkey.PrintKey, version=(1, 0, 0) + requirements.VersionRequirement( + name="printkey", component=printkey.PrintKey, version=(1, 0, 0) ), requirements.BooleanRequirement( name="dump", @@ -60,7 +61,7 @@ class Certificates(interfaces.plugins.PluginInterface): open_method: Type[interfaces.plugins.FileHandlerInterface], ) -> Optional[interfaces.plugins.FileHandlerInterface]: try: - dump_name = "{}-{}-{}.crt".format(hive_offset, reg_section, key_hash) + dump_name = f"{hive_offset}-{reg_section}-{key_hash}.crt" file_handle = open_method(dump_name) file_handle.write(certificate_data) return file_handle @@ -69,19 +70,20 @@ class Certificates(interfaces.plugins.PluginInterface): return None def _generator(self) -> Iterator[Tuple[int, Tuple[str, str, str, str]]]: - kernel = self.context.modules[self.config["kernel"]] - for hive in hivelist.HiveList.list_hives( - self.context, + context=self.context, base_config_path=self.config_path, - layer_name=kernel.layer_name, - symbol_table=kernel.symbol_table_name, + kernel_module_name=self.config["kernel"], ): for top_key in [ "Microsoft\\SystemCertificates", "Software\\Microsoft\\SystemCertificates", ]: - with contextlib.suppress(KeyError, exceptions.InvalidAddressException): + with contextlib.suppress( + KeyError, + registry_layer.RegistryException, + exceptions.InvalidAddressException, + ): # Walk it node_path = hive.get_key(top_key, return_list=True) for ( @@ -92,7 +94,11 @@ class Certificates(interfaces.plugins.PluginInterface): _volatility, node, ) in printkey.PrintKey.key_iterator(hive, node_path, recurse=True): - if not is_key and RegValueTypes(node.Type).name == "REG_BINARY": + if ( + not is_key + and registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_BINARY + ): name, certificate_data = self.parse_data(node.decode_data()) unique_key_offset = ( key_path.casefold().index(top_key.casefold()) diff --git a/volatility3/plugins/windows/statistics.py b/volatility3/plugins/windows/statistics.py index 7f56b75f8..e7557dc0c 100644 --- a/volatility3/plugins/windows/statistics.py +++ b/volatility3/plugins/windows/statistics.py @@ -64,9 +64,7 @@ class Statistics(plugins.PluginInterface): other_invalid += 1 page_size = expected_page_size vollog.debug( - "A non-page lookup invalid address exception occurred at: {} in layer {}".format( - hex(excp.invalid_address), excp.layer_name - ) + f"A non-page lookup invalid address exception occurred at: {hex(excp.invalid_address)} in layer {excp.layer_name}" ) page_addr += page_size diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index 3ca00e5dc..e894def9f 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -14,13 +14,15 @@ vollog = logging.getLogger(__name__) cached_validation_filepath = os.path.join(constants.CACHE_PATH, "valid_isf.hashcache") +validators = {} + def load_cached_validations() -> Set[str]: """Loads up the list of successfully cached json objects, so we don't need to revalidate them.""" validhashes: Set = set() if os.path.exists(cached_validation_filepath): - with open(cached_validation_filepath, "r") as f: + with open(cached_validation_filepath) as f: validhashes.update(json.load(f)) return validhashes @@ -46,7 +48,7 @@ def validate(input: Dict[str, Any], use_cache: bool = True) -> bool: if not os.path.exists(schema_path): vollog.debug(f"Schema for format not found: {schema_path}") return False - with open(schema_path, "r") as s: + with open(schema_path) as s: schema = json.load(s) return valid(input, schema, use_cache) @@ -66,7 +68,7 @@ def create_json_hash( if not os.path.exists(schema_path): vollog.debug(f"Schema for format not found: {schema_path}") return None - with open(schema_path, "r") as s: + with open(schema_path) as s: schema = json.load(s) return hashlib.sha1( bytes(json.dumps((input, schema), sort_keys=True), "utf-8") @@ -93,6 +95,13 @@ def valid( return True try: import jsonschema + + schema_key = json.dumps(schema, sort_keys=True) + if schema_key not in validators: + validator_class = jsonschema.validators.validator_for(schema) + validator_class.check_schema(schema) + validator = validator_class(schema) + validators[schema_key] = validator except ImportError: vollog.info("Dependency for validation unavailable: jsonschema") vollog.debug("All validations will report success, even with malformed input") @@ -100,7 +109,7 @@ def valid( try: vollog.debug("Validating JSON against schema...") - jsonschema.validate(input, schema) + validators[schema_key].validate(input) cached_validations.add(input_hash) vollog.debug("JSON validated against schema (result cached)") except jsonschema.exceptions.SchemaError: 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