Merge pull request #1880 from volatilityfoundation/release/v2.26.2

Release/v2.26.2
This commit is contained in:
ikelos
2025-09-25 21:10:33 +01:00
committed by GitHub
124 changed files with 6705 additions and 5560 deletions
+31 -30
View File
@@ -4,47 +4,48 @@ on:
branches:
- stable
- develop
- 'release/**'
- "release/**"
pull_request:
branches:
- stable
- 'release/**'
- "release/**"
workflow_dispatch:
jobs:
exe:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- 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
- name: Pyinstall executable
run: |
pyinstaller --clean -y vol.spec
pyinstaller --clean -y volshell.spec
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
pip install -e .[full,cloud]
- name: Move files
run: |
mv dist/vol.exe vol.exe
mv dist/volshell.exe volshell.exe
- name: Pyinstall executable
run: |
pyinstaller --clean -y vol.spec
pyinstaller --clean -y volshell.spec
- name: Archive
uses: actions/upload-artifact@v4
with:
name: volatility3-pyinstaller
path: |
vol.exe
volshell.exe
README.md
LICENSE.txt
- 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
+30 -31
View File
@@ -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}}"
+54 -47
View File
@@ -1,63 +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 Cmake build
pip install .[test]
- 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: |
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 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
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: 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: |
# VolShell
pytest ./test/plugins/windows/windows.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
pytest ./test/plugins/linux/linux.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -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
# Volatility
pytest ./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 ./test/plugins/linux/linux.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v --durations=0
# 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: Clean up post-test
run: |
rm -rf test_images
cd volatility3/symbols
rm -rf linux
rm -rf linux.zip
cd -
- 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 -
+3
View File
@@ -43,3 +43,6 @@ ENV/
# PyTest cache files
.pytest_cache/
# Coverage cache
.coverage
+104
View File
@@ -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 dont 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 thats 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.
+31 -27
View File
@@ -14,10 +14,33 @@ 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.
## Quick Start
1. Install the required dependencies:
```shell
pip install --user -e ".[full]"
```
2. See available options:
```shell
vol -h
```
3. To get more information on a Windows memory sample and to make sure Volatility supports that sample type, run `vol -f <imagepath> windows.info`:
```shell
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 `vol <plugin> -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).
@@ -38,38 +61,19 @@ python3 -m venv venv && . venv/bin/activate
pip install -e ".[dev]"
```
## Quick Start
1. Install Volatility 3 as documented in the Installing section of the readme.
2. See available options:
```shell
vol -h
```
3. To get more information on a Windows memory sample and to make sure Volatility supports that sample type, run `vol -f <imagepath> windows.info`:
```shell
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 `vol <plugin> -h` for more information on a particular command.
## Symbol Tables
Symbol table packs for the various operating systems are available for download at:
<https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip>
<https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>
<https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip>
<https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip>
<https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>
<https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip>
The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at:
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA256SUMS>
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA1SUMS>
<https://downloads.volatilityfoundation.org/volatility3/symbols/MD5SUMS>
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA256SUMS>
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA1SUMS>
<https://downloads.volatilityfoundation.org/volatility3/symbols/MD5SUMS>
Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file).
+1 -1
View File
@@ -339,7 +339,7 @@ if __name__ == "__main__":
"--vol3path",
type=str,
default=os.path.join(os.getcwd(), "volatility3"),
help="Path ot the volatility 3 directory",
help="Path to the volatility 3 directory",
)
parser.add_argument(
"--vol2path",
+159 -91
View File
@@ -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 <https://github.com/microsoft/avml>`_
* `LiME - Linux Memory Extract <https://github.com/504ensicsLabs/LiME>`_
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 <https://github.com/504ensicsLabs/LiME/issues/111>`_.
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 <https://github.com/Abyss-W4tcher/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 <https://isf-server.techanarchy.net/>`_ ,
which is built and maintained by `kevthehermit <https://twitter.com/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 plugins may
be added. For a complete reference, please see the volatility 3 :doc:`list of plugins <volatility3.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 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 <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/insomnihack/2020/09/17/Insomihack-getdents.html>`_.
Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/dfir/insomnihack-teaser-2020-getdents/>`_.
.. 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 <https://github.com/stuxnet999/>`_ 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 an 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 on the 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 systems 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.
+1 -1
View File
@@ -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 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
@@ -27,7 +27,7 @@ For plugin requests, please create an issue with a description of the requested
windows.crashinfo.Crashinfo
windows.dlllist.DllList
.. note:: Here the the command is piped to grep and head 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
-------------
@@ -97,7 +97,7 @@ windows.pstree
``windows.pstree`` helps to display the parent-child relationships between processes.
.. note:: Here the the command is piped to head 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
~~~~~~~~~~~~~~~~
+2 -1
View File
@@ -45,13 +45,14 @@ dev = [
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>=2.0.0,<3",
"sphinx-autodoc-typehints>=3.0.0,<4",
"sphinx-rtd-theme>=3.0.1,<4",
]
+13 -10
View File
@@ -29,7 +29,7 @@ class TestLinuxPslist:
class TestLinuxCheckIdt:
def test_linux_generic_check_idt(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.check_idt.Check_idt", image, volatility, python
"linux.malware.check_idt.Check_idt", image, volatility, python
)
assert rc == 0
@@ -41,7 +41,7 @@ class TestLinuxCheckIdt:
class TestLinuxCheckSyscall:
def test_linux_generic_check_syscall(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.check_syscall.Check_syscall", image, volatility, python
"linux.malware.check_syscall.Check_syscall", image, volatility, python
)
assert rc == 0
@@ -89,7 +89,7 @@ class TestLinuxProcMaps:
class TestLinuxTtyCheck:
def test_linux_generic_tty_check(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.tty_check.tty_check", image, volatility, python
"linux.malware.tty_check.Tty_Check", image, volatility, python
)
assert rc == 0
@@ -200,7 +200,7 @@ class TestLinuxCapabilities:
class TestLinuxCheckCreds:
def test_linux_generic_check_creds(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.check_creds.Check_creds", image, volatility, python
"linux.malware.check_creds.Check_creds", image, volatility, python
)
# linux-sample-1.bin has no processes sharing credentials.
@@ -251,7 +251,7 @@ class TestLinuxKthreads:
class TestLinuxMalfind:
def test_linux_generic_malfind(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.malfind.Malfind", image, volatility, python
"linux.malware.malfind.Malfind", image, volatility, python
)
# linux-sample-1.bin has no process memory ranges with potential injected code.
@@ -426,7 +426,7 @@ class TestLinuxPageCacheInodepages:
class TestLinuxCheckAfinfo:
def test_linux_generic_check_afinfo(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.check_afinfo.Check_afinfo", image, volatility, python
"linux.malware.check_afinfo.Check_afinfo", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
@@ -438,7 +438,7 @@ class TestLinuxCheckAfinfo:
class TestLinuxCheckModules:
def test_linux_generic_check_modules(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.check_modules.Check_modules", image, volatility, python
"linux.malware.check_modules.Check_modules", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
@@ -479,7 +479,10 @@ class TestLinuxIomem:
class TestLinuxKeyboardNotifiers:
def test_linux_generic_keyboard_notifiers(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python
"linux.malware.keyboard_notifiers.Keyboard_notifiers",
image,
volatility,
python,
)
# linux-sample-1.bin has no suspicious results for this plugin.
@@ -501,7 +504,7 @@ class TestLinuxKmesg:
class TestLinuxNetfilter:
def test_linux_generic_netfilter(self, image, volatility, python):
rc, out, _err = test_volatility.runvol_plugin(
"linux.netfilter.Netfilter", image, volatility, python
"linux.malware.netfilter.Netfilter", image, volatility, python
)
# linux-sample-1.bin has no suspicious results for this plugin.
@@ -525,7 +528,7 @@ class TestLinuxHiddenModules:
# 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.hidden_modules.Hidden_modules", image, volatility, python
"linux.malware.hidden_modules.Hidden_modules", image, volatility, python
)
# linux-sample-1.bin has no hidden modules.
+56 -1
View File
@@ -59,6 +59,16 @@ class TestWindowsPslist:
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
@@ -746,11 +756,56 @@ class TestWindowsKPCRs:
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.ldrmodules.LdrModules",
"windows.malware.ldrmodules.LdrModules",
image,
volatility,
python,
+3
View File
@@ -505,6 +505,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)
+2 -2
View File
@@ -49,7 +49,7 @@ def hex_bytes_as_text(value: bytes, width: int = 16) -> str:
output += "\n"
printables = ""
# Handle leftovers when the length is not mutiple of width
# Handle leftovers when the length is not a multiple of width
if printables:
padding = width - len(printables)
output += " " * padding
@@ -182,7 +182,7 @@ class LayerDataRenderer(CLITypeRenderer):
output += "\n"
printables = ""
# Handle leftovers when the length is not mutiple of width
# Handle leftovers when the length is not a multiple of width
if printables:
padding = self.width - len(printables)
output += " " * padding
+92 -3
View File
@@ -85,7 +85,7 @@ class Volshell(interfaces.plugins.PluginInterface):
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.
@@ -93,6 +93,9 @@ class Volshell(interfaces.plugins.PluginInterface):
Return a TreeGrid but this is always empty since the point of this plugin is to run interactively
"""
if additional_locals is None:
additional_locals = {}
# Try to enable tab completion
if not has_ipython:
try:
@@ -184,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),
@@ -479,7 +485,7 @@ class Volshell(interfaces.plugins.PluginInterface):
return
if hasattr(volobject.vol, "members"):
# display the header for this object, if the orginal object was just a type string, display the type information
# 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 = ":"
@@ -552,7 +558,7 @@ class Volshell(interfaces.plugins.PluginInterface):
)
else: # simple type with no members, only one line to print
# if the orginal object was just a type string, display the type information
# 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))
@@ -794,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"""
+8 -14
View File
@@ -13,7 +13,7 @@ import os
import traceback
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]
@@ -48,19 +48,13 @@ 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(
f"Framework interface version {interface_version()[0]} is incompatible with required version {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:
@@ -218,4 +212,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")
@@ -507,7 +507,7 @@ def load_cache_manager(cache_file: Optional[str] = None) -> CacheManagerInterfac
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-existant cache file provided")
raise ValueError("Non-existent cache file provided")
with open(cache_file, "rb") as fp:
header = fp.read(4)
if header not in [b"SQLi"]:
@@ -14,7 +14,7 @@ import os
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__)
@@ -551,7 +551,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
) -> 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
@@ -593,13 +593,14 @@ 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,
+1 -1
View File
@@ -1,7 +1,7 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 26 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_PATCH = 2 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
PACKAGE_VERSION = (
@@ -28,3 +28,5 @@ PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = (
# 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
+1 -1
View File
@@ -287,7 +287,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
+12 -4
View File
@@ -10,8 +10,7 @@ import inspect
from typing import Callable, Tuple
from volatility3.framework import interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework import interfaces, exceptions, versionutils
def method_being_removed(message: str, removal_date: str):
@@ -70,7 +69,7 @@ def deprecated_method(
interfaces.configuration.VersionableInterface,
):
# SemVer check
if not requirements.VersionRequirement.matches_required(
if not versionutils.matches_required(
replacement_version, replacement_base_class.version
):
raise exceptions.VersionMismatchException(
@@ -134,6 +133,15 @@ class PluginRenameClass:
),
)
else:
if not attr.startswith("__"):
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)
+1 -1
View File
@@ -267,7 +267,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
+1 -1
View File
@@ -678,7 +678,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()
+1 -1
View File
@@ -136,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):
+2 -2
View File
@@ -102,7 +102,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
return super().get_requirements() + [
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
@@ -115,7 +115,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
cls, base_layer: interfaces.layers.DataLayerInterface, name: str = ""
):
header = base_layer.read(0, 8)
if header[:4] != b"\x51\x45\x56\x4D":
if header[:4] != b"\x51\x45\x56\x4d":
raise exceptions.LayerException(name, "No QEMU magic bytes")
if header[4:] != b"\x00\x00\x00\x03":
raise exceptions.LayerException(name, "Unsupported QEMU version found")
+1 -1
View File
@@ -152,7 +152,7 @@ def bytes_to_decoded_string(
"""
Args:
data: The `bytes` buffer containing the string of a string at offset 0
encoding: An encoding value for the encoding paramater of `bytes.decode`
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
+1 -1
View File
@@ -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
@@ -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
@@ -1,215 +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 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
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"],
),
]
@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(),
)
@@ -1,71 +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, 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())
@@ -1,168 +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, 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
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)
# 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(),
)
@@ -1,70 +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, Dict, Generator
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, deprecation
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.interfaces import plugins
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 compare_kset_and_lsmod(
cls, context: str, 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]
run = linux_utilities_modules.ModuleDisplayPlugin.run
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
implementation = compare_kset_and_lsmod
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.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=(1, 0, 0),
),
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
@classmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.get_kset_modules,
removal_date="2025-09-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)
@@ -1,214 +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 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
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) -> 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(),
)
_version = (1, 0, 0)
@@ -6,14 +6,9 @@ import io
from dataclasses import dataclass
from typing import Type, List, Dict, Tuple
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import (
format_hints,
TreeGrid,
NotAvailableValue,
UnreadableValue,
)
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
@@ -181,7 +176,7 @@ class Fbdev(interfaces.plugins.PluginInterface):
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
id = "N-A" if isinstance(fb.id, NotAvailableValue) else fb.id
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)
@@ -193,9 +188,9 @@ class Fbdev(interfaces.plugins.PluginInterface):
final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size)
filename = f"{base_filename}.raw"
with open_method(filename) as f:
f.write(final_fb_buffer)
return f.preferred_filename
with open_method(filename) as fp:
fp.write(final_fb_buffer)
return fp.preferred_filename
@classmethod
def parse_fb_info(
@@ -216,7 +211,7 @@ class Fbdev(interfaces.plugins.PluginInterface):
- 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 NotAvailableValue()
id = utility.array_to_string(fb_info.fix.id) or renderers.NotAvailableValue()
color_fields = None
# 0 = color, 1 = grayscale, >1 = FOURCC
@@ -299,14 +294,14 @@ You can try using ffmpeg to decode the raw buffer. Example usage:
vollog.error(
f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".'
)
file_output = UnreadableValue()
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 = NotAvailableValue()
fb_device_name = renderers.NotAvailableValue()
yield (
0,
@@ -334,7 +329,7 @@ You can try using ffmpeg to decode the raw buffer. Example usage:
("Filename", str),
]
return TreeGrid(
return renderers.TreeGrid(
columns,
self._generator(),
)
@@ -1,197 +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.symbols.linux.utilities import (
modules as linux_utilities_modules,
)
from volatility3.framework import interfaces, exceptions, deprecation
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.interfaces import plugins
from volatility3.framework import interfaces, deprecation
from volatility3.plugins.linux.malware import hidden_modules
vollog = logging.getLogger(__name__)
class Hidden_modules(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, 25, 0)
_version = (3, 0, 2)
@classmethod
def find_hidden_modules(
cls, context, vmlinux_module_name: str
) -> extensions.module:
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
)
run = linux_utilities_modules.ModuleDisplayPlugin.run
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
implementation = find_hidden_modules
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="linux_utilities_modules_module_display_plugin",
component=linux_utilities_modules.ModuleDisplayPlugin,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(3, 0, 1),
),
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
@staticmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries,
removal_date="2025-09-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="2025-09-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="2025-09-25",
replacement_version=(3, 0, 0),
)
@staticmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.validate_alignment_patterns,
removal_date="2025-09-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
@@ -1,104 +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
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
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.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(),
)
_version = (1, 0, 0)
+10 -104
View File
@@ -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, 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
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)
_version = (1, 0, 3)
@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,
),
]
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]
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_name != "[vdso]":
data = proc_layer.read(vma.vm_start, 64, pad=True)
yield vma, vma_name, 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(
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 in self._list_injections(task):
if is_32bit_arch:
architecture = "intel"
else:
architecture = "intel64"
disasm = 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_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
)
),
)
@@ -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
"""
@@ -0,0 +1,215 @@
# 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(),
)
@@ -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())
@@ -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(),
)
@@ -0,0 +1,70 @@
# 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, Dict, Generator
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, deprecation
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.interfaces import plugins
vollog = logging.getLogger(__name__)
class Check_modules(plugins.PluginInterface):
"""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: str, 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]
run = linux_utilities_modules.ModuleDisplayPlugin.run
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
implementation = compare_kset_and_lsmod
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.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=(1, 0, 0),
),
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
@classmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.get_kset_modules,
removal_date="2025-09-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)
@@ -0,0 +1,215 @@
# 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(),
)
@@ -0,0 +1,197 @@
# 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, Set, Tuple, Iterable
from volatility3.framework.symbols.linux.utilities import (
modules as linux_utilities_modules,
)
from volatility3.framework import interfaces, exceptions, deprecation
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.interfaces import plugins
vollog = logging.getLogger(__name__)
class Hidden_modules(plugins.PluginInterface):
"""Carves memory to find hidden kernel modules"""
_required_framework_version = (2, 25, 0)
_version = (3, 0, 2)
@classmethod
def find_hidden_modules(
cls, context, vmlinux_module_name: str
) -> extensions.module:
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
)
run = linux_utilities_modules.ModuleDisplayPlugin.run
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
implementation = find_hidden_modules
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="linux_utilities_modules_module_display_plugin",
component=linux_utilities_modules.ModuleDisplayPlugin,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(3, 0, 1),
),
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
@staticmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries,
removal_date="2025-09-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="2025-09-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="2025-09-25",
replacement_version=(3, 0, 0),
)
@staticmethod
@deprecation.deprecated_method(
replacement=linux_utilities_modules.Modules.validate_alignment_patterns,
removal_date="2025-09-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
@@ -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(),
)
@@ -0,0 +1,114 @@
# 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, 3)
@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,
),
]
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]
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_name != "[vdso]":
data = proc_layer.read(vma.vm_start, 64, pad=True)
yield vma, vma_name, 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(
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 in self._list_injections(task):
if is_32bit_arch:
architecture = "intel"
else:
architecture = "intel64"
disasm = 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_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
)
),
)
@@ -0,0 +1,181 @@
# 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, Iterator
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, deprecation, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures
from volatility3.framework.symbols.linux.utilities import tainting
vollog = logging.getLogger(__name__)
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="2025-09-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="2025-09-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(),
)
@@ -0,0 +1,806 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
import logging
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from typing import Iterator, List, Tuple, Optional
from volatility3 import framework
from volatility3.framework import (
constants,
interfaces,
renderers,
exceptions,
deprecation,
)
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
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="2025-09-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())
@@ -0,0 +1,119 @@
# 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(),
)
+10 -170
View File
@@ -1,181 +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 logging
from typing import List, Dict, Iterator
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, deprecation, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures
from volatility3.framework.symbols.linux.utilities import tainting
from volatility3.framework import interfaces, deprecation
from volatility3.plugins.linux.malware import modxview
vollog = logging.getLogger(__name__)
class Modxview(interfaces.plugins.PluginInterface):
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."""
spot modules presence and taints (deprecated)."""
_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="2025-09-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="2025-09-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(),
)
@@ -281,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()
+11 -797
View File
@@ -1,806 +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
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from typing import Iterator, List, Tuple, Optional
from volatility3 import framework
from volatility3.framework import (
constants,
interfaces,
renderers,
exceptions,
deprecation,
)
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols.linux import network
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
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="2025-09-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 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)
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
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)
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)
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_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())
_required_framework_version = (2, 22, 0)
@@ -8,7 +8,7 @@ import datetime
import time
import tarfile
from dataclasses import dataclass, astuple
from typing import IO, List, Set, Type, Iterable, Tuple
from typing import IO, List, Set, Type, Iterable, Tuple, Union
from io import BytesIO
from pathlib import PurePath
@@ -283,9 +283,14 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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
@@ -318,9 +323,13 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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))
if follow_symlinks:
file_path = cls._follow_symlink(file_inode_ptr, file_path)
@@ -541,6 +550,7 @@ class InodePages(plugins.PluginInterface):
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:
@@ -569,6 +579,7 @@ class InodePages(plugins.PluginInterface):
page_index,
dump_safe,
page_flags,
filename,
)
yield 0, fields
@@ -610,6 +621,7 @@ class InodePages(plugins.PluginInterface):
vollog.error("The inode is not a regular file")
return None
filename = renderers.NotApplicableValue()
if self.config["dump"]:
open_method = self.open
inode_address = inode.vol.offset
@@ -618,8 +630,7 @@ class InodePages(plugins.PluginInterface):
self.write_inode_content_to_file(
self.context, vmlinux_layer.name, inode, filename, open_method
)
else:
yield from self._generate_inode_fields(inode, vmlinux_layer)
yield from self._generate_inode_fields(inode, vmlinux_layer, filename)
def run(self):
headers = [
@@ -629,6 +640,7 @@ class InodePages(plugins.PluginInterface):
("Index", int),
("DumpSafe", bool),
("Flags", str),
("Output File", str),
]
return renderers.TreeGrid(
+19 -9
View File
@@ -34,7 +34,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular linux memory image."""
_required_framework_version = (2, 13, 0)
_version = (4, 1, 0)
_version = (4, 1, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -262,17 +262,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 not task.is_valid():
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)
if filter_func(task):
continue
if not task.is_valid():
continue
yield task
if filter_func(task):
continue
if include_threads:
yield from task.get_threads()
yield task
if include_threads:
yield from task.get_threads()
def run(self):
pids = self.config.get("pid")
@@ -475,12 +475,6 @@ class Sockstat(plugins.PluginInterface):
requirements.VersionRequirement(
name="linux_net", component=network.NetSymbols, version=(1, 0, 0)
),
requirements.BooleanRequirement(
name="unix",
description=("Show UNIX domain Sockets only"),
default=False,
optional=True,
),
requirements.ListRequirement(
name="pids",
description="Filter results by process IDs. "
@@ -48,7 +48,7 @@ class FtraceOpsFlags(Enum):
@dataclass
class ParsedFtraceOps:
"""Parsed ftrace_ops struct representation, containing a selection of forensics valuable
informations."""
information."""
ftrace_ops_offset: int
callback_symbol: str
@@ -18,7 +18,7 @@ class PerfEvents(plugins.PluginInterface):
"""Lists performance events for each process."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -28,8 +28,8 @@ class PerfEvents(plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(4, 0, 0)
),
]
@@ -21,7 +21,7 @@ vollog = logging.getLogger(__name__)
@dataclass
class ParsedTracepointFunc:
"""Parsed tracepoint_func struct, containing a selection of forensics valuable
informations."""
information."""
tracepoint_name: str
tracepoint_address: int
+11 -109
View File
@@ -1,118 +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
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
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.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(),
)
_version = (1, 0, 0)
@@ -91,7 +91,7 @@ class VmaRegExScan(plugins.PluginInterface):
):
result_data = proc_layer.read(offset, self.MAXSIZE_DEFAULT, pad=True)
# reapply the regex in order to extact just the match
# reapply the regex in order to extract just the match
regex_result = re.match(regex_pattern, result_data)
if regex_result:
@@ -39,9 +39,6 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
+6 -6
View File
@@ -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
@@ -135,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:
@@ -180,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:
@@ -224,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:
@@ -259,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:
@@ -297,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:
+2 -2
View File
@@ -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)
),
]
+25 -11
View File
@@ -46,38 +46,52 @@ class RegExScan(plugins.PluginInterface):
),
]
def _generator(self, regex_pattern):
regex_pattern = bytes(regex_pattern, "UTF-8")
vollog.debug(f"RegEx Pattern: {regex_pattern}")
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}")
layer = self.context.layers[self.config["primary"]]
for offset in layer.scan(
context=self.context, scanner=scanners.RegExScanner(regex_pattern)
context=context, scanner=scanners.RegExScanner(pattern_bytes)
):
result_data = layer.read(offset, self.MAXSIZE_DEFAULT, pad=True)
result_data = layer.read(offset, maxsize, pad=True)
# reapply the regex in order to extact just the match
regex_result = re.match(regex_pattern, result_data)
# 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_DEFAULT)
# 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_DEFAULT)
# 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(self.config.get("pattern")),
self._generator(context, layer_name, pattern, maxsize),
)
+1 -1
View File
@@ -54,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)
@@ -48,7 +48,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
name="driverirp", component=driverirp.DriverIrp, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="handles", component=handles.Handles, version=(3, 0, 0)
name="handles", component=handles.Handles, version=(4, 0, 0)
),
]
@@ -1,28 +1,13 @@
# 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 interfaces, deprecation
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
from volatility3.plugins.windows.malware import direct_system_calls
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
@@ -38,19 +23,14 @@ syscall_finder_type = namedtuple(
],
)
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' instrunction 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"""
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)
@@ -81,392 +61,3 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
# 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 instrunction 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(),
)
@@ -1,101 +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, modules
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.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(),
)
@@ -5,18 +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,
exceptions,
constants,
renderers,
)
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import handles
from volatility3.plugins.windows import pslist
from volatility3.plugins.windows import handles, pslist
vollog = logging.getLogger(__name__)
@@ -76,7 +70,7 @@ class DumpFiles(interfaces.plugins.PluginInterface):
name="pslist", component=pslist.PsList, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="handles", component=handles.Handles, version=(3, 0, 0)
name="handles", component=handles.Handles, version=(4, 0, 0)
),
]
@@ -231,14 +225,11 @@ 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,
kernel_module_name=self.config["kernel"],
)
cookie = handles_plugin.find_cookie(
cookie = handles.Handles.find_cookie(
context=self.context,
kernel_module_name=self.config["kernel"],
)
@@ -255,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":
@@ -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(),
)
@@ -3,7 +3,7 @@
#
import logging
from typing import Dict, List, Optional
from typing import Dict, Iterator, List, Optional
from volatility3.framework import constants, exceptions, interfaces, renderers, symbols
from volatility3.framework.configuration import requirements
@@ -18,13 +18,9 @@ class Handles(interfaces.plugins.PluginInterface):
"""Lists process open handles."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 0)
_version = (4, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._type_map = None
self._cookie = None
self._level_mask = 7
LEVEL_MASK = 7
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -54,18 +50,27 @@ class Handles(interfaces.plugins.PluginInterface):
),
]
def _get_item(self, handle_table_entry, handle_value):
"""Given a handle table entry (_HANDLE_TABLE_ENTRY) structure from a
@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."""
_OBJECT_HEADER can be found, and construct and return the _OBJECT_HEADER
"""
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")
@@ -78,7 +83,7 @@ class Handles(interfaces.plugins.PluginInterface):
except AttributeError:
# starting with windows 8
is_64bit = symbols.symbol_table_is_64bit(
context=self.context, symbol_table_name=kernel.symbol_table_name
context=context, symbol_table_name=kernel.symbol_table_name
)
if is_64bit:
@@ -104,7 +109,7 @@ class Handles(interfaces.plugins.PluginInterface):
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,
@@ -205,11 +210,23 @@ class Handles(interfaces.plugins.PluginInterface):
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"]]
kernel = context.modules[kernel_module_name]
if level > 0:
subtype = kernel.get_type("pointer")
@@ -218,7 +235,7 @@ class Handles(interfaces.plugins.PluginInterface):
subtype = kernel.get_type("_HANDLE_TABLE_ENTRY")
count = 0x1000 / subtype.size
if not self.context.layers[kernel.layer_name].is_valid(offset):
if not context.layers[kernel.layer_name].is_valid(offset):
return None
table = kernel.object(
@@ -229,7 +246,7 @@ class Handles(interfaces.plugins.PluginInterface):
absolute=True,
)
layer_object = self.context.layers[kernel.layer_name]
layer_object = context.layers[kernel.layer_name]
masked_offset = offset & layer_object.maximum_address
for i in range(len(table)):
@@ -243,11 +260,13 @@ class Handles(interfaces.plugins.PluginInterface):
# 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 self.context.layers[kernel.layer_name].is_valid(entry.vol.offset):
if not context.layers[kernel.layer_name].is_valid(entry.vol.offset):
continue
if level > 0:
yield from self._make_handle_array(entry, level - 1, depth)
yield from cls._make_handle_array(
context, kernel_module_name, entry, level - 1, depth
)
depth += 1
else:
handle_multiplier = 4
@@ -258,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
@@ -272,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,
@@ -283,7 +313,9 @@ class Handles(interfaces.plugins.PluginInterface):
)
return None
yield from self._make_handle_array(TableCode, table_levels)
yield from cls._make_handle_array(
context, kernel_module_name, TableCode, table_levels
)
def _generator(self, procs):
type_map = self.get_type_map(
@@ -306,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:
@@ -1,222 +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__)
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"""
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=(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 = "<Non-File Backed Region>"
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,
)
),
)
_version = (1, 0, 0)
@@ -1,119 +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 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 import direct_system_calls
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):
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)
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("<I", jmp_address_str[2:])[0]
if not relative_offset or relative_offset == -1:
return None
# compute the target address of the jmp (dereference)
jmp_address = inst.address + relative_offset + 6
try:
jmp_target_str = proc_layer.read(jmp_address, 8)
except exceptions.InvalidAddressException:
return None
# compute from the target address then read from it
jmp_target_address = struct.unpack("<Q", jmp_target_str)[0]
try:
jmp_target = proc_layer.read(jmp_target_address, 2)
except exceptions.InvalidAddressException:
return None
# check that the address points to a 'syscall' instruction
if jmp_target == b"\x0f\x05":
return jmp_target_address
return None
@@ -1,128 +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=(3, 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))
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,
kernel_module_name=self.config["kernel"],
filter_func=filter_func,
)
),
)
+10 -283
View File
@@ -1,293 +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 typing import Iterable, Generator, Tuple
from volatility3.framework import interfaces, symbols, 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
from volatility3.framework import interfaces, deprecation
from volatility3.plugins.windows.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, 22, 0)
_version = (1, 1, 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.BooleanRequirement(
name="dump",
description="Extract injected VADs",
default=False,
optional=True,
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0)
),
]
@classmethod
def is_vad_empty(cls, proc_layer, vad):
"""Check if a VAD region is either entirely unavailable due to paging,
entirely consisting of zeros, or a combination of the two. This helps
ignore false positives whose VAD flags match task._injection_filter
requirements but there's no data and thus not worth reporting it.
Args:
proc_layer: the process layer
vad: the MMVAD structure to test
Returns:
A boolean indicating whether a vad is empty or not
"""
CHUNK_SIZE = 0x1000
all_zero_page = b"\x00" * CHUNK_SIZE
offset = 0
vad_length = vad.get_size()
while offset < vad_length:
next_addr = vad.get_start() + offset
if (
proc_layer.is_valid(next_addr, CHUNK_SIZE)
and proc_layer.read(next_addr, CHUNK_SIZE) != all_zero_page
):
return False
offset += CHUNK_SIZE
return True
@classmethod
def list_injections(
cls,
context: interfaces.context.ContextInterface,
kernel_layer_name: str,
symbol_table: str,
proc: interfaces.objects.ObjectInterface,
) -> 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 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(
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.warning(
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[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 = 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,
)
),
)
@@ -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
"""
@@ -0,0 +1,472 @@
# 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, <system call number>
# 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(),
)
@@ -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(),
)
@@ -0,0 +1,223 @@
# 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 = "<Non-File Backed Region>"
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,
)
),
)
@@ -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("<I", jmp_address_str[2:])[0]
if not relative_offset or relative_offset == -1:
return None
# compute the target address of the jmp (dereference)
jmp_address = inst.address + relative_offset + 6
try:
jmp_target_str = proc_layer.read(jmp_address, 8)
except exceptions.InvalidAddressException:
return None
# compute from the target address then read from it
jmp_target_address = struct.unpack("<Q", jmp_target_str)[0]
try:
jmp_target = proc_layer.read(jmp_target_address, 2)
except exceptions.InvalidAddressException:
return None
# check that the address points to a 'syscall' instruction
if jmp_target == b"\x0f\x05":
return jmp_target_address
return None
@@ -0,0 +1,128 @@
# 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 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
vollog = logging.getLogger(__name__)
class LdrModules(interfaces.plugins.PluginInterface):
"""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=(3, 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))
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,
kernel_module_name=self.config["kernel"],
filter_func=filter_func,
)
),
)
@@ -0,0 +1,293 @@
# 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 logging
from typing import Iterable, Generator, Tuple
from volatility3.framework import interfaces, symbols, 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 Malfind(interfaces.plugins.PluginInterface):
"""Lists process memory ranges that potentially contain injected code."""
_required_framework_version = (2, 22, 0)
_version = (1, 1, 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.BooleanRequirement(
name="dump",
description="Extract injected VADs",
default=False,
optional=True,
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0)
),
]
@classmethod
def is_vad_empty(cls, proc_layer, vad):
"""Check if a VAD region is either entirely unavailable due to paging,
entirely consisting of zeros, or a combination of the two. This helps
ignore false positives whose VAD flags match task._injection_filter
requirements but there's no data and thus not worth reporting it.
Args:
proc_layer: the process layer
vad: the MMVAD structure to test
Returns:
A boolean indicating whether a VAD is empty or not
"""
CHUNK_SIZE = 0x1000
all_zero_page = b"\x00" * CHUNK_SIZE
offset = 0
vad_length = vad.get_size()
while offset < vad_length:
next_addr = vad.get_start() + offset
if (
proc_layer.is_valid(next_addr, CHUNK_SIZE)
and proc_layer.read(next_addr, CHUNK_SIZE) != all_zero_page
):
return False
offset += CHUNK_SIZE
return True
@classmethod
def list_injections(
cls,
context: interfaces.context.ContextInterface,
kernel_layer_name: str,
symbol_table: str,
proc: interfaces.objects.ObjectInterface,
) -> 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,
)
),
)
@@ -0,0 +1,220 @@
# 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,
)
),
)
@@ -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(),
)
@@ -0,0 +1,686 @@
# 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,
)
),
)
@@ -0,0 +1,221 @@
# 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 = "<Non-File Backed Region>"
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(),
)
@@ -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]
@@ -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(),
)
@@ -85,7 +85,7 @@ class Threads(thrdscan.ThrdScan):
# 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, ...)
# kernel processes (MemCompression, Registry, ...)
if pid != 4 and ppid != 4:
continue
@@ -37,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]
@@ -243,7 +243,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 7, 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 wil now throw a ValueError if the provided wanted symbol information does not follow the spec
# 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
@@ -649,7 +649,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
and wanted_addresses_identifier not in wanted_symbols
):
vollog.warning(
"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
@@ -671,7 +671,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
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, wanted_value, symbol_value
else:
@@ -131,7 +131,7 @@ class PoolScanner(plugins.PluginInterface):
"""A generic pool scanner plugin."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 0)
_version = (3, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -142,7 +142,7 @@ class PoolScanner(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="handles", component=handles.Handles, version=(3, 0, 0)
name="handles", component=handles.Handles, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="pool_header_scanner",
@@ -343,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.NONPAGED | PoolType.FREE,
page_type=PoolType.PAGED | PoolType.FREE,
),
# symlinks on windows starting with windows 8
PoolConstraint(
@@ -351,7 +351,7 @@ class PoolScanner(plugins.PluginInterface):
type_name=symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK",
object_type="SymbolicLink",
size=(72, None),
page_type=PoolType.NONPAGED | PoolType.FREE,
page_type=PoolType.PAGED | PoolType.FREE,
),
# registry hives
PoolConstraint(
@@ -1,220 +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 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
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 or Vads that are DeleteOnClose"""
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)."""
_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 addreses 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,
)
),
)
_version = (1, 0, 0)
@@ -6,13 +6,13 @@ import datetime
import logging
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__)
@@ -24,7 +24,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
# 3.0.0 - changed signature for `list_processes`
_version = (3, 0, 0)
_version = (3, 0, 1)
PHYSICAL_DEFAULT = False
@classmethod
@@ -261,9 +261,18 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
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"]]
+14 -228
View File
@@ -1,29 +1,24 @@
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, 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,
)
from volatility3.framework import interfaces, deprecation
from volatility3.plugins.windows.malware import psxview
vollog = logging.getLogger(__name__)
class PsXView(plugins.PluginInterface):
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.
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."""
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.
@@ -35,212 +30,3 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter
_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=(3, 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] = []
handles_plugin = handles.Handles(
context=self.context, config_path=self.config_path
)
type_map = handles_plugin.get_type_map(
context=self.context, kernel_module_name=self.config["kernel"]
)
cookie = handles_plugin.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_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):
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(),
)
@@ -350,12 +350,12 @@ class Hashdump(interfaces.plugins.PluginInterface):
if not user_key:
return []
return [k for k in user_key.get_subkeys() if k.Name != "Names"]
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 necesary to decrypt hashes
Returns the scrambled bootkey necessary to decrypt hashes
"""
cs = 1
lsa_base = f"ControlSet{cs:03}" + "\\Control\\Lsa"
@@ -602,7 +602,7 @@ 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.
if the data could not be parsed due to an insufficient number of bytes.
"""
try:
revision, subid_count, id_authority = struct.unpack(
@@ -817,7 +817,7 @@ class TaskTrigger:
_ = reader.read_u4() # timeout seconds
repetition_interval_secs = reader.read_u4()
_ = reader.read_u4() # reptition duration seconds
_ = reader.read_u4() # repetition duration seconds
_ = reader.read_u4() # repetition duration seconds 2
_ = reader.read_bool() # stop at duration end
@@ -174,6 +174,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
@@ -204,7 +206,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",
@@ -216,6 +217,8 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf
if not shim_entry.is_valid():
continue
seen.add(physical_addr)
yield shim_entry
@classmethod
@@ -233,7 +236,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
@@ -579,13 +582,19 @@ 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, kernel_module_name)
if module.BaseDllName.String in module_list
)
except StopIteration:
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 krnl_mod is None:
vollog.warning("Failed to find kernel module")
return None
kernel = context.modules[kernel_module_name]

Some files were not shown because too many files have changed in this diff Show More